//#region src/page/context.ts
function isAstroInput(input) {
	return "Astro" in input;
}
/**
* Build a PublicPageContext from template input.
*/
function createPublicPageContext(input) {
	let url;
	let path;
	let locale;
	if (isAstroInput(input)) {
		url = input.Astro.url.href;
		path = input.Astro.url.pathname;
		locale = input.Astro.currentLocale ?? null;
	} else {
		const parsed = typeof input.url === "string" ? new URL(input.url) : input.url;
		url = parsed.href;
		path = parsed.pathname;
		locale = input.locale ?? null;
	}
	return {
		url,
		path,
		locale,
		kind: input.kind,
		pageType: input.pageType ?? (input.kind === "content" ? "article" : "website"),
		title: input.title ?? null,
		pageTitle: input.pageTitle ?? null,
		description: input.description ?? null,
		canonical: input.canonical ?? null,
		image: input.image ?? null,
		content: input.content ? {
			collection: input.content.collection,
			id: input.content.id,
			slug: input.content.slug ?? null
		} : void 0,
		seo: input.seo,
		articleMeta: input.articleMeta,
		siteName: input.siteName,
		breadcrumbs: input.breadcrumbs,
		siteUrl: input.siteUrl
	};
}

//#endregion
//#region src/page/metadata.ts
/** Schemes safe for use in link href attributes */
const SAFE_HREF_RE = /^(https?|at):\/\//i;
const HTML_ESCAPE_MAP = {
	"&": "&amp;",
	"<": "&lt;",
	">": "&gt;",
	"\"": "&quot;",
	"'": "&#39;"
};
const HTML_ESCAPE_RE = /[&<>"']/g;
/** Escape a string for safe use in an HTML attribute value */
function escapeHtmlAttr(value) {
	return value.replace(HTML_ESCAPE_RE, (ch) => HTML_ESCAPE_MAP[ch] ?? ch);
}
/** Validate that a URL uses a safe scheme (http, https, at) */
function isSafeHref(url) {
	return SAFE_HREF_RE.test(url);
}
const JSONLD_LT_RE = /</g;
const JSONLD_GT_RE = />/g;
const JSONLD_U2028_RE = /\u2028/g;
const JSONLD_U2029_RE = /\u2029/g;
/**
* Safely serialize a value for embedding in a <script type="application/ld+json"> tag.
*
* Plain JSON.stringify is not sufficient because:
* - "<\/script>" in a nested string breaks out of the script tag
* - "<!--" can open an HTML comment
* - U+2028/U+2029 are line terminators in some JS engines
*/
function safeJsonLdSerialize(value) {
	return JSON.stringify(value).replace(JSONLD_LT_RE, "\\u003c").replace(JSONLD_GT_RE, "\\u003e").replace(JSONLD_U2028_RE, "\\u2028").replace(JSONLD_U2029_RE, "\\u2029");
}
/**
* Resolve a flat list of contributions into deduplicated metadata.
* First contribution wins for any given dedupe key.
*/
function resolvePageMetadata(contributions) {
	const result = {
		meta: [],
		properties: [],
		links: [],
		jsonld: []
	};
	const seenMeta = /* @__PURE__ */ new Set();
	const seenProperties = /* @__PURE__ */ new Set();
	const seenLinks = /* @__PURE__ */ new Set();
	const seenJsonLd = /* @__PURE__ */ new Set();
	for (const c of contributions) switch (c.kind) {
		case "meta": {
			const dedupeKey = c.key ?? c.name;
			if (seenMeta.has(dedupeKey)) continue;
			seenMeta.add(dedupeKey);
			result.meta.push({
				name: c.name,
				content: c.content
			});
			break;
		}
		case "property": {
			const dedupeKey = c.key ?? c.property;
			if (seenProperties.has(dedupeKey)) continue;
			seenProperties.add(dedupeKey);
			result.properties.push({
				property: c.property,
				content: c.content
			});
			break;
		}
		case "link":
			if (!isSafeHref(c.href)) {
				if (import.meta.env?.DEV) console.warn(`[page:metadata] Rejected link contribution with unsafe href scheme: ${c.href}`);
				continue;
			}
			if (c.rel === "canonical") {
				if (seenLinks.has("canonical")) continue;
				seenLinks.add("canonical");
			} else {
				const dedupeKey = c.key ?? c.hreflang ?? c.href;
				if (seenLinks.has(dedupeKey)) continue;
				seenLinks.add(dedupeKey);
			}
			result.links.push({
				rel: c.rel,
				href: c.href,
				...c.hreflang && { hreflang: c.hreflang }
			});
			break;
		case "jsonld":
			if (c.id) {
				if (seenJsonLd.has(c.id)) continue;
				seenJsonLd.add(c.id);
			}
			result.jsonld.push({
				id: c.id,
				json: safeJsonLdSerialize(c.graph)
			});
			break;
		default: break;
	}
	return result;
}
/** Render resolved metadata to an HTML string for embedding in <head> */
function renderPageMetadata(metadata) {
	const parts = [];
	for (const m of metadata.meta) parts.push(`<meta name="${escapeHtmlAttr(m.name)}" content="${escapeHtmlAttr(m.content)}">`);
	for (const p of metadata.properties) parts.push(`<meta property="${escapeHtmlAttr(p.property)}" content="${escapeHtmlAttr(p.content)}">`);
	for (const l of metadata.links) {
		let tag = `<link rel="${escapeHtmlAttr(l.rel)}" href="${escapeHtmlAttr(l.href)}"`;
		if (l.hreflang) tag += ` hreflang="${escapeHtmlAttr(l.hreflang)}"`;
		tag += ">";
		parts.push(tag);
	}
	for (const j of metadata.jsonld) parts.push(`<script type="application/ld+json">${j.json}<\/script>`);
	return parts.join("\n");
}

//#endregion
//#region src/page/fragments.ts
/** Escape sequences that would break out of a script tag */
const SCRIPT_CLOSE_RE = /<\//g;
/**
* Filter contributions to a specific placement and deduplicate.
* - Contributions with the same `key + placement` are deduped (first wins).
* - External scripts with the same `src + placement` are deduped.
*/
function resolveFragments(contributions, placement) {
	const filtered = contributions.filter((c) => c.placement === placement);
	const seen = /* @__PURE__ */ new Set();
	const result = [];
	for (const c of filtered) {
		if (c.key) {
			const dedupeKey = `key:${c.key}`;
			if (seen.has(dedupeKey)) continue;
			seen.add(dedupeKey);
		} else if (c.kind === "external-script") {
			const dedupeKey = `src:${c.src}`;
			if (seen.has(dedupeKey)) continue;
			seen.add(dedupeKey);
		}
		result.push(c);
	}
	return result;
}
const EVENT_HANDLER_RE = /^on/i;
function renderAttributes(attrs) {
	return Object.entries(attrs).filter(([k]) => !EVENT_HANDLER_RE.test(k)).map(([k, v]) => ` ${escapeHtmlAttr(k)}="${escapeHtmlAttr(v)}"`).join("");
}
/** Render a single fragment contribution to HTML */
function renderFragment(c) {
	switch (c.kind) {
		case "external-script": {
			let tag = `<script src="${escapeHtmlAttr(c.src)}"`;
			if (c.async) tag += " async";
			if (c.defer) tag += " defer";
			if (c.attributes) tag += renderAttributes(c.attributes);
			tag += "><\/script>";
			return tag;
		}
		case "inline-script": {
			let tag = "<script";
			if (c.attributes) tag += renderAttributes(c.attributes);
			tag += `>${c.code.replace(SCRIPT_CLOSE_RE, "<\\/")}<\/script>`;
			return tag;
		}
		case "html": return c.html;
	}
}
/** Render a list of fragment contributions to an HTML string */
function renderFragments(contributions, placement) {
	return resolveFragments(contributions, placement).map(renderFragment).join("\n");
}

//#endregion
//#region src/page/jsonld.ts
/**
* Remove null/undefined values from a JSON-LD object recursively.
* JSON-LD validators prefer absent keys over null values.
*/
function cleanJsonLd(obj) {
	const cleaned = {};
	for (const [key, value] of Object.entries(obj)) if (value !== void 0 && value !== null) if (typeof value === "object" && !Array.isArray(value)) cleaned[key] = cleanJsonLd(value);
	else cleaned[key] = value;
	return cleaned;
}
/**
* Build a BlogPosting JSON-LD graph from page context.
* Used for article-type content pages.
*/
function buildBlogPostingJsonLd(page) {
	if (page.pageType !== "article" || !page.canonical) return null;
	const ogTitle = page.seo?.ogTitle ?? page.pageTitle ?? page.title;
	const description = page.seo?.ogDescription || page.description;
	const ogImage = page.seo?.ogImage || page.image;
	const publishedTime = page.articleMeta?.publishedTime;
	const modifiedTime = page.articleMeta?.modifiedTime;
	const author = page.articleMeta?.author;
	const siteName = page.siteName;
	return cleanJsonLd({
		"@context": "https://schema.org",
		"@type": "BlogPosting",
		headline: ogTitle,
		description,
		image: ogImage || void 0,
		url: page.canonical,
		datePublished: publishedTime || void 0,
		dateModified: modifiedTime || publishedTime || void 0,
		author: author ? {
			"@type": "Person",
			name: author
		} : void 0,
		publisher: siteName ? {
			"@type": "Organization",
			name: siteName
		} : void 0,
		mainEntityOfPage: {
			"@type": "WebPage",
			"@id": page.canonical
		}
	});
}
/**
* Build a WebSite JSON-LD graph from page context.
* Used for non-article pages (homepage, listing pages, etc.)
*/
function buildWebSiteJsonLd(page) {
	const siteName = page.siteName;
	if (!siteName) return null;
	let siteUrl;
	if (page.siteUrl) siteUrl = page.siteUrl;
	else try {
		siteUrl = new URL(page.url).origin;
	} catch {
		siteUrl = page.canonical || page.url;
	}
	return cleanJsonLd({
		"@context": "https://schema.org",
		"@type": "WebSite",
		name: siteName,
		url: siteUrl
	});
}

//#endregion
//#region src/page/seo-contributions.ts
/**
* Generate base metadata contributions from a page context's SEO data.
* Returns an empty array if no SEO-relevant data is present.
*/
function generateBaseSeoContributions(page) {
	const contributions = [];
	const description = page.description;
	const ogTitle = page.seo?.ogTitle ?? page.pageTitle ?? page.title;
	const ogDescription = page.seo?.ogDescription || description;
	const ogImage = page.seo?.ogImage || page.image;
	const robots = page.seo?.robots;
	const canonical = page.canonical;
	const siteName = page.siteName;
	if (description) contributions.push({
		kind: "meta",
		name: "description",
		content: description
	});
	if (robots) contributions.push({
		kind: "meta",
		name: "robots",
		content: robots
	});
	if (canonical) contributions.push({
		kind: "link",
		rel: "canonical",
		href: canonical
	});
	contributions.push({
		kind: "property",
		property: "og:type",
		content: page.pageType === "article" ? "article" : "website"
	});
	if (ogTitle) contributions.push({
		kind: "property",
		property: "og:title",
		content: ogTitle
	});
	if (ogDescription) contributions.push({
		kind: "property",
		property: "og:description",
		content: ogDescription
	});
	if (ogImage) contributions.push({
		kind: "property",
		property: "og:image",
		content: ogImage
	});
	if (canonical) contributions.push({
		kind: "property",
		property: "og:url",
		content: canonical
	});
	if (siteName) contributions.push({
		kind: "property",
		property: "og:site_name",
		content: siteName
	});
	contributions.push({
		kind: "meta",
		name: "twitter:card",
		content: ogImage ? "summary_large_image" : "summary"
	});
	if (ogTitle) contributions.push({
		kind: "meta",
		name: "twitter:title",
		content: ogTitle
	});
	if (ogDescription) contributions.push({
		kind: "meta",
		name: "twitter:description",
		content: ogDescription
	});
	if (ogImage) contributions.push({
		kind: "meta",
		name: "twitter:image",
		content: ogImage
	});
	if (page.pageType === "article" && page.articleMeta) {
		const { publishedTime, modifiedTime, author } = page.articleMeta;
		if (publishedTime) contributions.push({
			kind: "property",
			property: "article:published_time",
			content: publishedTime
		});
		if (modifiedTime) contributions.push({
			kind: "property",
			property: "article:modified_time",
			content: modifiedTime
		});
		if (author) contributions.push({
			kind: "property",
			property: "article:author",
			content: author
		});
	}
	if (page.pageType === "article") {
		const blogPosting = buildBlogPostingJsonLd(page);
		if (blogPosting) contributions.push({
			kind: "jsonld",
			id: "primary",
			graph: blogPosting
		});
	} else if (siteName) {
		const webSite = buildWebSiteJsonLd(page);
		if (webSite) contributions.push({
			kind: "jsonld",
			id: "primary",
			graph: webSite
		});
	}
	return contributions;
}

//#endregion
//#region src/page/index.ts
/**
* Get the page runtime from Astro locals. Returns undefined when
* EmDash is not initialized (components render nothing in that case).
*/
function getPageRuntime(locals) {
	const emdash = locals.emdash;
	if (emdash && typeof emdash === "object" && "collectPageMetadata" in emdash && "collectPageFragments" in emdash) return emdash;
}

//#endregion
export { buildBlogPostingJsonLd, buildWebSiteJsonLd, cleanJsonLd, createPublicPageContext, escapeHtmlAttr, generateBaseSeoContributions, getPageRuntime, renderFragments, renderPageMetadata, resolveFragments, resolvePageMetadata, safeJsonLdSerialize };
//# sourceMappingURL=index.mjs.map