@contentrain/emitter-astro 0.18.41 → 0.18.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["addressKey","addressKey"],"sources":["../src/types.ts","../src/images.ts","../src/util.ts","../src/pages.ts","../src/alternates.ts","../src/noindex.ts","../src/scaffold.ts","../src/balance.ts","../src/seo.ts","../src/feed.ts","../src/layouts.ts","../src/embed.ts","../src/ui-strings.ts","../src/components.ts","../src/chrome.ts","../src/css.ts","../src/redirects.ts","../src/published.ts","../src/seo-entry.ts","../src/index.ts"],"sourcesContent":["import type { ContentStatus, EntrySourceRef, ProjectIR, RawRedirect, RuntimeBinding } from '@contentrain/types'\n\n// ─── Emit input ───\n\n/**\n * One piece of content ready to render: body already HTML, assets already\n * rewritten to local paths, dates already formatted in the site's observed\n * formats. The emitter renders; it does not fetch, localize, or format —\n * those are producer concerns, and keeping them out is what makes this\n * package portable.\n */\n/** A term as a list card prints it: its name and the archive it links to. */\nexport interface EmitTermRef {\n name: string\n link?: string\n}\n\nexport interface EmitPost {\n slug: string\n title: string\n /** Rendered, asset-rewritten HTML body. */\n body: string\n /**\n * Route parameters for this post beyond `slug` — the date parts of a\n * `/%year%/%monthnum%/%day%/%postname%/` permalink, a post id, anything the\n * route pattern names. Without them every post but the template one would be\n * generated at the wrong address.\n */\n params?: Record<string, string>\n /**\n * Stylesheets only this page loads (page-builder sites emit CSS per page).\n * Family-level stylesheets stay on `LayoutFamily.css.files`.\n */\n css?: string[]\n /** The site's date-format catalog applied to this post's date (index-aligned with `date{n}` marks). */\n dates?: string[]\n author?: string\n /**\n * The author's page — their archive on the site, absolute or site-root-\n * relative. The Article's author gets it as `url`, the address the\n * archive's ProfilePage names its Person at, so the two are one person.\n */\n author_url?: string\n author_first?: string\n author_last?: string\n /**\n * Term names, or `{ name, link }` objects for themes whose term lists link\n * each term — a repeat block then reads `item_name` / `item_link`. The\n * `term{n}` and `terms` marks print the name either way.\n */\n terms?: Array<string | EmitTermRef>\n /** Local featured-image file names (largest first). */\n featured?: string[]\n excerpt?: string\n /** Excerpt as source HTML, for themes whose cards keep links and formatting. */\n excerpt_html?: string\n /**\n * Meta description. Falls back to `excerpt` with markup stripped — a card\n * and a description tag want the same sentence, so most producers set neither.\n */\n description?: string\n /**\n * Social image, absolute or site-root-relative. `featured` holds bare file\n * names whose serving path only the producer knows, so a crawler-usable\n * `og:image` needs this; a `featured` entry that is already a path is used.\n */\n image?: string\n /** What `image` is, for the `og:image:*` tags. Describes `image` only — ignored without it. */\n image_meta?: ImageMeta\n /** Canonical override — for a page that should point somewhere else. Default: this page's own address. */\n canonical?: string\n /**\n * The document title (`<title>`, and the default og/twitter title) when the\n * source's SEO plugin composed one apart from the entry's — Yoast's\n * \"Post – Site\". `title` stays the entry's own name: its marks, the Article\n * headline, the last breadcrumb. Default: `title`.\n */\n seo_title?: string\n /** Open Graph text and image the source set for this page, over the derived ones. */\n open_graph?: SocialOverride\n /** Twitter card values the source set for this page. Unset values follow Open Graph. */\n twitter?: TwitterOverride\n /**\n * The page's JSON-LD as the source's SEO plugin rendered it — a `@graph`\n * object, an array of nodes, or one node. Printed instead of the graph the\n * emitter would build, so FAQ, HowTo, Product and whatever else the plugin\n * described survive. The migrated site keeps the source addresses, so the\n * graph's `@id`s and URLs stay true; media URLs in it are printed as given\n * and are the producer's to rewrite. A WebSite `SearchAction` is dropped (a\n * static site has no `?s=` search), as is a copy of the head's WebSite node.\n */\n schema?: unknown\n /** ISO 8601, for Article structured data. `dates` holds display strings, which schema.org cannot read. */\n published_at?: string\n modified_at?: string\n /** Every author, for repeat blocks — `author` stays the first one. */\n authors?: string[]\n /** Producer-supplied extra marks, merged last. */\n marks?: Record<string, unknown>\n /** Locale of this entry, on a multilingual site — overrides the route's. */\n locale?: string\n /**\n * The trail to this page for BreadcrumbList — home first, the page itself\n * NOT included: the emitter appends it with the address it built, so the\n * last crumb cannot disagree with the page. Each path is site-root-relative.\n * Without a trail no BreadcrumbList is printed; a trail with an invalid\n * crumb is dropped with a warning.\n */\n breadcrumbs?: Breadcrumb[]\n /**\n * Content-store address of this post (model, entry id, locale) — what a\n * mounted `comments` component addresses its thread by. Only the tool that\n * wrote the content store knows it; without it the comments region mounts\n * as a placeholder.\n */\n entry?: EntrySourceRef\n /**\n * The source page was `noindex` (e.g. Yoast `robots.index`). The page gets\n * `<meta name=\"robots\" content=\"noindex\">` and is left out of the sitemap —\n * a migration must not make indexable what the site kept out of search.\n */\n noindex?: boolean\n /** The source page was `nofollow`: `<meta name=\"robots\" content=\"nofollow\">`. */\n nofollow?: boolean\n /**\n * The entry's workflow status in the content store (`EntryMeta.status`).\n * Only `published` is built; unset reads as published unless\n * `EmitOptions.requireStatus` is set. Any other status\n * keeps the entry out of the site — no page, sitemap line, feed item,\n * llms.txt link or list card — and `EmitResult.withheld` names it.\n */\n status?: ContentStatus\n /**\n * When a published entry goes live (`EntryMeta.publish_at`, ISO 8601) — a\n * WordPress post scheduled for later. Until then it is held back like a\n * draft; compared with `EmitOptions.now`. A value that is not a date holds\n * the entry back too.\n */\n publish_at?: string\n /**\n * Who may read the entry (the store's `visibility` field — WordPress\n * `public`, `private`, or `password` for a password-protected post). Anything\n * but `public` is never built, whatever `status` says: no page, sitemap\n * line, feed item, list card, share card or link to it. Unset reads as public.\n */\n visibility?: 'public' | 'private' | 'password' | (string & {})\n}\n\n/** An entry the build leaves out because it is not published yet. */\nexport interface WithheldEntry {\n collection: string\n slug: string\n status?: ContentStatus\n publish_at?: string\n visibility?: string\n /**\n * `visibility`: password-protected or private · `status`: not published ·\n * `status_missing`: no status, with `EmitOptions.requireStatus` ·\n * `scheduled`: publish_at is ahead · `publish_at_invalid`: publish_at is not a date.\n */\n reason: 'visibility' | 'status' | 'status_missing' | 'scheduled' | 'publish_at_invalid'\n}\n\n/** A link on a published page to a held-back entry, kept as its text. */\nexport interface WithheldLink {\n /** The page it was on (site-root path), or `<collection>/<slug>` when no route builds it. */\n page: string\n /** The address it pointed at. */\n href: string\n}\n\n/**\n * One block of a list: a template, optional wrapper, and how many items it takes.\n *\n * Themes commonly render the newest post as a big card and the rest as a grid —\n * two different templates in two different containers. One template per list\n * cannot express that: either everything becomes a big card, or the big card is\n * baked into the chrome and page 2 shows the wrong post.\n */\nexport interface ListSection {\n /** Item markup with `@@marks@@`. */\n template: string\n /** Markup around this section's items; `<!--@@items@@-->` marks where they go. */\n wrapper?: string\n /** Items this section takes, in order. Omitted = the remaining items. */\n count?: number\n}\n\n/** One static path of a list route: its params and the items its list renders. */\nexport interface QueryPage {\n params: Record<string, string>\n items: EmitPost[]\n /**\n * Page-level marks for the list page's chrome — a term's display NAME where\n * the route parameter only carries its slug, its description, its count.\n * Without these every category page would print the template category's name.\n */\n marks?: Record<string, unknown>\n /** Stylesheets only this list page loads. */\n css?: string[]\n /**\n * Item markup with `@@mark@@` placeholders (title, date{n}, author, excerpt,\n * feat, slug), extracted from the source list. When present, list pages\n * render each item by filling the template; when absent, a plain fallback\n * list is emitted and a warning recorded — a silent fallback would read as\n * fidelity when it is not.\n *\n * Shorthand for a single-section list; `sections` takes precedence.\n */\n item_template?: string\n /** Lists whose items are not all rendered alike — a big card then a grid. */\n sections?: ListSection[]\n /** Document title for this page (`<title>`), e.g. \"Category: News – Site\". */\n title?: string\n /** Meta description for this list page. */\n description?: string\n /** Social image, absolute or site-root-relative. */\n image?: string\n /** What `image` is, for the `og:image:*` tags. Ignored without `image`. */\n image_meta?: ImageMeta\n /** Canonical override; default is this page's own address. */\n canonical?: string\n /** The trail to this list page, itself excluded — see `EmitPost.breadcrumbs`. */\n breadcrumbs?: Breadcrumb[]\n /** See `EmitPost.noindex`. */\n noindex?: boolean\n /** See `EmitPost.nofollow`. */\n nofollow?: boolean\n /** See `EmitPost.open_graph`. */\n open_graph?: SocialOverride\n /** See `EmitPost.twitter`. */\n twitter?: TwitterOverride\n /** See `EmitPost.schema`. */\n schema?: unknown\n /**\n * On an author's archive: who it is about. The page becomes a ProfilePage\n * whose main entity is this Person, at the page's own address.\n */\n profile?: AuthorProfile\n}\n\n/** An author as their archive page presents them. */\nexport interface AuthorProfile {\n name: string\n /** The author's bio. */\n description?: string\n /** Avatar or photo, absolute or site-root-relative. */\n image?: string\n /** Profiles elsewhere (social accounts, a personal site) — schema.org `sameAs`; http(s) only. */\n same_as?: string[]\n}\n\n/**\n * Share-card values a page's source set by hand. Each one left out falls back\n * to the value derived from the page. `image` is absolute or site-root-relative.\n */\nexport interface SocialOverride {\n title?: string\n description?: string\n image?: string\n}\n\nexport interface TwitterOverride extends SocialOverride {\n /** `summary` or `summary_large_image`; anything else is ignored. */\n card?: string\n}\n\n/** One step of a breadcrumb trail. */\nexport interface Breadcrumb {\n name: string\n /** Site-root-relative (`/category/news/`). */\n path: string\n}\n\n/**\n * A social image's size and type. Share cards lay out before they fetch the\n * image; with `og:image:width` / `height` the first share renders at the right\n * aspect instead of as a placeholder. The producer measured the file — the\n * emitter cannot, it never sees it.\n */\nexport interface ImageMeta {\n /** Pixels. A value that is not a positive integer is left out. */\n width?: number\n height?: number\n /** MIME type, `image/…`. Anything else is left out. */\n type?: string\n}\n\n/** Default collection name when a `single` route does not name one. */\nexport const DEFAULT_COLLECTION = 'posts'\n\nexport interface EmitContent {\n /** The default (`posts`) collection — shorthand for `collections.posts`. */\n posts?: EmitPost[]\n /**\n * Content per collection, for sites with more than one kind of single page\n * (posts, pages, custom post types). A `single` route reads the collection\n * named by `RouteModel.collection`.\n */\n collections?: Record<string, EmitPost[]>\n /** QueryBinding id → the static paths (and items) that query produces. */\n queries?: Record<string, QueryPage[]>\n}\n\n/** A legacy stylesheet provided by the producer, referenced from `LayoutFamily.css.files`. */\nexport interface EmitCssFile {\n path: string\n content: string\n}\n\n/** A host the emitted site may optimize images from (Astro \\`image.remotePatterns\\`). */\nexport interface RemoteImagePattern {\n protocol?: 'https' | 'http'\n /** Exact host, or \\`*.example.com\\` for any subdomain. */\n hostname: string\n /** Path prefix, e.g. \\`/media/\\`. Optional. */\n pathname?: string\n}\n\nexport interface ImageOptions {\n /** Default true. \\`false\\` emits no image pass at all. */\n enabled?: boolean\n /** Hosts whose images are optimized. The runtime host (Studio's CDN) is always added. */\n remotePatterns?: RemoteImagePattern[]\n /** srcset widths in px. Default 480, 768, 1024, 1600; widths above the width the HTML declares are dropped. */\n widths?: number[]\n /** \\`sizes\\` for an image that has none. Default \\`(max-width: 768px) 100vw, 768px\\`. */\n sizes?: string\n}\n\nexport interface EmitOptions {\n /** Generated project's package name. Default: \"migrated-site\". */\n projectName?: string\n /** Include Tailwind 4 as the evolution layer (default true). */\n tailwind?: boolean\n /**\n * Emit per-page SEO — title, description, canonical, Open Graph, Twitter card\n * and Article structured data — and remove the template page's copies of those\n * tags from the head chrome. Default true: inheriting one page's canonical and\n * og:title on every page is worse than having neither. Set false to keep the\n * source head verbatim and own these tags in the producer.\n */\n seo?: boolean\n /**\n * Emit a sitemap and a `robots.txt` that points at it. Default true.\n *\n * The sitemap is generated by `@astrojs/sitemap` at build time rather than\n * computed here, because only the build knows the real set of URLs — every\n * dynamic route, every paginated page, whatever `getStaticPaths` returned. A\n * sitemap the emitter worked out on its own would be a second source of truth\n * that can disagree with the site it describes.\n *\n * Independent of `seo`: a producer that writes its own meta tags has not\n * thereby said it writes its own sitemap, and coupling the two would drop the\n * sitemap silently for exactly that producer.\n */\n sitemap?: boolean\n /**\n * Build-time image optimization for content bodies (\\`astro:assets\\`):\n * srcset, WebP, width/height and lazy loading for images on allowed hosts —\n * migrated media on the runtime host is always allowed — and lazy loading\n * with async decoding for every other image. Default on.\n */\n images?: ImageOptions\n /**\n * Build the site's RSS feed at `/feed.xml` — the newest posts, as WordPress\n * serves them at `/feed/` — and point the theme's head link at it. Default\n * true. Needs `site.url` (feed links are absolute) and a route that builds\n * the `posts` collection.\n */\n feed?: boolean\n /**\n * Build `/llms.txt` (llmstxt.org): the site's name, tagline and newest pages\n * of each collection, for language models. Default true. Needs `site.url`.\n */\n llms?: boolean\n /** The site's tagline (WordPress `blogdescription`): the feed's description and the llms.txt summary. */\n siteDescription?: string\n /**\n * Whether the source site's addresses end in a slash (WordPress\n * `RawRouting.trailing_slash`): `/hello/` or `/hello`. Default true. With\n * false the build writes files (`/hello.html`, which Netlify and Cloudflare\n * Pages serve at `/hello`, and Vercel with the `cleanUrls` this adds to\n * `vercel.json`), and canonical, og:url, hreflang, the feed and llms.txt\n * name every page without the slash — the migrated site keeps the form the\n * source was indexed under.\n */\n trailingSlash?: boolean\n /**\n * The site's interface-text dictionary (see \\`UI_STRINGS_MODEL\\`). Comments\n * and forms read it at build time from \\`dir\\` (default\n * \\`.contentrain/content/site/ui-strings\\`, project-relative).\n *\n * \\`locales\\` declares which languages the producer wrote. The emitter cannot\n * see the store, so this is how it can say at emit time that a page's\n * language has no dictionary; the build reports the same from the files.\n */\n uiStrings?: { dir?: string; locales?: string[] }\n /**\n * The host the site deploys to, for `EmitInput.redirects`: its own redirect\n * file is written so the rules answer with a real HTTP status (a static\n * Astro build serves them as meta-refresh pages, kept as the fallback).\n * Unset: Netlify `public/_redirects` and `vercel.json` are both written; a\n * Cloudflare Pages site must name its host (no forced rules there).\n */\n redirectHost?: 'netlify' | 'cloudflare' | 'vercel'\n /**\n * The moment `EmitPost.publish_at` is compared with, ISO 8601. Default: the\n * time of the emit. The data files hold only what is live then, so a\n * scheduled entry is built by the first emit after its time.\n */\n now?: string\n /**\n * Hold back every entry — in posts, collections and list items — that\n * carries no `EmitPost.status`, and say so. Default false: an entry with\n * no status is published, as before statuses existed. A producer that\n * reads statuses from the content store sets it, so an entry it failed to\n * give one fails closed instead of going out as published.\n */\n requireStatus?: boolean\n}\n\nexport interface EmitInput {\n ir: ProjectIR\n content?: EmitContent\n css?: EmitCssFile[]\n options?: EmitOptions\n /**\n * Where runtime components (comments, forms) talk to. Without it they are\n * emitted as placeholders and a warning names each one — a migration is\n * complete without any runtime offer being accepted.\n */\n runtime?: RuntimeBinding\n /**\n * The live site's redirect rules (`RawIR.redirects`). Plain one-to-one rules\n * are written to `astro.config` `redirects`; patterns, regular expressions,\n * other statuses and rules on an address the site builds a page at come\n * back in `EmitResult.redirects.manual` with the reason, to set up at the host.\n */\n redirects?: RawRedirect[]\n /**\n * Rules served only by the host's redirect file (`public/_redirects`,\n * `vercel.json`), never by `astro.config` — so no meta-refresh page is built\n * for them. For bulk rules the site does not need as pages: one per\n * WordPress attachment page runs to tens of thousands, and a static build\n * would write an HTML file for each. They pass the same checks as\n * `redirects`, and the site's own rules and the feed redirects win an\n * address they share. Without a fallback page, a rule the host file cannot\n * hold — a pattern, or past Cloudflare's or Vercel's rule limit, where these\n * go last — is returned in `EmitResult.redirects.manual`.\n */\n hostRedirects?: RawRedirect[]\n}\n\n// ─── Emit output ───\n\n/**\n * Pure result: every generated file as path → content. Writing to disk is a\n * separate, trivial step (`writeEmit`) so the core stays testable and usable\n * from any host — CLI, service, or another emitter wrapping this one.\n */\nexport interface EmitResult {\n files: Record<string, string>\n warnings: string[]\n /** Present when `EmitInput.redirects` or `hostRedirects` was given. */\n redirects?: {\n /** Rules written: `redirects` to astro.config and the host files, `hostRedirects` to the host files. */\n written: RawRedirect[]\n /** Rules not written, with why — to set up by hand at the host. */\n manual: Array<{ redirect: RawRedirect; reason: string }>\n /** The host redirect files written, e.g. `public/_redirects (netlify)`. Empty: meta-refresh only. */\n host_files: string[]\n /**\n * Rules (by `from`) left out of a Cloudflare / Vercel file at its rule\n * limit. A `redirects` rule keeps its meta-refresh page there. A\n * `hostRedirects` rule is here only with no host named (the Netlify file\n * holds it, `vercel.json` does not); past a named host's limit it is in `manual`.\n */\n host_over_limit: string[]\n }\n /**\n * Present when entries were held back (`EmitPost.status`, `publish_at`):\n * each one, and each link to one that a published page carried — kept as\n * its text. Redirects to a held-back entry are in `redirects.manual`.\n */\n withheld?: {\n entries: WithheldEntry[]\n links: WithheldLink[]\n }\n}\n\nexport type { ProjectIR, RawRedirect, RuntimeBinding, EntrySourceRef }\n","// Image optimization for migrated content (AI-10).\n//\n// Post bodies arrive as finished HTML and render through `set:html`, so\n// Astro's <Image> component cannot reach the images inside them. The\n// emitted site instead runs one build-time pass over each page's content:\n// every <img> on a host the site is allowed to optimize goes through\n// `astro:assets`' `getImage()` — resized to a srcset, re-encoded to WebP,\n// width/height filled in — and every <img>, optimized or not, gets\n// `loading` (the first one eager with `fetchpriority=\"high\"`, it is usually\n// the largest paint; the rest lazy) and `decoding=\"async\"`.\n//\n// Two emitted files:\n// - `src/lib/images.ts` — the pure rewrite, with the optimizer injected. No\n// Astro import, so it is tested and type-checked on its own.\n// - `src/lib/optimize-images.ts` — binds it to `getImage()` and the host\n// allow-list; the only file that imports `astro:assets`.\n\nimport type { EmitOptions, RemoteImagePattern, RuntimeBinding } from './types.js'\n\nexport const DEFAULT_IMAGE_WIDTHS = [480, 768, 1024, 1600]\nexport const DEFAULT_IMAGE_SIZES = '(max-width: 768px) 100vw, 768px'\n\nexport function imagesEnabled(options: EmitOptions): boolean {\n return options.images?.enabled !== false\n}\n\n/**\n * The hosts the site may optimize: the producer's list plus the runtime host,\n * because migrated media is rehosted there. Deduplicated, stable order.\n */\nexport function imagePatterns(options: EmitOptions, runtime?: RuntimeBinding): RemoteImagePattern[] {\n const out: RemoteImagePattern[] = [...(options.images?.remotePatterns ?? [])]\n if (runtime?.base_url) {\n try {\n const url = new URL(runtime.base_url)\n out.push({ protocol: url.protocol === 'http:' ? 'http' : 'https', hostname: url.hostname })\n }\n catch { /* an unparseable base_url adds no pattern */ }\n }\n const seen = new Set<string>()\n return out.filter((p) => {\n const key = `${p.protocol ?? 'https'}|${p.hostname}|${p.pathname ?? ''}`\n if (seen.has(key)) return false\n seen.add(key)\n return true\n })\n}\n\n/** The `image` block for astro.config.mjs — Astro enforces the same allow-list. */\nexport function astroImageConfig(patterns: RemoteImagePattern[]): string | null {\n if (patterns.length === 0) return null\n const list = patterns.map((p) => {\n const parts = [`protocol: ${JSON.stringify(p.protocol ?? 'https')}`, `hostname: ${JSON.stringify(p.hostname)}`]\n if (p.pathname) parts.push(`pathname: ${JSON.stringify(`${p.pathname.replace(/\\/?$/, '/')}**`)}`)\n return `{ ${parts.join(', ')} }`\n })\n return ` image: { remotePatterns: [${list.join(', ')}] },`\n}\n\nexport const IMAGES_TS = `// Emitted by @contentrain/emitter-astro — image pass over content HTML.\n// Pure: the optimizer is injected (see optimize-images.ts), so this file has\n// no Astro dependency and never breaks a page — an image the optimizer\n// cannot handle keeps its original markup.\n\nexport interface OptimizedImage {\n src: string\n srcset?: string\n width?: number\n height?: number\n}\n\nexport type ImageOptimizer = (\n src: string,\n hints: { width?: number; height?: number },\n) => Promise<OptimizedImage | null>\n\n// Quoted attribute values may hold a \">\" (alt=\"a > b\"), so a tag ends at the\n// first \">\" outside quotes. An unquoted value may hold a quote (alt=don't),\n// which a browser reads as part of the value; there the quote-aware match\n// would run on to the next quote and swallow the markup after the tag. A\n// match that reaches a \"<\" is that case, and the tag is read up to its first\n// \">\" instead.\nconst IMG_START = /<img\\\\b/gi\nconst IMG_QUOTED = /<img\\\\b(?:[^>\"']|\"[^\"]*\"|'[^']*')*>/y\nconst IMG_PLAIN = /<img\\\\b[^>]*>/y\n// An unquoted value runs to whitespace or \">\", quotes included, as a browser reads it.\nconst ATTR = /([^\\\\s=/>]+)(?:\\\\s*=\\\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\\\s>]+)))?/g\n\n/** Every <img> tag in the html, with where it starts. */\nfunction imgTags(html: string): Array<{ start: number; tag: string }> {\n const found: Array<{ start: number; tag: string }> = []\n for (const open of html.matchAll(IMG_START)) {\n const start = open.index ?? 0\n if (found.length && start < found[found.length - 1]!.start + found[found.length - 1]!.tag.length) continue\n IMG_QUOTED.lastIndex = start\n let tag = IMG_QUOTED.exec(html)?.[0]\n if (tag === undefined || tag.includes('<', 1)) {\n IMG_PLAIN.lastIndex = start\n tag = IMG_PLAIN.exec(html)?.[0]\n }\n if (tag !== undefined) found.push({ start, tag })\n }\n return found\n}\nconst SKIP_EXT = /\\\\.(svg|gif)(?:$|[?#])/i\n\n// Attribute values are kept as the source wrote them (entities and all) and\n// written back unchanged; only values this pass sets are escaped. Reading a\n// value decodes it: a WordPress src like \"photo.jpg?w=800&amp;ssl=1\" is the\n// URL \"photo.jpg?w=800&ssl=1\".\nfunction parseAttrs(tag: string): Array<[string, string | null]> {\n const inner = tag.replace(/^<img\\\\b/i, '').replace(/\\\\/?>$/, '')\n const attrs: Array<[string, string | null]> = []\n for (const m of inner.matchAll(ATTR)) {\n const name = m[1]\n if (!name) continue\n // Written back double-quoted, so a double quote inside another form is escaped.\n const value = m[2] ?? (m[3] ?? m[4])?.replace(/\"/g, '&quot;')\n attrs.push([name, value === undefined ? null : value])\n }\n return attrs\n}\n\nconst NAMED: Record<string, string> = { amp: '&', quot: '\"', apos: \"'\", lt: '<', gt: '>' }\n\nfunction decodeAttr(value: string): string {\n return value.replace(/&(?:#(\\\\d+)|#x([0-9a-f]+)|([a-z]+));/gi, (entity, dec, hex, name) => {\n if (dec || hex) {\n const code = dec ? Number(dec) : parseInt(hex, 16)\n return code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : entity\n }\n return NAMED[name.toLowerCase()] ?? entity\n })\n}\n\nfunction escapeAttr(value: string): string {\n return value.replace(/&/g, '&amp;').replace(/\"/g, '&quot;')\n}\n\nfunction serialize(attrs: Array<[string, string | null]>): string {\n return \\`<img\\${attrs.map(([n, v]) => (v === null ? \\` \\${n}\\` : \\` \\${n}=\"\\${v}\"\\`)).join('')}>\\`\n}\n\nfunction positiveInt(value: string | null | undefined): number | undefined {\n if (!value) return undefined\n const n = Number(value)\n return Number.isInteger(n) && n > 0 ? n : undefined\n}\n\n/**\n * Rewrite every <img> in the html. The first image is eager with high fetch\n * priority; the rest are lazy. Images the optimizer accepts get its src,\n * srcset and size. An existing loading or decoding attribute is respected.\n */\nexport async function rewriteImages(\n html: string,\n optimize: ImageOptimizer,\n options: { sizes: string },\n): Promise<string> {\n const found = imgTags(html)\n if (!found.length) return html\n const replacements: string[] = []\n for (let index = 0; index < found.length; index++) {\n const tag = found[index]!.tag\n const attrs = parseAttrs(tag)\n const get = (name: string) => {\n const raw = attrs.find(([n]) => n.toLowerCase() === name)?.[1]\n return raw == null ? raw : decodeAttr(raw)\n }\n const has = (name: string) => attrs.some(([n]) => n.toLowerCase() === name)\n const set = (name: string, value: string) => {\n const i = attrs.findIndex(([n]) => n.toLowerCase() === name)\n if (i >= 0) attrs[i] = [attrs[i]![0], escapeAttr(value)]\n else attrs.push([name, escapeAttr(value)])\n }\n const remove = (name: string) => {\n const i = attrs.findIndex(([n]) => n.toLowerCase() === name)\n if (i >= 0) attrs.splice(i, 1)\n }\n\n if (!has('loading')) {\n set('loading', index === 0 ? 'eager' : 'lazy')\n if (index === 0 && !has('fetchpriority')) set('fetchpriority', 'high')\n }\n if (!has('decoding')) set('decoding', 'async')\n\n const src = get('src')\n const keep = has('data-cr-keep') || !src || src.startsWith('data:') || SKIP_EXT.test(src)\n if (!keep) {\n try {\n const result = await optimize(src, { width: positiveInt(get('width')), height: positiveInt(get('height')) })\n if (result) {\n set('src', result.src)\n if (result.srcset) {\n set('srcset', result.srcset)\n if (!has('sizes')) set('sizes', options.sizes)\n } else {\n // The source's own srcset (WordPress's size variants) would win\n // over the optimized src in every browser that reads srcset.\n remove('srcset')\n remove('sizes')\n }\n if (result.width && result.height && !has('width') && !has('height')) {\n set('width', String(result.width))\n set('height', String(result.height))\n }\n }\n }\n catch {\n // Keep the original image: a failed optimization must not cost the page its picture.\n }\n }\n replacements.push(serialize(attrs))\n }\n let out = ''\n let at = 0\n found.forEach(({ start, tag }, i) => {\n out += html.slice(at, start) + replacements[i]\n at = start + tag.length\n })\n return out + html.slice(at)\n}\n`\n\nexport function optimizeImagesTs(patterns: RemoteImagePattern[], widths: number[], sizes: string): string {\n return `// Emitted by @contentrain/emitter-astro — binds the image pass to astro:assets.\n// Only hosts on this list are optimized; Astro enforces the same list\n// (image.remotePatterns in astro.config.mjs). Everything else keeps its URL\n// and still gets lazy loading and async decoding (see images.ts).\nimport { getImage } from 'astro:assets'\nimport { rewriteImages } from './images'\n\nconst PATTERNS: Array<{ protocol: string; hostname: string; pathname?: string }> = ${JSON.stringify(patterns.map((p) => ({ protocol: p.protocol ?? 'https', hostname: p.hostname, ...(p.pathname ? { pathname: p.pathname } : {}) })))}\nconst WIDTHS = ${JSON.stringify(widths.toSorted((a, b) => a - b))}\nconst SIZES = ${JSON.stringify(sizes)}\n\n// Astro downloads a remote image when the build writes its assets — after\n// this pass, outside its error handling — and one image that does not answer\n// 200 fails the whole astro build. So an image is only handed to getImage()\n// once it answered with an image here; anything else keeps its original URL.\n// One check per URL per build.\nconst CHECK_TIMEOUT_MS = 10_000\nconst checked = new Map<string, Promise<boolean>>()\n\nasync function answersWithImage(src: string, method: 'HEAD' | 'GET'): Promise<boolean | null> {\n try {\n const res = await fetch(src, { method, redirect: 'follow', signal: AbortSignal.timeout(CHECK_TIMEOUT_MS) })\n if (method === 'GET') await res.body?.cancel()\n // Some hosts refuse HEAD; ask again with GET.\n if (method === 'HEAD' && (res.status === 405 || res.status === 501 || res.status === 403)) return null\n return res.ok && (res.headers.get('content-type') ?? '').toLowerCase().startsWith('image/')\n }\n catch {\n return method === 'HEAD' ? null : false\n }\n}\n\nfunction reachable(src: string): Promise<boolean> {\n let pending = checked.get(src)\n if (!pending) {\n pending = answersWithImage(src, 'HEAD').then((ok) => ok ?? answersWithImage(src, 'GET').then((retry) => retry === true))\n checked.set(src, pending)\n }\n return pending\n}\n\nfunction allowed(src: string): boolean {\n let url: URL\n try {\n url = new URL(src)\n }\n catch {\n return false\n }\n return PATTERNS.some((p) =>\n url.protocol === \\`\\${p.protocol}:\\`\n && (p.hostname.startsWith('*.') ? url.hostname.endsWith(p.hostname.slice(1)) : url.hostname === p.hostname)\n && (!p.pathname || url.pathname.startsWith(p.pathname)))\n}\n\nexport async function optimizeHtmlImages(html: string): Promise<string> {\n if (!html.includes('<img')) return html\n return rewriteImages(html, async (src, hints) => {\n if (!allowed(src) || !(await reachable(src))) return null\n const sized = hints.width !== undefined && hints.height !== undefined\n const widths = WIDTHS.filter((w) => hints.width === undefined || w <= hints.width)\n const image = await getImage({\n src,\n format: 'webp',\n ...(sized ? { width: hints.width, height: hints.height } : { inferSize: true }),\n ...(widths.length ? { widths } : {}),\n })\n const width = Number(image.attributes.width)\n const height = Number(image.attributes.height)\n return {\n src: image.src,\n srcset: image.srcSet.attribute || undefined,\n ...(width > 0 && height > 0 ? { width, height } : {}),\n }\n }, { sizes: SIZES })\n}\n`\n}\n","/** \"f-archive\" → \"FArchive\", \"posts\" → \"Posts\" — Astro component file names. */\nexport function pascalCase(id: string): string {\n return id\n .split(/[^a-zA-Z0-9]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join('')\n}\n\n/**\n * Route pattern → Astro pages path.\n * `/` → `index.astro`; `/category/:term/page/:page` → `category/[term]/page/[page].astro`;\n * a trailing `*` on a parameter makes it a rest parameter — `/category/:term*`\n * → `category/[...term].astro`, which is what keeps a nested taxonomy address\n * (`/category/about-cc/events/`) from collapsing to its last segment.\n * Returns null for patterns Astro's file routing cannot express (recorded as a warning upstream).\n */\nexport function patternToPagePath(pattern: string): string | null {\n if (pattern === '/' || pattern === '') return 'index.astro'\n const segments = pattern.replace(/^\\/+|\\/+$/g, '').split('/')\n // Literal Unicode segments preserve the source permalink. Validate literals\n // separately from parameters so widening the alphabet cannot admit traversal\n // or turn a source filename containing brackets into an Astro parameter.\n if (segments.some((seg) => seg.startsWith(':')\n ? !/^:[a-zA-Z0-9_]+\\*?$/.test(seg)\n : seg === '.' || seg === '..' || !/^[\\p{L}\\p{M}\\p{N}._-]+$/u.test(seg))) return null\n const mapped = segments.map((seg) => {\n if (!seg.startsWith(':')) return seg\n const rest = seg.endsWith('*')\n const name = seg.slice(1, rest ? -1 : undefined)\n return rest ? `[...${name}]` : `[${name}]`\n })\n const last = mapped.pop()\n return [...mapped, `${last}.astro`].join('/')\n}\n\n/** Index of the rest parameter segment in a page path, or -1. */\nexport function restParamIndex(pagePath: string): number {\n return pagePath.split('/').findIndex((seg) => seg.startsWith('[...'))\n}\n\nconst sortDeep = (v: unknown): unknown => {\n if (Array.isArray(v)) return v.map(sortDeep)\n if (v && typeof v === 'object') {\n return Object.fromEntries(\n Object.entries(v as Record<string, unknown>)\n .toSorted(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([k, val]) => [k, sortDeep(val)]),\n )\n }\n return v\n}\n\n/** JSON with stable key order and trailing newline — deterministic output, stable diffs. */\nexport function stableJson(value: unknown): string {\n return `${JSON.stringify(sortDeep(value), null, 2)}\\n`\n}\n","// Routes → Astro pages.\n//\n// Three shapes, matching what a migrated site actually needs:\n// - `single`: one page per post, body injected with `set:html` inside the family layout.\n// - list kinds (`archive`/`term`/`author`/`date`/`front` with a query): static\n// paths come from the producer's query data; items render through the\n// extracted item template when one exists.\n// - static kinds (no query): the family chrome IS the page.\n//\n// Pagination is a route parameter (`/news/page/:page`), never a separate\n// family — the generated paths simply include the page number in params.\n\nimport type { RouteModel, LayoutFamily } from '@contentrain/types'\nimport type { EmitContent, EmitPost, QueryPage } from './types.js'\nimport { DEFAULT_COLLECTION } from './types.js'\nimport { pascalCase, patternToPagePath, restParamIndex, stableJson } from './util.js'\n\nexport interface PageGenResult {\n files: Record<string, string>\n warnings: string[]\n}\n\nexport function routeFiles(\n route: RouteModel,\n family: LayoutFamily | undefined,\n content: EmitContent,\n siteLocale: string,\n seo = true,\n /** The route's pages each have a feed at <page>/feed.xml (see feed.ts): the page links it. */\n archiveFeed = false,\n): PageGenResult {\n const warnings: string[] = []\n const files: Record<string, string> = {}\n const pagePath = patternToPagePath(route.pattern)\n if (!pagePath) {\n return { files, warnings: [`route ${route.id}: pattern \"${route.pattern}\" cannot be expressed as an Astro page — skipped`] }\n }\n if (!family) {\n return { files, warnings: [`route ${route.id}: family \"${route.family}\" not found — skipped`] }\n }\n const layout = pascalCase(family.id)\n const depth = pagePath.split('/').length - 1\n const up = '../'.repeat(depth + 1) || '../'\n const hasParams = pagePath.includes('[')\n\n // Astro matches a rest parameter greedily, so anything after it is ambiguous.\n const restAt = restParamIndex(pagePath)\n if (restAt !== -1 && restAt !== pagePath.split('/').length - 1) {\n warnings.push(\n `route ${route.id}: rest parameter is not the final segment of \"${route.pattern}\" — Astro matches rest parameters greedily; verify this route resolves`,\n )\n }\n\n // Collection-driven: `single` always, and any route that names a collection —\n // a site's pages and custom post types are per-entry routes too, they just are\n // not called \"single\".\n if (route.kind === 'single' || route.collection !== undefined) {\n const collection = route.collection ?? DEFAULT_COLLECTION\n const posts = collectionItems(content, collection)\n if (!posts.length) {\n warnings.push(`route ${route.id}: collection \"${collection}\" has no items — page emitted, data file empty`)\n }\n files[`src/data/${collection}.json`] = stableJson(posts)\n files[`src/pages/${pagePath}`] = singlePage(route, layout, up, collection, siteLocale, hasParams, seo)\n return { files, warnings }\n }\n\n const queryPages: QueryPage[] = route.query ? (content.queries?.[route.query] ?? []) : []\n if (route.query) {\n if (!queryPages.length) warnings.push(`route ${route.id}: query \"${route.query}\" has no data — page emitted, data file empty`)\n if (queryPages.some((p) => !p.item_template && !p.sections?.length)) {\n warnings.push(`route ${route.id}: item template missing on some pages — plain fallback list rendered (not fidelity)`)\n }\n files[`src/data/queries/${route.query}.json`] = stableJson(queryPages)\n files[`src/pages/${pagePath}`] = listPage(route, layout, up, hasParams, siteLocale, seo, archiveFeed)\n return { files, warnings }\n }\n\n files[`src/pages/${pagePath}`] = staticPage(route, layout, up, hasParams, warnings, siteLocale, seo)\n return { files, warnings }\n}\n\n/** Items of a named collection; `posts` also accepts the `content.posts` shorthand. */\nexport function collectionItems(content: EmitContent, collection: string): EmitPost[] {\n const named = content.collections?.[collection]\n if (named) return named\n return collection === DEFAULT_COLLECTION ? (content.posts ?? []) : []\n}\n\nfunction singlePage(route: RouteModel, layout: string, up: string, collection: string, siteLocale: string, hasParams: boolean, seo: boolean): string {\n const locale = JSON.stringify(route.locale ?? siteLocale)\n return `---\n// Route: ${route.id} (${route.pattern}) — collection: ${collection} — emitted by @contentrain/emitter-astro\nimport Layout from '${up}layouts/${layout}.astro'\nimport data from '${up}data/${collection}.json'\nimport { postMarks${seo ? ', postSeo' : ''}, type EmittedPost } from '${up}lib/fill'\n\n${hasParams ? `export function getStaticPaths() {\n // The JSON is data; its contract is EmittedPost. Inferring the type from the\n // file would type only the fields this site happens to carry, and \"astro\n // check\" (which the build runs first) would then reject post.params on a site\n // whose permalinks need none. The cast lives INSIDE getStaticPaths because\n // Astro hoists this function: it sees imports, not the component scope.\n const posts = data as EmittedPost[]\n // A post carries its own route parameters (date parts of a dated permalink,\n // a post id) — using the template post's would send every other post to the\n // wrong address.\n return posts.map((post) => ({ params: { ...(post.params ?? {}), slug: post.slug }, props: { post } }))\n}\ntype Props = { post: EmittedPost }\nconst { post } = Astro.props as Props` : `const posts = data as EmittedPost[]\nif (posts.length !== 1) throw new Error(${JSON.stringify(`Static content route requires exactly one entry: ${route.id}`)})\nconst post = posts[0]!`}\n---\n<Layout\n title={post.title}\n marks={postMarks(post)}\n body={post.body}\n css={post.css ?? []}\n lang={post.locale ?? ${locale}}\n entry={post.entry}${seo ? `\n seo={postSeo(post${route.kind === 'page' ? \", 'page'\" : ''})}` : ''}\n/>\n`\n}\n\nfunction listPage(route: RouteModel, layout: string, up: string, hasParams: boolean, siteLocale: string, seo: boolean, archiveFeed = false): string {\n const locale = JSON.stringify(route.locale ?? siteLocale)\n // Chain the fallbacks HERE, not in the emitted source: a route without a title\n // used to emit `page.title ?? \"\" ?? ''`, which \"astro check\" rejects as never\n // nullish — and the build runs check first, so the whole site failed to build.\n const routeTitle = route.title === undefined ? `''` : JSON.stringify(route.title)\n // The cast lives inside getStaticPaths: Astro hoists it above the component\n // scope, so it can read imports and nothing else.\n const paths = hasParams\n ? `export function getStaticPaths() {\n const pages = data as EmittedQueryPage[]\n return pages.map((page) => ({ params: page.params, props: { page } }))\n}\ntype Props = { page: EmittedQueryPage }\nconst { page } = Astro.props as Props`\n : `const page = (data as EmittedQueryPage[])[0] ?? { params: {}, items: [], title: undefined }`\n return `---\n// Route: ${route.id} (${route.pattern}) — emitted by @contentrain/emitter-astro\nimport Layout from '${up}layouts/${layout}.astro'\nimport data from '${up}data/queries/${route.query}.json'\nimport { ${archiveFeed && seo ? 'entryAddress, ' : ''}renderQueryPage, type EmittedQueryPage } from '${up}lib/fill'\n\n${paths}\n// A list renders in sections (a big card then a grid) — one template is the\n// single-section case. Without a template at all it falls back to a plain list\n// and the emitter has already warned: silence would read as fidelity.\nconst content = renderQueryPage(page)\n// Route params carry slugs; page marks carry what the chrome needs to SHOW\n// (a term's display name, description, count).\nconst marks = { ...page.params, ...(page.marks ?? {}) }\nconst title = page.title ?? ${routeTitle}\n---\n<Layout\n title={title || String(marks.term_name ?? page.params.term ?? '')}\n marks={marks}\n body={content}\n css={page.css ?? []}\n lang={${locale}}${seo ? `\n seo={{ description: page.description, image: page.image, imageMeta: page.image_meta, canonical: page.canonical, breadcrumbs: page.breadcrumbs, noindex: page.noindex, nofollow: page.nofollow, openGraph: page.open_graph, twitter: page.twitter, schema: page.schema, profile: page.profile,${archiveFeed ? ` feed: { path: (entryAddress(${JSON.stringify(route.pattern)}, page.params) ?? '/') + 'feed.xml', title: title || undefined },` : ''} type: 'website', pageType: 'CollectionPage' }}` : ''}\n/>\n`\n}\n\nfunction staticPage(route: RouteModel, layout: string, up: string, hasParams: boolean, warnings: string[], siteLocale: string, seo: boolean): string {\n if (hasParams) warnings.push(`route ${route.id}: parameterized route without a query — emitted with empty paths`)\n const paths = hasParams\n ? `export function getStaticPaths() {\n return []\n}\n`\n : ''\n return `---\n// Route: ${route.id} (${route.pattern}) — emitted by @contentrain/emitter-astro\nimport Layout from '${up}layouts/${layout}.astro'\n${paths}---\n<Layout title=${JSON.stringify(route.title ?? '')} marks={{}} lang={${JSON.stringify(route.locale ?? siteLocale)}}${seo ? ` seo={{ type: 'website' }}` : ''} />\n`\n}\n\n/** Share a catchall while preserving the original editable collections. */\nexport function sharedCollectionPage(routes: RouteModel[], content: EmitContent, siteLocale: string, seo = true): string | undefined {\n const pattern = routes[0]?.pattern\n if (!pattern || routes.length < 2 || !/^\\/(?:[^:*]+\\/)*:slug\\*$/.test(pattern)\n || routes.some(r => r.pattern !== pattern || r.query || (r.kind !== 'single' && !r.collection))) return undefined\n const seen = new Set<string>()\n for (const route of routes) for (const post of collectionItems(content, route.collection ?? DEFAULT_COLLECTION)) {\n let key: string\n try { key = decodeURIComponent(post.slug).replace(/\\/+$/, '') }\n catch { return undefined }\n if (seen.has(key)) return undefined\n seen.add(key)\n }\n const up = '../'.repeat(patternToPagePath(pattern)!.split('/').length)\n return `---\n// Shared collection routes: ${routes.map(r => r.id).join(', ')}\n${routes.map((r, i) => `import Layout${i} from '${up}layouts/${pascalCase(r.family)}.astro'\\nimport data${i} from '${up}data/${r.collection ?? DEFAULT_COLLECTION}.json'`).join('\\n')}\nimport { postMarks${seo ? ', postSeo' : ''}, type EmittedPost } from '${up}lib/fill'\nexport function getStaticPaths() {\n const sources = [${routes.map((_, i) => `data${i}`).join(', ')}] as EmittedPost[][]\n const seen = new Set<string>()\n return sources.flatMap((posts, routeIndex) => posts.map(post => {\n const key = decodeURIComponent(post.slug).replace(/\\\\/+$/, '')\n if (seen.has(key)) throw new Error('Route collision in shared collections: ' + post.slug)\n seen.add(key)\n return { params: { ...(post.params ?? {}), slug: post.slug }, props: { post, routeIndex } }\n }))\n}\nconst { post, routeIndex } = Astro.props as { post: EmittedPost, routeIndex: number }\nconst layouts = [${routes.map((_, i) => `Layout${i}`).join(', ')}]\nconst locales = ${JSON.stringify(routes.map(r => r.locale ?? siteLocale))}\nconst kinds = ${JSON.stringify(routes.map(r => (r.kind === 'page' ? 'page' : 'post')))} as Array<'post' | 'page'>\nconst Layout = layouts[routeIndex]!\n---\n<Layout title={post.title} marks={postMarks(post)} body={post.body}\n css={post.css ?? []} lang={post.locale ?? locales[routeIndex]} entry={post.entry}${seo ? ' seo={postSeo(post, kinds[routeIndex])}' : ''} />\n`\n}\n","// hreflang: which pages are translations of each other, and where each one is.\n//\n// Both halves come from what the emitter already holds, so neither can drift:\n//\n// - A translation group is the content-store entry. The importer folds the\n// translations of one post into one entry id across locales, and a post\n// carries that address as `EmitPost.entry` — the same key the comments\n// component threads by.\n// - An address is computed from the route pattern and the post's own\n// parameters, the very values `getStaticPaths` hands Astro. The path printed\n// in a `<link rel=\"alternate\">` is therefore the path Astro builds, not a\n// producer's recomputation of it.\n//\n// A post whose address is ambiguous gets no alternates. A wrong hreflang tells\n// a search engine to serve the wrong page to a language's readers; a missing\n// one only leaves it to work that out.\n\nimport type { RouteModel } from '@contentrain/types'\nimport type { EmitContent, EmitPost } from './types.js'\nimport { DEFAULT_COLLECTION } from './types.js'\nimport { collectionItems } from './pages.js'\nimport { patternToPagePath } from './util.js'\n\nexport interface Alternate {\n /** A language tag, or `x-default`. */\n lang: string\n /** Site-root-relative path, as the build serves it. */\n path: string\n}\n\n/**\n * The path Astro builds for a route pattern and a set of parameters, in the\n * directory format the scaffold configures (`/en/hello/`). Null when a\n * parameter has no value — that page has no address to name.\n */\nexport function entryPath(pattern: string, params: Record<string, string | undefined>): string | null {\n const segments = pattern.replace(/^\\/+|\\/+$/g, '').split('/').filter(Boolean)\n if (!segments.length) return '/'\n const out: string[] = []\n for (const segment of segments) {\n if (!segment.startsWith(':')) {\n out.push(segment)\n continue\n }\n const value = params[segment.slice(1).replace(/\\*$/, '')]?.replace(/^\\/+|\\/+$/g, '')\n if (!value) return null\n out.push(value)\n }\n return `/${out.join('/')}/`\n}\n\ninterface Placement {\n lang: string\n path: string | null\n key?: string\n collection: string\n}\n\nexport interface AlternatesResult {\n /** The content with `alternates` on every post that has translations. */\n content: EmitContent\n warnings: string[]\n}\n\nconst byLangTag = (a: Alternate, b: Alternate) => (a.lang < b.lang ? -1 : a.lang > b.lang ? 1 : 0)\n\n/**\n * Attach `alternates` to every entry that has translations on the site.\n *\n * The language of a page is the one its `<html lang>` gets: the post's own\n * locale, else its route's, else the site's. A site with one language has no\n * alternates and nothing is attached.\n */\nexport function withAlternates(routes: RouteModel[], content: EmitContent, siteLocale: string): AlternatesResult {\n const warnings: string[] = []\n const placements = new Map<EmitPost, Placement[]>()\n\n for (const route of routes) {\n if (route.kind !== 'single' && route.collection === undefined) continue\n const pagePath = patternToPagePath(route.pattern)\n if (!pagePath) continue\n const collection = route.collection ?? DEFAULT_COLLECTION\n const posts = collectionItems(content, collection)\n // A static route renders its one entry at the pattern itself; with more\n // than one entry the page throws at build, and there is no address.\n if (!pagePath.includes('[') && posts.length !== 1) continue\n for (const post of posts) {\n const list = placements.get(post) ?? []\n list.push({\n lang: post.locale ?? route.locale ?? siteLocale,\n path: entryPath(route.pattern, { ...post.params, slug: post.slug }),\n key: post.entry ? JSON.stringify([post.entry.model_id, post.entry.entry_id]) : undefined,\n collection,\n })\n placements.set(post, list)\n }\n }\n\n const langs = new Set([...placements.values()].flat().map((p) => p.lang))\n if (langs.size < 2) return { content, warnings }\n\n // One address per post: a post that two routes generate has two, and naming\n // either would be a guess.\n const groups = new Map<string, Array<{ post: EmitPost; lang: string; path: string }>>()\n const unkeyed = new Map<string, number>()\n const ambiguous = new Map<string, number>()\n for (const [post, list] of placements) {\n const first = list[0]!\n if (list.length > 1 || first.path === null) {\n ambiguous.set(first.collection, (ambiguous.get(first.collection) ?? 0) + 1)\n continue\n }\n if (!first.key) {\n unkeyed.set(first.collection, (unkeyed.get(first.collection) ?? 0) + 1)\n continue\n }\n const group = groups.get(first.key) ?? []\n group.push({ post, lang: first.lang, path: first.path })\n groups.set(first.key, group)\n }\n for (const [collection, count] of unkeyed) {\n warnings.push(`collection ${collection}: ${count} posts carry no entry address — hreflang cannot link them to their translations`)\n }\n for (const [collection, count] of ambiguous) {\n warnings.push(`collection ${collection}: ${count} posts have no single address (served by more than one route, or a route parameter without a value) — no hreflang on those pages`)\n }\n\n const alternatesOf = new Map<EmitPost, Alternate[]>()\n for (const group of groups.values()) {\n if (group.length < 2) continue\n const byLang = new Map<string, string>()\n let conflict = false\n for (const member of group) {\n const seen = byLang.get(member.lang)\n if (seen !== undefined && seen !== member.path) conflict = true\n byLang.set(member.lang, member.path)\n }\n const entry = group[0]!.post.entry!\n if (conflict) {\n warnings.push(`entry ${entry.model_id}/${entry.entry_id}: two pages claim the same language — no hreflang for this entry`)\n continue\n }\n if (byLang.size < 2) continue\n const alternates: Alternate[] = [...byLang].map(([lang, path]) => ({ lang, path })).toSorted(byLangTag)\n const fallback = byLang.get(siteLocale)\n if (fallback !== undefined) alternates.push({ lang: 'x-default', path: fallback })\n for (const member of group) alternatesOf.set(member.post, alternates)\n }\n if (!alternatesOf.size) return { content, warnings }\n\n const mark = (posts: EmitPost[]) => posts.map((post) => {\n const alternates = alternatesOf.get(post)\n return alternates ? { ...post, alternates } : post\n })\n return {\n content: {\n ...content,\n ...(content.posts ? { posts: mark(content.posts) } : {}),\n ...(content.collections\n ? { collections: Object.fromEntries(Object.entries(content.collections).map(([name, posts]) => [name, mark(posts)])) }\n : {}),\n },\n warnings,\n }\n}\n","// Pages the source site kept out of search (`EmitPost.noindex`,\n// `QueryPage.noindex`) stay out of the migrated site's sitemap.\n//\n// `@astrojs/sitemap` lists every page the build writes; its `filter` is the\n// only way to leave one out. The addresses are computed here from the route\n// pattern and the entry's parameters — the values `getStaticPaths` builds the\n// page from — so the filter names exactly the pages that carry the robots meta.\n\nimport type { RouteModel } from '@contentrain/types'\nimport type { EmitContent } from './types.js'\nimport { DEFAULT_COLLECTION } from './types.js'\nimport { entryPath } from './alternates.js'\nimport { collectionItems } from './pages.js'\nimport { patternToPagePath } from './util.js'\n\n/** Site-root paths (directory format, `/a/b/`) of every noindex page, sorted. */\nexport function noindexPaths(routes: RouteModel[], content: EmitContent): string[] {\n const out = new Set<string>()\n for (const route of routes) {\n const pagePath = patternToPagePath(route.pattern)\n if (!pagePath) continue\n const dynamic = pagePath.includes('[')\n if (route.kind === 'single' || route.collection !== undefined) {\n const posts = collectionItems(content, route.collection ?? DEFAULT_COLLECTION)\n // A parameterless collection route renders its one entry at the pattern.\n if (!dynamic && posts.length !== 1) continue\n for (const post of posts) {\n if (!post.noindex) continue\n const path = entryPath(route.pattern, { ...post.params, slug: post.slug })\n if (path) out.add(path)\n }\n }\n if (route.query) {\n for (const page of content.queries?.[route.query] ?? []) {\n if (!page.noindex) continue\n const path = entryPath(route.pattern, page.params)\n if (path) out.add(path)\n }\n }\n }\n return [...out].toSorted()\n}\n\n/**\n * The `sitemap()` call for astro.config.mjs. The sitemap hands `filter` an\n * absolute, percent-encoded URL; it is compared by decoded path with the\n * trailing slash normalised, the form `noindexPaths` returns.\n */\nexport function sitemapIntegration(paths: string[]): string {\n if (!paths.length) return 'sitemap()'\n return `sitemap({ filter: (page) => !NOINDEX.has(sitemapPath(page)) })`\n}\n\n/** The declarations `sitemapIntegration` refers to, placed above `defineConfig`. */\nexport function sitemapFilterDeclarations(paths: string[]): string[] {\n if (!paths.length) return []\n return [\n `// Pages the source site kept out of search (noindex) stay out of the sitemap.`,\n `const NOINDEX = new Set(${JSON.stringify(paths)})`,\n `const sitemapPath = (page) => {`,\n ` let path = new URL(page).pathname`,\n ` try { path = decodeURI(path) } catch { /* keep the encoded path */ }`,\n ` return path.endsWith('/') ? path : \\`\\${path}/\\``,\n `}`,\n ``,\n ]\n}\n","// Project scaffolding: manifest, config, styles, and the shared fill helper\n// the generated pages use to pour content into `@@mark@@` placeholders.\n\nimport type { DesignTokens, ProjectIR } from '@contentrain/types'\nimport type { EmitOptions, RuntimeBinding } from './types.js'\nimport { DEFAULT_IMAGE_SIZES, DEFAULT_IMAGE_WIDTHS, IMAGES_TS, astroImageConfig, imagePatterns, imagesEnabled, optimizeImagesTs } from './images.js'\nimport { stableJson } from './util.js'\nimport { sitemapFilterDeclarations, sitemapIntegration } from './noindex.js'\n\n/**\n * `noindex` holds the site-root paths the sitemap leaves out (see noindex.ts);\n * `redirectsConfig` is the `redirects` block for astro.config.mjs (see redirects.ts);\n * `runtime` adds its host to the image allow-list (see images.ts).\n */\nexport function scaffoldFiles(ir: ProjectIR, options: EmitOptions, noindex: string[] = [], redirectsConfig: string | null = null, runtime?: RuntimeBinding): Record<string, string> {\n const tailwind = options.tailwind !== false\n const split = ir.viewport_strategy === 'split'\n // The sitemap integration needs an absolute site to build URLs from; without\n // one it has nothing to write, so it is only wired when there is a site.\n const sitemap = options.sitemap !== false && Boolean(ir.site.url)\n const images = imagesEnabled(options)\n const imageConfig = images ? astroImageConfig(imagePatterns(options, runtime)) : null\n const files: Record<string, string> = {}\n\n const pkg: Record<string, unknown> = {\n name: options.projectName ?? 'migrated-site',\n private: true,\n type: 'module',\n scripts: {\n dev: 'astro dev',\n // `astro check` first: type errors in generated pages should fail the\n // build, not surface later in a browser.\n build: 'astro check && astro build',\n preview: 'astro preview',\n // Split viewport production (skeleton): one build per device class, served by device.\n ...(split ? { 'build:desktop': 'VIEWPORT=desktop astro build --outDir dist/desktop', 'build:mobile': 'VIEWPORT=mobile astro build --outDir dist/mobile' } : {}),\n },\n dependencies: {\n astro: '^5.0.0',\n ...(tailwind ? { tailwindcss: '^4.0.0', '@tailwindcss/vite': '^4.0.0' } : {}),\n ...(sitemap ? { '@astrojs/sitemap': '^3.7.0' } : {}),\n // Astro's default image service; getImage() needs it at build time.\n ...(images ? { sharp: '^0.34.0' } : {}),\n // `build` runs `astro check`, so the checker is a build dependency, not a\n // dev one — as create-astro writes it. As devDependencies they were left\n // out by every install with NODE_ENV=production (a worker image, a host's\n // production build), and Astro then asks to install them: with no\n // terminal the build fails, with CI set `astro check` exits 0 unchecked.\n '@astrojs/check': '^0.9.0',\n typescript: '^5.7.0',\n },\n }\n files['package.json'] = stableJson(pkg)\n\n files['astro.config.mjs'] = [\n `import { defineConfig } from 'astro/config'`,\n ...(tailwind ? [`import tailwindcss from '@tailwindcss/vite'`] : []),\n ...(sitemap ? [`import sitemap from '@astrojs/sitemap'`] : []),\n ``,\n ...(sitemap ? sitemapFilterDeclarations(noindex) : []),\n `export default defineConfig({`,\n // Canonical URLs and sitemaps hang off \\`site\\` — for a migration, SEO\n // continuity is the point, so the source site's URL always lands here.\n // Astro rejects an empty string as an invalid URL and refuses to build, so\n // with no site the key is left out; the emitter warns about that instead.\n ...(ir.site.url ? [` site: ${JSON.stringify(ir.site.url)},`] : []),\n // A site whose addresses end in a slash is built as directories\n // (/hello/index.html); one whose addresses do not, as files (/hello.html),\n // which hosts serve at /hello — either way at the source's own address.\n ...(options.trailingSlash === false\n ? [` build: { format: 'file' },`, ` trailingSlash: 'never',`]\n : [` build: { format: 'directory' },`]),\n ...(redirectsConfig ? [redirectsConfig] : []),\n ...(sitemap ? [` integrations: [${sitemapIntegration(noindex)}],`] : []),\n // Hosts content images may be optimized from — the same list the emitted\n // src/lib/optimize-images.ts checks before calling getImage().\n ...(imageConfig ? [imageConfig] : []),\n ...(tailwind ? [` vite: { plugins: [tailwindcss()] },`] : []),\n `})`,\n ``,\n ].join('\\n')\n\n // Astro wants a tsconfig in every project — editors and the compiler read\n // it even in JS-only projects, and the emitted src/lib/fill.ts is TS.\n files['tsconfig.json'] = `${JSON.stringify({ extends: 'astro/tsconfigs/base', include: ['.astro/types.d.ts', '**/*'], exclude: ['dist', 'public', 'node_modules'] }, null, 2)}\\n`\n\n if (tailwind) files['src/styles/modern.css'] = modernCss(ir.tokens)\n\n if (options.sitemap !== false) files['public/robots.txt'] = robotsTxt(ir.site.url)\n\n files['src/lib/fill.ts'] = FILL_TS\n if (images) {\n files['src/lib/images.ts'] = IMAGES_TS\n files['src/lib/optimize-images.ts'] = optimizeImagesTs(\n imagePatterns(options, runtime),\n options.images?.widths ?? DEFAULT_IMAGE_WIDTHS,\n options.images?.sizes ?? DEFAULT_IMAGE_SIZES,\n )\n }\n return files\n}\n\n/**\n * `robots.txt` allowing crawlers and naming the sitemap.\n *\n * The `Sitemap:` line must be an absolute URL — a relative one is invalid and\n * crawlers ignore it. Without a site to make it absolute the line is left out\n * rather than pointed at a build host; the emitter warns about the missing site\n * separately.\n *\n * Nothing is disallowed. A WordPress robots.txt typically keeps crawlers out of\n * `/wp-admin/`; the migrated site has no such path, so carrying the rule over\n * would only restrict a site that no longer needs restricting.\n *\n * The sitemap is resolved under the site's own path, not the host root: a site\n * at `https://example.com/blog` serves its build output, sitemap included, under\n * `/blog/`.\n */\nexport function robotsTxt(siteUrl: string | undefined): string {\n const lines = ['User-agent: *', 'Allow: /']\n if (siteUrl) {\n const base = siteUrl.endsWith('/') ? siteUrl : `${siteUrl}/`\n lines.push('', `Sitemap: ${new URL('sitemap-index.xml', base).href}`)\n }\n return `${lines.join('\\n')}\\n`\n}\n\n/** Tailwind 4 evolution layer: CSS-first config with the site's extracted tokens. */\nfunction modernCss(tokens: DesignTokens | undefined): string {\n const lines: string[] = [`@import 'tailwindcss';`, ``]\n const theme: string[] = []\n const push = (prefix: string, map: Record<string, string> | undefined) => {\n for (const [key, value] of Object.entries(map ?? {})) theme.push(` --${prefix}-${key}: ${value};`)\n }\n push('color', tokens?.colors)\n push('font', tokens?.font_families)\n push('text', tokens?.font_sizes)\n push('spacing', tokens?.spacing)\n push('breakpoint', tokens?.breakpoints)\n if (theme.length) lines.push(`@theme {`, ...theme, `}`)\n return `${lines.join('\\n')}\\n`\n}\n\nconst FILL_TS = `// Emitted by @contentrain/emitter-astro — the template runtime.\n//\n// Chrome, item templates and attribute values carry three marker forms:\n// @@mark@@ — a value, escaped\n// @@mark_html@@ — a value, raw HTML\n// <!--@@repeat:list@@-->…<!--@@/repeat@@--> — once per list item\n// <!--@@if:name@@-->…<!--@@/if@@--> — only when name has a value\n// Rendering order is repeats → conditionals → marks, so a repeat body may hold\n// conditionals and marks that only make sense per item.\n\n/** Where page content splices into the body chrome — must match @contentrain/types CHROME_BODY_SLOT. */\nexport const BODY_SLOT = '<!--@@body@@-->'\n\nconst REPEAT_RE = /<!--@@repeat:([a-z0-9_]+)(?:\\\\|([\\\\s\\\\S]*?))?@@-->([\\\\s\\\\S]*?)<!--@@\\\\/repeat@@-->/gi\nconst IF_RE = /<!--@@if:(!?)([a-z0-9_]+)@@-->([\\\\s\\\\S]*?)<!--@@\\\\/if@@-->/gi\nconst MARK_RE = /@@([a-z0-9_]+)@@/gi\n\nexport type Values = Record<string, unknown>\n\nexport const esc = (value: unknown): string =>\n String(value ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;')\n\nconst isFilled = (value: unknown): boolean =>\n Array.isArray(value) ? value.length > 0 : value !== undefined && value !== null && value !== ''\n\n/**\n * Replace @@marks@@. Values are escaped unless the mark name ends in _html —\n * escaping is the default so content-derived text can never break the page,\n * and the _html opt-in exists for themes that print a post's own markup\n * (full content in a list card, a link-bearing excerpt).\n */\nexport function fillMarks(html: string, values: Values): string {\n return html.replace(MARK_RE, (_all, key: string) => {\n const value = values[key]\n if (Array.isArray(value)) return value.map((v) => esc(v && typeof v === 'object' && 'name' in v ? (v as { name: unknown }).name : v)).join(', ')\n return key.toLowerCase().endsWith('_html') ? String(value ?? '') : esc(value ?? '')\n })\n}\n\n/** Expand repeat blocks; each item renders the inner fragment with its own values. */\nexport function expandRepeats(html: string, values: Values): string {\n return html.replace(REPEAT_RE, (_all, name: string, sep: string | undefined, inner: string) => {\n const list = values[name]\n if (!Array.isArray(list) || list.length === 0) return ''\n return list\n .map((item, index) => {\n // An object item prints its name at @@item@@ and exposes item_<key> for\n // the rest, so a template written for string terms keeps working when\n // the producer starts sending { name, link }.\n const record = item && typeof item === 'object' ? (item as Record<string, unknown>) : undefined\n const itemValues: Values = { ...values, item: record && 'name' in record ? record.name : item, item_index: String(index) }\n if (record) {\n for (const [k, v] of Object.entries(record)) itemValues['item_' + k] = v\n }\n return renderTemplate(inner, itemValues)\n })\n .join(sep ?? '')\n })\n}\n\n/** Drop conditional blocks whose value is empty (or present, when negated). */\nexport function applyConditions(html: string, values: Values): string {\n return html.replace(IF_RE, (_all, negate: string, name: string, inner: string) =>\n isFilled(values[name]) !== (negate === '!') ? inner : '',\n )\n}\n\n/** Full render: repeats → conditionals → marks. */\nexport function renderTemplate(html: string, values: Values): string {\n return fillMarks(applyConditions(expandRepeats(html, values), values), values)\n}\n\n/** Fill marks inside attribute values (per-page classes like postid-123). */\nexport function fillAttrs(attrs: Record<string, string> | undefined, values: Values): Record<string, string> {\n const out: Record<string, string> = {}\n for (const [name, value] of Object.entries(attrs ?? {})) out[name] = fillMarks(value, values)\n return out\n}\n\n/**\n * Compose the body chrome with the page content. The split happens BEFORE any\n * rendering: the @@…@@ pattern would otherwise consume the @@body@@ inside the\n * marker comment, leaving <!----> behind and silently dropping the content\n * (measured cost on a real page: 49.8 vs 97.8).\n */\nexport function composeBody(chromeBody: string, values: Values, content: string): string {\n return chromeBody\n .split(BODY_SLOT)\n .map((part) => renderTemplate(part, values))\n .join(content)\n}\n\n/** Component mount marker — must match @contentrain/types CHROME_COMPONENT_OPEN / CHROME_COMPONENT_CLOSE. */\nexport const COMPONENT_OPEN = '<!--@@component:'\nexport const COMPONENT_CLOSE = '@@-->'\nconst COMPONENT_RE = /<!--@@component:([^@\\\\s]+)@@-->/g\n\nexport interface BodyPart {\n html: string\n /** A mount point: the component id the marker named. */\n component?: string\n}\n\n/**\n * Split rendered chrome at component markers into html parts interleaved with\n * mount points, so the layout can render a real component where the theme's\n * comments form (or contact form) stood. The parts are output in order, so the\n * page's HTML is exactly the chrome with the component's markup at the marker —\n * no parser sees the pieces separately.\n */\nexport function splitComponents(html: string): BodyPart[] {\n const parts: BodyPart[] = []\n let last = 0\n for (const match of html.matchAll(COMPONENT_RE)) {\n parts.push({ html: html.slice(last, match.index) })\n parts.push({ html: '', component: match[1] ?? '' })\n last = match.index + match[0].length\n }\n parts.push({ html: html.slice(last) })\n return parts\n}\n\n/** Where a list section's wrapper takes its items — must match @contentrain/types LIST_ITEMS_SLOT. */\nexport const ITEMS_SLOT = '<!--@@items@@-->'\n\nexport interface ListSection {\n template: string\n wrapper?: string\n count?: number\n}\n\n/**\n * Render a list in sections. Themes often render the newest post as a big card\n * and the rest as a grid — two templates in two containers — so a list is a\n * sequence of sections, each taking a count of items (or the remainder).\n */\nexport function renderSections<T>(\n sections: ListSection[],\n items: T[],\n values: (item: T) => Values,\n): string {\n let index = 0\n const out: string[] = []\n for (const section of sections) {\n const take = section.count ?? items.length - index\n const slice = items.slice(index, index + take)\n index += slice.length\n if (slice.length === 0) continue\n const rendered = slice.map((item) => renderTemplate(section.template, values(item))).join('')\n out.push(section.wrapper ? section.wrapper.split(ITEMS_SLOT).join(rendered) : rendered)\n }\n return out.join('')\n}\n\n/**\n * Render a query's results for a region inside a page — a \"recent posts\" block\n * in an article's chrome, not a list page.\n *\n * A list PAGE maps each result set to a route, so several are expected. A\n * region has no route to distinguish them, so exactly one is expected and\n * anything else is a build error rather than a guess: picking the first would\n * silently render one category's posts under every category.\n *\n * Without item markup the region gets the same plain list a list page does,\n * and the emitter has warned. An empty string here was a region that vanished\n * from every page with nothing anywhere saying so.\n */\nexport function renderQuery(pages: EmittedQueryPage[], id: string): string {\n if (pages.length !== 1) {\n throw new Error(\n 'Query \"' + id + '\" fills a page region, so it must have exactly one result set; got '\n + pages.length + '. A region has no route parameter to choose between them.',\n )\n }\n return renderQueryPage(pages[0]!)\n}\n\n/**\n * One result set as markup: its sections, its single item template, or — when\n * the producer supplied neither — a plain list of links. The plain list is not\n * fidelity and the emitter warns wherever it will be used; it exists so the\n * items are still there, because a list that renders nothing looks like a list\n * with no posts.\n */\nexport function renderQueryPage(page: EmittedQueryPage): string {\n const sections = page.sections?.length ? page.sections : page.item_template ? [{ template: page.item_template }] : []\n if (sections.length) return renderSections(sections, page.items, postMarks)\n return '<ul class=\"cr-post-list\">'\n + page.items.map((item) => '<li><a href=\"/' + item.slug + '/\">' + esc(item.title) + '</a></li>').join('')\n + '</ul>'\n}\n\n// ─── SEO helpers ───\n\n/**\n * A meta description from free text: markup stripped, whitespace collapsed,\n * cut at a word boundary. The cap is a rendering limit, not an editorial\n * choice — an excerpt is a paragraph and a description tag is a line.\n */\nexport function seoDescription(text: string | undefined, max = 300): string | undefined {\n if (!text) return undefined\n const flat = text.replace(/<[^>]*>/g, ' ').replace(/\\\\s+/g, ' ').trim()\n if (!flat) return undefined\n if (flat.length <= max) return flat\n const cut = flat.slice(0, max)\n const space = cut.lastIndexOf(' ')\n // Concatenation, not a template literal: this file is emitted from one, and\n // an unescaped interpolation would be evaluated at emit time.\n return (space > max * 0.6 ? cut.slice(0, space) : cut).trimEnd() + '…'\n}\n\n/**\n * Absolute URL for a social tag or a canonical link. Crawlers do not resolve\n * relative og:image or og:url, so a value that cannot be made absolute is\n * dropped rather than emitted half-formed. Needs \\`site\\` in astro.config.mjs.\n */\nexport function absoluteUrl(value: string | undefined, site: URL | undefined): string | undefined {\n if (!value) return undefined\n if (/^https?:\\\\/\\\\//i.test(value)) return value\n if (!site) return undefined\n try {\n return new URL(value, site).toString()\n } catch {\n return undefined\n }\n}\n\nexport interface ImageMeta {\n width?: number\n height?: number\n type?: string\n}\n\n/**\n * The \\`og:image:*\\` values worth printing. A width that is not a positive\n * integer, or a type that is not an image MIME type, would be a wrong tag\n * rather than a missing one, so it is dropped.\n */\nexport function imageMetaTags(meta: ImageMeta | undefined): { width?: string; height?: string; type?: string } {\n const size = (n: unknown) => (typeof n === 'number' && Number.isInteger(n) && n > 0 ? String(n) : undefined)\n const type = typeof meta?.type === 'string' && /^image\\\\/[a-z0-9.+-]+$/i.test(meta.type) ? meta.type.toLowerCase() : undefined\n return { width: size(meta?.width), height: size(meta?.height), type }\n}\n\n/**\n * JSON-LD payload for a \\`<script type=\"application/ld+json\">\\`. \\`<\\` is escaped\n * so a value containing \\`</script>\\` cannot close the block and inject markup.\n */\nexport function jsonLd(value: unknown): string {\n return JSON.stringify(value).replace(/</g, '\\\\\\\\u003c')\n}\n\nexport interface Breadcrumb {\n name: string\n path: string\n}\n\n/**\n * A trail every crumb of which can be printed, or undefined. A crumb needs a\n * name and a site-root-relative path; one bad crumb drops the trail, because a\n * breadcrumb with a hole in it states a hierarchy the site does not have.\n */\nexport function validTrail(trail: unknown): Breadcrumb[] | undefined {\n if (!Array.isArray(trail) || !trail.length) return undefined\n const ok = trail.every((c) => c && typeof c === 'object'\n && typeof (c as Breadcrumb).name === 'string' && (c as Breadcrumb).name.trim() !== ''\n && typeof (c as Breadcrumb).path === 'string'\n && !(c as Breadcrumb).path.includes(String.fromCharCode(92))\n && ![...(c as Breadcrumb).path].some((char) => char.charCodeAt(0) <= 32) && /^\\\\/(?!\\\\/)/.test((c as Breadcrumb).path))\n return ok ? (trail as Breadcrumb[]) : undefined\n}\n\nexport interface PageStructuredDataInput {\n url?: string\n site?: URL\n /** The document title: the page node's name. */\n title: string\n /** The entry's own title, when \\`title\\` is one an SEO plugin composed: Article headline, last crumb. */\n headline?: string\n description?: string\n image?: string\n locale?: string\n article: boolean\n pageType?: 'WebPage' | 'CollectionPage'\n publishedAt?: string\n modifiedAt?: string\n author?: string\n /** The author's page (their archive), absolute: the Article author's \\`url\\`. */\n authorUrl?: string\n /** On an author's archive: who the page is about — it becomes a ProfilePage whose main entity is this Person. */\n profile?: AuthorProfile\n /** false: the site's addresses end without a slash — an author's page is named in that form. */\n trailingSlash?: boolean\n siteName?: string\n /** \\`@id\\` of the site's own WebSite node, when its head declares one. */\n websiteId?: string\n /** \\`@id\\` of the node the kept head says publishes the site (Organization or Person). */\n publisherId?: string\n /** What \\`image\\` is — an Article image with its size is an ImageObject. */\n imageMeta?: ImageMeta\n breadcrumbs?: Breadcrumb[]\n}\n\n/**\n * Open Graph's locale form, \\`ll_RR\\` (\\`tr_TR\\`), from a BCP 47 tag\n * (\\`tr-TR\\`). A tag without a region has no OG form — guessing one would\n * name the wrong country — so it gives undefined and the tag is left out.\n */\nexport function ogLocale(tag: string | undefined): string | undefined {\n const match = /^([a-z]{2,3})[-_]([a-z]{2})$/i.exec(tag ?? '')\n return match ? match[1]!.toLowerCase() + '_' + match[2]!.toUpperCase() : undefined\n}\n\n/** An author as their archive page presents them: the Person a ProfilePage is about. */\nexport interface AuthorProfile {\n name: string\n /** The author's bio. */\n description?: string\n /** Avatar or photo, absolute or site-root-relative. */\n image?: string\n /** The author's profiles elsewhere (social accounts, a personal site) — schema.org \\`sameAs\\`. */\n same_as?: string[]\n}\n\n/**\n * An address on this site in the form its pages are named in — absolute, the\n * page path in the site's trailing-slash form, no query or fragment — so a\n * reference to a page and the page itself name one URL. Another host's\n * address is kept as given.\n */\nfunction siteAddress(value: string | undefined, site: URL | undefined, trailingSlash = true): string | undefined {\n const absolute = absoluteUrl(value, site)\n if (!absolute || !site) return absolute\n const url = new URL(absolute)\n if (url.host.toLowerCase() !== site.host.toLowerCase()) return absolute\n return absoluteUrl(pagePath(url.pathname, trailingSlash), site)\n}\n\n/** An Article image: an ImageObject when its size is known, the bare URL otherwise. */\nfunction articleImage(url: string, meta: ImageMeta | undefined): string | Record<string, unknown> {\n const tags = imageMetaTags(meta)\n if (!tags.width || !tags.height) return url\n return { '@type': 'ImageObject', url, width: Number(tags.width), height: Number(tags.height) }\n}\n\n/**\n * Who publishes the article: the node the site's own head declares, by\n * \\`@id\\`, so the logo and profile it carries count; else the site by name\n * and address. Nothing is invented beyond the name the site gives itself.\n */\nfunction publisher(input: PageStructuredDataInput): Record<string, unknown> | undefined {\n if (input.publisherId) return { '@id': input.publisherId }\n if (!input.siteName) return undefined\n // The site's own address, path included: a site under /blog/ is not its host.\n const home = input.site ? new URL(input.site.pathname.endsWith('/') ? input.site.href : input.site.href + '/').toString() : undefined\n return { '@type': 'Organization', name: input.siteName, ...(home ? { url: home } : {}) }\n}\n\n/**\n * The page's structured data as one graph: the page itself (WebPage, or\n * CollectionPage for a list), its breadcrumb trail, and on an entry the\n * Article whose main entity it is — linked by \\`@id\\` rather than repeated.\n *\n * The page node needs an absolute address to be anything; without \\`site\\`\n * only an entry's Article remains, as before. \\`isPartOf\\` points at the site's\n * own WebSite node when the kept head declares one — never at a WebSite this\n * function invents.\n */\nexport function pageStructuredData(input: PageStructuredDataInput): Record<string, unknown> | undefined {\n const graph: Array<Record<string, unknown>> = []\n const { url } = input\n const trail = url ? validTrail(input.breadcrumbs) : undefined\n const crumbs = trail\n ? trail.map((c) => ({ name: c.name, item: absoluteUrl(c.path, input.site) }))\n : []\n const breadcrumbId = url && trail && crumbs.every((c) => c.item) ? url + '#breadcrumb' : undefined\n const authorUrl = siteAddress(input.authorUrl, input.site, input.trailingSlash)\n // An author's archive is about a person: the page is a ProfilePage and the\n // Person its main entity, at the page's own address — the one the author's\n // articles name as their author's url.\n const sameAs = (input.profile?.same_as ?? []).filter((u) => /^https?:\\\\/\\\\//i.test(u))\n const profile = url && input.profile?.name ? {\n '@type': 'Person',\n '@id': url + '#person',\n name: input.profile.name,\n url,\n ...(input.profile.description ? { description: input.profile.description } : {}),\n ...(absoluteUrl(input.profile.image, input.site) ? { image: absoluteUrl(input.profile.image, input.site) } : {}),\n ...(sameAs.length ? { sameAs } : {}),\n } : undefined\n if (url) {\n graph.push({\n '@type': profile ? 'ProfilePage' : input.pageType ?? 'WebPage',\n '@id': url,\n url,\n ...(input.title ? { name: input.title } : {}),\n ...(input.description ? { description: input.description } : {}),\n ...(input.locale ? { inLanguage: input.locale } : {}),\n ...(input.websiteId ? { isPartOf: { '@id': input.websiteId } } : {}),\n ...(breadcrumbId ? { breadcrumb: { '@id': breadcrumbId } } : {}),\n ...(profile ? { mainEntity: { '@id': profile['@id'] } } : {}),\n // A page is dated as its entry is — WordPress pages have dates too.\n ...(input.publishedAt ? { datePublished: input.publishedAt } : {}),\n ...(input.modifiedAt ? { dateModified: input.modifiedAt } : {}),\n })\n }\n if (profile) graph.push(profile)\n if (breadcrumbId) {\n graph.push({\n '@type': 'BreadcrumbList',\n '@id': breadcrumbId,\n itemListElement: [...crumbs, { name: input.headline ?? input.title, item: url }].map((c, i) => ({\n '@type': 'ListItem',\n position: i + 1,\n name: c.name,\n item: c.item,\n })),\n })\n }\n if (input.article) {\n graph.push({\n '@type': 'Article',\n headline: input.headline ?? input.title,\n ...(input.description ? { description: input.description } : {}),\n ...(input.image ? { image: [articleImage(input.image, input.imageMeta)] } : {}),\n ...(input.publishedAt ? { datePublished: input.publishedAt } : {}),\n ...(input.modifiedAt ? { dateModified: input.modifiedAt } : {}),\n // The author by the same @id their archive's ProfilePage gives its Person,\n // so the two are one entity; name and url stay for a reader of this page alone.\n ...(input.author ? { author: { '@type': 'Person', ...(authorUrl ? { '@id': authorUrl + '#person' } : {}), name: input.author, ...(authorUrl ? { url: authorUrl } : {}) } } : {}),\n ...(url ? { mainEntityOfPage: { '@id': url } } : {}),\n ...(publisher(input) ? { publisher: publisher(input) } : {}),\n })\n }\n return graph.length ? { '@context': 'https://schema.org', '@graph': graph } : undefined\n}\n\nconst SITE_SEARCH = 'SearchAction'\n\n/**\n * An \\`@id\\` compared as an address: the scheme, the host's case and a slash\n * before the fragment are not part of it, so \\`https://a.com/#website\\` and\n * \\`http://A.com#website\\` name one node — the head and a page's graph can\n * each have been rewritten to the new origin in their own way.\n */\nconst ldIdKey = (id: string): string => id.trim()\n .replace(/^(?:https?:)?\\\\/\\\\/[^/?#]*/i, (origin) => origin.toLowerCase().replace(/^https?:/, ''))\n .replace(/\\\\/+(?=#|$)/, '')\n\n/**\n * The page's structured data as the source's SEO plugin rendered it, ready to\n * print — or undefined, and the generated graph is used. What the plugin\n * described is kept as it was (FAQ, HowTo, Product, its WebPage and Article)\n * except a WebSite's SearchAction, which would be false on the migrated site\n * (a static site has no ?s= search), and the nodes the kept head already\n * carries (\\`headIds\\` — WebSite, Organization, its logo), which every page\n * would otherwise print twice.\n */\nexport function sourceStructuredData(schema: unknown, headIds: string[] = []): Record<string, unknown> | undefined {\n if (!schema || typeof schema !== 'object') return undefined\n const root = schema as Record<string, unknown>\n const graph = root['@graph']\n const nodes: unknown[] = Array.isArray(schema) ? schema : Array.isArray(graph) ? graph : [schema]\n const typesOf = (node: Record<string, unknown>): string[] =>\n (Array.isArray(node['@type']) ? node['@type'] : [node['@type']]).filter((t): t is string => typeof t === 'string')\n const inHead = new Set(headIds.map(ldIdKey))\n const kept: Array<Record<string, unknown>> = []\n for (const value of nodes) {\n if (!value || typeof value !== 'object' || Array.isArray(value)) continue\n let node = value as Record<string, unknown>\n if (!typesOf(node).length) continue\n const website = typesOf(node).some((t) => t.toLowerCase() === 'website')\n if (typeof node['@id'] === 'string' && inHead.has(ldIdKey(node['@id']))) continue\n if (website && node['potentialAction'] !== undefined) {\n const action = node['potentialAction']\n const actions = (Array.isArray(action) ? action : [action]).filter((a) =>\n !(a && typeof a === 'object' && typesOf(a as Record<string, unknown>).includes(SITE_SEARCH)))\n const { potentialAction: _, ...rest } = node\n node = actions.length ? { ...rest, potentialAction: Array.isArray(action) ? actions : actions[0] } : rest\n }\n kept.push(node)\n }\n if (!kept.length) return undefined\n const context = Array.isArray(schema) ? undefined : root['@context']\n return { '@context': context ?? 'https://schema.org', '@graph': kept }\n}\n\n// ─── Feed and llms.txt ───\n\n/**\n * Site-root address of an entry: its route pattern filled with the entry's\n * parameters — the values getStaticPaths builds the page from — in the\n * directory format the build writes. Undefined when a parameter is missing.\n */\nexport function entryAddress(pattern: string, params: Record<string, string | undefined>, trailingSlash = true): string | undefined {\n const segments = pattern.replace(/^\\\\/+|\\\\/+$/g, '').split('/').filter(Boolean)\n if (!segments.length) return '/'\n const out: string[] = []\n for (const segment of segments) {\n if (!segment.startsWith(':')) {\n out.push(segment)\n continue\n }\n const value = params[segment.slice(1).replace(/\\\\*$/, '')]?.replace(/^\\\\/+|\\\\/+$/g, '')\n if (!value) return undefined\n out.push(value)\n }\n return '/' + out.join('/') + (trailingSlash ? '/' : '')\n}\n\n/**\n * The address a built page is served at, from the path Astro gives it. The\n * file build names a page \\`/hello.html\\` and the host serves it at\n * \\`/hello\\`; the source site's own form — with or without the trailing\n * slash — is what its canonical, og:url and hreflang must name.\n */\nexport function pagePath(pathname: string, trailingSlash = true): string {\n const path = pathname.replace(/\\\\/index\\\\.html$/, '/').replace(/\\\\.html$/, '')\n if (path === '/' || path === '') return '/'\n const bare = path.replace(/\\\\/+$/, '')\n return trailingSlash ? bare + '/' : bare\n}\n\n/** One page as a feed item or an llms.txt link. */\nexport interface SiteLink {\n title: string\n url: string\n description?: string\n /** ISO 8601. */\n date?: string\n author?: string\n}\n\n/**\n * A collection's entries as absolute links, newest first; undated entries\n * keep their order after the dated ones. Only the entries in \\`locale\\` —\n * an entry without one is in the site's default, \\`siteLocale\\` — and with\n * \\`indexedOnly\\`, none the source kept out of search.\n */\nexport function entryLinks(posts: EmittedPost[], pattern: string, site: URL | undefined, locale: string, siteLocale: string, indexedOnly = false, trailingSlash = true): SiteLink[] {\n const links: SiteLink[] = []\n for (const post of posts) {\n if ((post.locale ?? siteLocale) !== locale) continue\n if (indexedOnly && post.noindex) continue\n const url = absoluteUrl(entryAddress(pattern, { ...(post.params ?? {}), slug: post.slug }, trailingSlash), site)\n if (!url) continue\n links.push({\n title: post.title,\n url,\n description: seoDescription(post.description ?? post.excerpt),\n date: post.published_at,\n author: post.author,\n })\n }\n const time = (link: SiteLink) => (link.date ? Date.parse(link.date) || 0 : 0)\n return links\n .map((link, index) => ({ link, index }))\n .sort((a, b) => time(b.link) - time(a.link) || a.index - b.index)\n .map((entry) => entry.link)\n}\n\n/** Text for XML: escaped, and without the control characters XML 1.0 forbids. */\nconst xmlText = (value: string): string => value\n .replace(/[\\\\u0000-\\\\u0008\\\\u000B\\\\u000C\\\\u000E-\\\\u001F]/g, '')\n .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;')\n\nexport interface RssChannel {\n title: string\n /** The site's home page, absolute. */\n link: string\n /** The feed's own address, absolute. */\n self: string\n description?: string\n language?: string\n items: SiteLink[]\n}\n\n/**\n * RSS 2.0, the format WordPress serves at /feed/. The channel's\n * \\`lastBuildDate\\` is its newest item's date, so rebuilding unchanged\n * content writes the same bytes.\n */\nexport function rssFeed(channel: RssChannel): string {\n const rfc822 = (iso: string | undefined): string | undefined => {\n const time = iso ? Date.parse(iso) : NaN\n return Number.isNaN(time) ? undefined : new Date(time).toUTCString()\n }\n const tag = (name: string, value: string | undefined) => (value ? '<' + name + '>' + xmlText(value) + '</' + name + '>' : '')\n const newest = channel.items.map((item) => rfc822(item.date)).find(Boolean)\n const items = channel.items.map((item) => [\n '<item>',\n tag('title', item.title),\n tag('link', item.url),\n '<guid isPermaLink=\"true\">' + xmlText(item.url) + '</guid>',\n tag('pubDate', rfc822(item.date)),\n tag('dc:creator', item.author),\n tag('description', item.description),\n '</item>',\n ].filter(Boolean).join(''))\n return [\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n '<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\">',\n '<channel>',\n tag('title', channel.title),\n tag('link', channel.link),\n '<atom:link href=\"' + xmlText(channel.self) + '\" rel=\"self\" type=\"application/rss+xml\"/>',\n // Required by RSS 2.0; a site without a tagline gets its title.\n tag('description', channel.description || channel.title),\n tag('language', channel.language),\n tag('lastBuildDate', newest),\n ...items,\n '</channel>',\n '</rss>',\n ].filter(Boolean).join('\\\\n') + '\\\\n'\n}\n\nexport interface LlmsSection {\n name: string\n links: SiteLink[]\n}\n\n/**\n * /llms.txt (llmstxt.org): the site's name, its tagline, and a section of\n * links per kind of page — what a language model reads to find its way\n * around the site, as Yoast's generator writes it.\n */\nexport function llmsTxt(input: { title: string; description?: string; sections: LlmsSection[] }): string {\n const line = (value: string) => value.replace(/\\\\s+/g, ' ').trim()\n const label = (value: string) => line(value).replace(/[\\\\[\\\\]\\\\\\\\]/g, '\\\\\\\\$&')\n const href = (url: string) => url.replace(/\\\\(/g, '%28').replace(/\\\\)/g, '%29').replace(/\\\\s/g, '%20')\n const out = ['# ' + line(input.title)]\n if (input.description && line(input.description)) out.push('', '> ' + line(input.description))\n for (const section of input.sections) {\n if (!section.links.length) continue\n out.push('', '## ' + line(section.name), '')\n for (const link of section.links) {\n const description = link.description ? line(link.description) : ''\n out.push('- [' + label(link.title) + '](' + href(link.url) + ')' + (description ? ': ' + description : ''))\n }\n }\n return out.join('\\\\n') + '\\\\n'\n}\n\n/** Emitted stylesheets live under /styles/legacy/ — pages reference them by file name. */\nexport const cssHref = (file: string): string => '/styles/legacy/' + (file.split('/').pop() ?? file)\n\n/** A term as a list card prints it: name, and the archive it links to. */\nexport interface TermRef {\n name: string\n link?: string\n}\n\n/** The display name of a term given as a string or a TermRef. */\nexport const termName = (term: string | TermRef): string => (typeof term === 'string' ? term : term.name)\n\nexport interface MarkablePost {\n slug: string\n title: string\n body?: string\n dates?: string[]\n author?: string\n author_first?: string\n author_last?: string\n authors?: string[]\n /**\n * Strings, or objects for themes whose term lists link each term\n * (\\`<!--@@repeat:terms@@-->\\` with \\`item_name\\` / \\`item_link\\`). The\n * \\`term{n}\\` and \\`terms\\` marks print the name either way.\n */\n terms?: Array<string | TermRef>\n featured?: string[]\n excerpt?: string\n excerpt_html?: string\n marks?: Record<string, unknown>\n}\n\n/**\n * The mark vocabulary: title, author (+ first/last), date{n}, term{n}, feat{n},\n * excerpt, slug — plus \\`terms\\`/\\`authors\\` as LISTS for repeat blocks, the\n * _html variants for raw insertion, and any producer-supplied extras.\n */\nexport function postMarks(post: MarkablePost): Values {\n const values: Values = {\n title: post.title,\n author: post.author ?? '',\n author_first: post.author_first ?? '',\n author_last: post.author_last ?? '',\n excerpt: post.excerpt ?? '',\n // The plain excerpt is text: escaped here, or a decoded \\`<script>\\` in it would print as markup.\n excerpt_html: post.excerpt_html ?? esc(post.excerpt ?? ''),\n body_html: post.body ?? '',\n slug: post.slug,\n feat: post.featured?.[0] ?? '',\n // Objects stay objects for repeat blocks (item_name / item_link); the joined\n // \\`terms\\` mark prints names, so fillMarks never sees [object Object].\n terms: (post.terms ?? []).map((t) => (typeof t === 'string' ? { name: t, link: '' } : { name: t.name, link: t.link ?? '' })),\n terms_names: (post.terms ?? []).map(termName),\n authors: post.authors ?? (post.author ? [post.author] : []),\n }\n for (const [i, d] of (post.dates ?? []).entries()) values['date' + i] = d\n for (const [i, t] of (post.terms ?? []).entries()) values['term' + i] = termName(t)\n for (const [i, f] of (post.featured ?? []).entries()) values['feat' + i] = f\n return { ...values, ...(post.marks ?? {}) }\n}\n\n/**\n * The shapes of the emitted data files. Pages import JSON and assert these\n * rather than letting TypeScript infer the shape from the file's contents:\n * inference reads only the fields the data HAPPENS to carry, so a site whose\n * posts need no extra route parameters produced a type without \\`params\\` and\n * \\`astro check\\` — which the build runs first — failed on the page that reads\n * it. The contract is what the emitter may write, not what one site wrote.\n */\n/** Content-store address of an entry — what a mounted comments component keys its thread by. */\nexport interface EntryRef {\n model_id: string\n entry_id: string\n locale?: string\n}\n\n/** Share-card values a page's source set by hand; each one left out is derived. */\nexport interface SocialOverride {\n title?: string\n description?: string\n image?: string\n}\n\nexport interface TwitterOverride extends SocialOverride {\n card?: string\n}\n\nexport interface SeoInput {\n title?: string\n headline?: string\n description?: string\n canonical?: string\n image?: string\n imageMeta?: ImageMeta\n alternates?: Array<{ lang: string; path: string }>\n breadcrumbs?: Breadcrumb[]\n pageType?: 'WebPage' | 'CollectionPage'\n type?: 'article' | 'website'\n publishedAt?: string\n modifiedAt?: string\n author?: string\n authorUrl?: string\n profile?: AuthorProfile\n /** An archive page's own feed, site-root-relative — the Seo component links it. */\n feed?: { path: string; title?: string }\n noindex?: boolean\n nofollow?: boolean\n openGraph?: SocialOverride\n twitter?: TwitterOverride\n schema?: unknown\n}\n\n/**\n * The SEO of one entry page. \\`featured\\` holds bare file names — only the\n * producer knows where media is served from — so an image is taken from it\n * only when it already reads as a path; otherwise \\`image\\` supplies it and\n * the tag is omitted rather than pointing at a URL that does not resolve.\n */\nexport function postSeo(post: EmittedPost, kind: 'post' | 'page' = 'post'): SeoInput {\n const featured = (post.featured ?? []).find((f) => f.startsWith('/') || /^https?:/i.test(f))\n return {\n // The document title an SEO plugin composed; the entry's own name stays the headline.\n title: post.seo_title || post.title,\n headline: post.title,\n description: post.description ?? post.excerpt,\n canonical: post.canonical,\n image: post.image ?? featured,\n // The measurements describe \\`image\\`; a \\`featured\\` fallback is another file.\n imageMeta: post.image ? post.image_meta : undefined,\n alternates: post.alternates,\n breadcrumbs: post.breadcrumbs,\n // A WordPress page is a web page, not an article: og:type website, no\n // Article node, no article:* tags — as the source's SEO plugin printed it.\n type: kind === 'page' ? 'website' : 'article',\n publishedAt: post.published_at,\n modifiedAt: post.modified_at,\n author: post.author,\n authorUrl: post.author_url,\n noindex: post.noindex,\n nofollow: post.nofollow,\n openGraph: post.open_graph,\n twitter: post.twitter,\n schema: post.schema,\n }\n}\n\nexport interface EmittedPost extends MarkablePost {\n body: string\n /** Meta description; falls back to the excerpt. */\n description?: string\n /** Social image, absolute or site-root-relative. */\n image?: string\n image_meta?: ImageMeta\n /** Translations of this entry, itself included, and x-default. Computed by the emitter. */\n alternates?: Array<{ lang: string; path: string }>\n /** Trail to this page, itself excluded. */\n breadcrumbs?: Breadcrumb[]\n /** Canonical override; default is the page's own address. */\n canonical?: string\n /** The author's page (their archive), absolute or site-root-relative. */\n author_url?: string\n /** Document title, when the source composed one apart from \\`title\\`. */\n seo_title?: string\n open_graph?: SocialOverride\n twitter?: TwitterOverride\n /** The source SEO plugin's JSON-LD for this page. */\n schema?: unknown\n /** ISO 8601, for Article structured data. */\n published_at?: string\n modified_at?: string\n /** Route parameters beyond slug — the date parts of a dated permalink, a post id. */\n params?: Record<string, string>\n /** Stylesheets only this page loads. */\n css?: string[]\n locale?: string\n /** Present when the producer bound this post to the content store. */\n entry?: EntryRef\n /** Kept out of search: robots meta, and no sitemap entry. */\n noindex?: boolean\n nofollow?: boolean\n}\n\n/** One static path of a list route, as it appears in the emitted query data. */\nexport interface EmittedQueryPage {\n params: Record<string, string>\n items: EmittedPost[]\n marks?: Record<string, unknown>\n css?: string[]\n item_template?: string\n sections?: ListSection[]\n title?: string\n description?: string\n image?: string\n image_meta?: ImageMeta\n canonical?: string\n breadcrumbs?: Breadcrumb[]\n noindex?: boolean\n nofollow?: boolean\n open_graph?: SocialOverride\n twitter?: TwitterOverride\n schema?: unknown\n /** On an author's archive: who the page is about. */\n profile?: AuthorProfile\n}\n`\n","// Fragment balance check.\n//\n// Chrome is injected as raw HTML, so an unbalanced fragment does not fail the\n// build — the browser silently repairs it and the page loses its layout. That\n// is the failure that cost a page 36 against 100 when body chrome was split\n// into before/after halves. Lifting a header or footer out of the body blob is\n// only safe when each fragment closes what it opens, so the emitter checks and\n// says which tags are dangling rather than trusting the producer's word.\n//\n// This is deliberately not an HTML parser: it skips comments, doctypes and raw\n// text elements, tolerates the tags HTML lets you leave open, and reports what\n// it finds. It answers \"was this split mid-element?\", not \"is this valid HTML?\".\n\nconst VOID_TAGS = new Set([\n 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',\n 'link', 'meta', 'param', 'source', 'track', 'wbr',\n])\n\n/** Tags a browser closes for you. Theme markup is full of them, so counting\n * them would report every real page as broken. */\nconst OPTIONAL_END_TAGS = new Set([\n 'p', 'li', 'dt', 'dd', 'option', 'optgroup', 'thead', 'tbody',\n 'tfoot', 'tr', 'td', 'th', 'rt', 'rp',\n])\n\n/** Elements whose content is text, not markup — `<script>if (a < b)</script>`. */\nconst RAW_TEXT_TAGS = new Set(['script', 'style', 'textarea', 'title'])\n\ninterface Tag {\n name: string\n closing: boolean\n selfClosing: boolean\n}\n\nfunction scanTags(html: string): Tag[] {\n const tags: Tag[] = []\n let i = 0\n while (i < html.length) {\n const lt = html.indexOf('<', i)\n if (lt === -1) break\n if (html.startsWith('<!--', lt)) {\n const end = html.indexOf('-->', lt + 4)\n i = end === -1 ? html.length : end + 3\n continue\n }\n if (html.startsWith('<!', lt) || html.startsWith('<?', lt)) {\n const end = html.indexOf('>', lt)\n i = end === -1 ? html.length : end + 1\n continue\n }\n const head = /^<(\\/?)([a-zA-Z][a-zA-Z0-9-]*)/.exec(html.slice(lt, lt + 64))\n const [matched = '', slash = '', rawName = ''] = head ?? []\n if (!rawName) {\n i = lt + 1\n continue\n }\n // Walk to the tag's `>`, ignoring one inside a quoted attribute value.\n let j = lt + matched.length\n let quote = ''\n while (j < html.length) {\n const ch = html[j]\n if (quote) {\n if (ch === quote) quote = ''\n } else if (ch === '\"' || ch === \"'\") {\n quote = ch\n } else if (ch === '>') {\n break\n }\n j++\n }\n const name = rawName.toLowerCase()\n const closing = slash === '/'\n tags.push({ name, closing, selfClosing: html.slice(lt, j + 1).endsWith('/>') })\n i = j + 1\n if (!closing && RAW_TEXT_TAGS.has(name)) {\n // Searched in `html` itself, not in `html.toLowerCase()`: lowercasing\n // changes the length of some text ('İ' becomes two code units), so an\n // index found in the lowered copy lands past the real `</script` once\n // such a character comes before it — and every raw-text element after a\n // Turkish capital read as never closed. HTML matches tag names by ASCII\n // case only, which is what `i` without `u` does.\n const end = new RegExp(`</${name}`, 'gi')\n end.lastIndex = i\n const close = end.exec(html)?.index ?? -1\n i = close === -1 ? html.length : close\n }\n }\n return tags\n}\n\nexport interface BalanceReport {\n /** Elements opened and never closed — the fragment ends mid-element. */\n unclosed: string[]\n /** Elements closed but never opened — the fragment starts mid-element. */\n unopened: string[]\n}\n\nexport function checkBalance(html: string): BalanceReport {\n const stack: string[] = []\n const unopened: string[] = []\n for (const tag of scanTags(html)) {\n if (VOID_TAGS.has(tag.name) || tag.selfClosing || OPTIONAL_END_TAGS.has(tag.name)) continue\n if (!tag.closing) {\n stack.push(tag.name)\n continue\n }\n const at = stack.lastIndexOf(tag.name)\n if (at === -1) unopened.push(tag.name)\n else stack.length = at\n }\n return { unclosed: stack.toReversed(), unopened }\n}\n\n/** Human-readable balance complaint, or null when the fragment is sound. */\nexport function balanceWarning(html: string): string | null {\n const { unclosed, unopened } = checkBalance(html)\n const parts: string[] = []\n if (unclosed.length) parts.push(`never closed: ${unclosed.join(', ')}`)\n if (unopened.length) parts.push(`closed but never opened: ${unopened.join(', ')}`)\n return parts.length ? parts.join('; ') : null\n}\n","// Per-page SEO: the one head region the emitter owns.\n//\n// A migrated page used to inherit the TEMPLATE page's head verbatim, so every\n// post carried the template's `<link rel=\"canonical\">`, its `og:title` and its\n// Article JSON-LD. That is worse than having none: a whole site canonicalised\n// onto one URL de-indexes itself, and every share card shows the wrong story.\n// SEO continuity is the reason a migration keeps the source addresses at all,\n// so the emitter derives these tags per page from the entry and removes the\n// template's copies rather than letting two of each fight.\n//\n// What stays: everything else the theme put in `<head>` — charset, preloads,\n// feeds, icons, verification tokens, and the structured data that describes\n// the site rather than the page (WebSite, Organization).\n\n/**\n * Tags the emitter owns; a source copy of any of these is replaced, not\n * duplicated. `twitter:site` is the site's handle, the same on every page and\n * not derivable from an entry, so it stays in the head.\n */\nconst TITLE_RE = /<title\\b[^>]*>[\\s\\S]*?<\\/title>\\s*/gi\nconst META_RE = /<meta\\b[^>]*\\b(?:name|property)\\s*=\\s*[\"'](?:description|robots|googlebot|og:[^\"']*|twitter:(?!site[\"'])[^\"']*)[\"'][^>]*>\\s*/gi\nconst CANONICAL_RE = /<link\\b[^>]*\\brel\\s*=\\s*[\"'][^\"']*\\bcanonical\\b[^\"']*[\"'][^>]*>\\s*/gi\n/**\n * A translation link names the template page's translations. An RSS\n * `rel=\"alternate\"` carries no hreflang and stays.\n */\nconst HREFLANG_RE = /<link\\b(?=[^>]*\\brel\\s*=\\s*[\"']?[^\"'>]*\\balternate\\b)(?=[^>]*\\bhreflang\\s*=)[^>]*>\\s*/gi\nconst JSONLD_RE = /<script\\b[^>]*\\btype\\s*=\\s*[\"']application\\/ld\\+json[\"'][^>]*>([\\s\\S]*?)<\\/script>\\s*/gi\n\n/**\n * Structured data that describes THIS page. An Article names one post, a\n * WebPage (and every subtype — CollectionPage for an archive, ProfilePage for an\n * author) one address, a BreadcrumbList one page's trail: copied from the\n * template, each is a lie on every other page. Matched by suffix because\n * schema.org names its page and article subtypes that way.\n */\nfunction isPageType(type: string): boolean {\n const t = type.toLowerCase()\n return t.endsWith('page') || t.endsWith('article') || PAGE_POSTINGS.has(t)\n}\nconst PAGE_POSTINGS = new Set(['blogposting', 'liveblogposting', 'socialmediaposting', 'discussionforumposting', 'breadcrumblist'])\n\n/**\n * Structured data that describes the SITE and is true on every page. An\n * SEO plugin writes it into the same `@graph` as the page's own nodes — Yoast\n * and Rank Math put WebSite, Organization, WebPage and BreadcrumbList in one\n * block — so removing page-scoped blocks whole took the site's identity with\n * them.\n */\nconst SITE_TYPES = new Set([\n 'website', 'organization', 'corporation', 'localbusiness', 'newsmediaorganization',\n 'educationalorganization', 'ngo', 'onlinebusiness', 'onlinestore', 'governmentorganization',\n])\n\ntype LdNode = Record<string, unknown>\n\nconst typesOf = (node: unknown): string[] => {\n const type = (node as LdNode | null)?.['@type']\n return (Array.isArray(type) ? type : [type]).filter((t): t is string => typeof t === 'string')\n}\nconst isPageNode = (node: unknown) => typesOf(node).some(isPageType)\nconst isSiteNode = (node: unknown) => typesOf(node).some((t) => SITE_TYPES.has(t.toLowerCase()))\n\n/** Every `{\"@id\": …}` reference anywhere inside a node. */\nfunction references(value: unknown, out = new Set<string>()): Set<string> {\n if (Array.isArray(value)) {\n for (const v of value) references(v, out)\n } else if (value && typeof value === 'object') {\n const node = value as LdNode\n const keys = Object.keys(node)\n if (keys.length === 1 && typeof node['@id'] === 'string') out.add(node['@id'])\n for (const v of Object.values(node)) references(v, out)\n }\n return out\n}\n\n/**\n * A WebSite's SearchAction points at WordPress search (`/?s=`). A static site\n * has no search endpoint, so the claim would be false on the new site.\n */\nfunction withoutSearchAction(node: LdNode): { node: LdNode; dropped: boolean } {\n const action = node['potentialAction']\n const actions = Array.isArray(action) ? action : action === undefined ? [] : [action]\n const kept = actions.filter((a) => !typesOf(a).includes('SearchAction'))\n if (kept.length === actions.length) return { node, dropped: false }\n const { potentialAction: _, ...rest } = node\n return { node: kept.length ? { ...rest, potentialAction: Array.isArray(action) ? kept : kept[0] } : rest, dropped: true }\n}\n\ninterface LdDecision {\n /** The block to keep, re-serialized; undefined to keep the original bytes; null to remove it. */\n json: string | null | undefined\n kept: string[]\n droppedSearch: boolean\n}\n\n/**\n * What to do with one JSON-LD block. A block with no page-scoped node stays as\n * it was. A block with one loses its page-scoped nodes and keeps the site's —\n * WebSite, Organization — together with the nodes those refer to by `@id` (a\n * logo, the person a personal site is published by), never pulling a page node\n * back in. Nothing site-scoped left means the block goes.\n */\nfunction decideLd(json: string): LdDecision {\n let parsed: unknown\n try {\n parsed = JSON.parse(json)\n } catch {\n // Unparseable JSON-LD is left alone: removing markup we cannot read would\n // be guessing, and a broken block is the theme's problem, not ours.\n return { json: undefined, kept: [], droppedSearch: false }\n }\n const graph = (parsed as { '@graph'?: unknown } | null)?.['@graph']\n const nodes: unknown[] = Array.isArray(parsed) ? parsed : Array.isArray(graph) ? graph : [parsed]\n if (!nodes.some(isPageNode)) return { json: undefined, kept: [], droppedSearch: false }\n\n const byId = new Map<string, unknown>()\n for (const node of nodes) {\n const id = (node as LdNode | null)?.['@id']\n if (typeof id === 'string') byId.set(id, node)\n }\n const keep = new Set<unknown>(nodes.filter((n) => isSiteNode(n) && !isPageNode(n)))\n const queue = [...keep]\n while (queue.length) {\n for (const id of references(queue.shift())) {\n const target = byId.get(id)\n if (target && !keep.has(target) && !isPageNode(target)) {\n keep.add(target)\n queue.push(target)\n }\n }\n }\n if (!keep.size) return { json: null, kept: [], droppedSearch: false }\n\n let droppedSearch = false\n const kept = nodes.filter((n) => keep.has(n)).map((n) => {\n if (!typesOf(n).some((t) => t.toLowerCase() === 'website')) return n\n const result = withoutSearchAction(n as LdNode)\n droppedSearch ||= result.dropped\n return result.node\n })\n const context = (parsed as LdNode | null)?.['@context']\n const rebuilt = Array.isArray(parsed)\n ? kept\n : Array.isArray(graph)\n ? { ...(context === undefined ? {} : { '@context': context }), '@graph': kept }\n : kept[0]\n return {\n // `<` escaped so a string value can never close the script element.\n json: JSON.stringify(rebuilt).replace(/</g, '\\\\u003c'),\n kept: [...new Set(kept.flatMap(typesOf))],\n droppedSearch,\n }\n}\n\n/**\n * \\`@id\\` of the WebSite node in a head's structured data, if it declares one\n * — the node the page's WebPage says it is part of. Only what the head says:\n * no id, no link.\n */\nexport function websiteIdOf(html: string): string | undefined {\n for (const match of html.matchAll(JSONLD_RE)) {\n let parsed: unknown\n try {\n parsed = JSON.parse(match[1] ?? '')\n } catch {\n continue\n }\n const graph = (parsed as { '@graph'?: unknown } | null)?.['@graph']\n const nodes: unknown[] = Array.isArray(parsed) ? parsed : Array.isArray(graph) ? graph : [parsed]\n for (const node of nodes) {\n const id = (node as LdNode | null)?.['@id']\n if (typeof id === 'string' && id && typesOf(node).some((t) => t.toLowerCase() === 'website')) return id\n }\n }\n return undefined\n}\n\n/**\n * \\`@id\\` of every node in a head's structured data — the site's own nodes\n * (WebSite, Organization, its logo) that every page already carries. A page's\n * plugin graph repeats them; printing them twice is valid but is noise.\n */\nexport function headLdIdsOf(html: string): string[] {\n const ids: string[] = []\n for (const match of html.matchAll(JSONLD_RE)) {\n let parsed: unknown\n try {\n parsed = JSON.parse(match[1] ?? '')\n } catch {\n continue\n }\n const graph = (parsed as { '@graph'?: unknown } | null)?.['@graph']\n const nodes: unknown[] = Array.isArray(parsed) ? parsed : Array.isArray(graph) ? graph : [parsed]\n for (const node of nodes) {\n const id = (node as LdNode | null)?.['@id']\n if (typeof id === 'string' && id && !ids.includes(id)) ids.push(id)\n }\n }\n return ids\n}\n\n/**\n * `@id` of the node that publishes the site, as the head's structured data\n * says: the WebSite's own `publisher` reference (Yoast and Rank Math write\n * one — an Organization, or the Person a personal site is published by),\n * else the head's first Organization. The Article names it by `@id`, so the\n * logo and profile the site declared count for every post.\n */\nexport function publisherIdOf(html: string): string | undefined {\n // A reference only counts when the node it names is on the page: an Article\n // pointing at an @id nothing declares names no one.\n const declared = new Set(headLdIdsOf(html))\n let organization: string | undefined\n for (const match of html.matchAll(JSONLD_RE)) {\n let parsed: unknown\n try {\n parsed = JSON.parse(match[1] ?? '')\n } catch {\n continue\n }\n const graph = (parsed as { '@graph'?: unknown } | null)?.['@graph']\n const nodes: unknown[] = Array.isArray(parsed) ? parsed : Array.isArray(graph) ? graph : [parsed]\n for (const node of nodes) {\n const types = typesOf(node).map((t) => t.toLowerCase())\n if (types.includes('website')) {\n const ref = (node as LdNode)['publisher']\n const id = (Array.isArray(ref) ? ref[0] : ref) as LdNode | undefined\n if (typeof id?.['@id'] === 'string' && declared.has(id['@id'])) return id['@id']\n }\n const id = (node as LdNode | null)?.['@id']\n if (!organization && typeof id === 'string' && id && isSiteNode(node) && !types.includes('website')) organization = id\n }\n }\n return organization\n}\n\nexport interface StripResult {\n html: string\n /** What was taken out, for the emit warning — never a silent removal. */\n removed: string[]\n /** Site-wide structured-data types kept out of a block that was otherwise page-scoped. */\n kept: string[]\n /**\n * The robots directives of the template's `robots` / `googlebot` tags that\n * are site settings rather than this page's indexing — Yoast's\n * `max-image-preview:large, max-snippet:-1, max-video-preview:-1`. The Seo\n * component prints them on every page, after the page's own noindex/nofollow.\n */\n robots: string[]\n}\n\n/** Directives that say whether THIS page is indexed or followed; everything else is a site setting. */\nconst PAGE_ROBOTS = new Set(['index', 'noindex', 'follow', 'nofollow', 'all', 'none'])\n\nfunction siteRobots(tag: string, into: string[]): void {\n const content = /\\bcontent\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)')/i.exec(tag)\n for (const raw of (content?.[1] ?? content?.[2] ?? '').split(',')) {\n const directive = raw.trim().toLowerCase()\n if (directive && !PAGE_ROBOTS.has(directive) && !into.includes(directive)) into.push(directive)\n }\n}\n\n/** Remove the source head's per-page SEO tags so the emitter's own are the only ones. */\nexport function stripSeoTags(html: string): StripResult {\n const removed: string[] = []\n const kept: string[] = []\n const robots: string[] = []\n const note = (label: string) => {\n if (!removed.includes(label)) removed.push(label)\n }\n let out = html.replace(TITLE_RE, () => {\n note('<title>')\n return ''\n })\n out = out.replace(META_RE, (tag) => {\n const name = /\\b(?:name|property)\\s*=\\s*[\"']([^\"']+)[\"']/i.exec(tag)?.[1] ?? 'meta'\n if (/^(?:robots|googlebot)$/i.test(name)) siteRobots(tag, robots)\n note(name.toLowerCase().startsWith('og:') ? 'og:*' : name.toLowerCase().startsWith('twitter:') ? 'twitter:*' : name)\n return ''\n })\n out = out.replace(CANONICAL_RE, () => {\n note('canonical')\n return ''\n })\n out = out.replace(HREFLANG_RE, () => {\n note('hreflang')\n return ''\n })\n out = out.replace(JSONLD_RE, (tag: string, json: string) => {\n const decision = decideLd(json)\n if (decision.json === undefined) return tag\n note('page-scoped JSON-LD')\n if (decision.droppedSearch) note('WebSite SearchAction')\n for (const type of decision.kept) if (!kept.includes(type)) kept.push(type)\n if (decision.json === null) return ''\n const open = tag.slice(0, tag.indexOf('>') + 1)\n return `${open}${decision.json}</script>\\n`\n })\n return { html: out, removed, kept, robots }\n}\n\n/**\n * Head-only tags found in BODY chrome. A browser that closed `<head>` early\n * leaves the template's canonical and og tags in the body, and a faithful clone\n * carries them there — so the page ends up with the emitter's correct tags AND\n * the template's stale ones, which no fidelity score can see.\n *\n * They are reported, not removed: the body is page content, and `<title>` is\n * legal inside `<svg>`, so cutting tags out of it would break real markup to\n * fix an invisible one. Only unambiguous head tags are looked for.\n */\nexport function bodySeoLeaks(html: string): string[] {\n const found: string[] = []\n if (/<link\\b[^>]*\\brel\\s*=\\s*[\"'][^\"']*\\bcanonical\\b/i.test(html)) found.push('canonical')\n if (/<meta\\b[^>]*\\bproperty\\s*=\\s*[\"']og:/i.test(html)) found.push('og:*')\n if (/<meta\\b[^>]*\\bname\\s*=\\s*[\"']description[\"']/i.test(html)) found.push('description')\n return found\n}\n\n/**\n * `src/components/Seo.astro`. The canonical address comes from `Astro.url` and\n * `Astro.site` rather than from data: it is then, by construction, the address\n * Astro actually generated, and no producer has to recompute a permalink.\n */\nexport const SEO_COMPONENT = `---\n/**\n * Per-page SEO — emitted by @contentrain/emitter-astro.\n *\n * Title, description, canonical, Open Graph, Twitter card and (on entry pages)\n * Article structured data, derived from the entry. The source theme's copies of\n * these tags are removed from the head chrome at emit time, so each appears once.\n *\n * Canonical and absolute URLs need \\`site\\` in astro.config.mjs; without it the\n * canonical link and og:url are omitted rather than pointing at a build host.\n */\nimport { absoluteUrl, imageMetaTags, jsonLd, ogLocale, pagePath, pageStructuredData, seoDescription, sourceStructuredData, type AuthorProfile, type Breadcrumb, type ImageMeta, type SocialOverride, type TwitterOverride } from '../lib/fill'\n\ninterface Props {\n /** The document title — an SEO plugin's composed one when the source had it. */\n title?: string\n /** The entry's own title, when it differs from \\`title\\`: Article headline, last breadcrumb. */\n headline?: string\n /** Meta description; falls back to the entry's excerpt, tags stripped. */\n description?: string\n /** Overrides the generated address — for a page that canonicalises elsewhere. */\n canonical?: string\n /** Social image: absolute, or site-root-relative. */\n image?: string\n /** Size and type of \\`image\\`, as the producer measured it. */\n imageMeta?: ImageMeta\n /** This page's translations, itself included, plus x-default. */\n alternates?: Array<{ lang: string; path: string }>\n /** The trail to this page, itself excluded — the page is appended with its own address. */\n breadcrumbs?: Breadcrumb[]\n /** \\`CollectionPage\\` on a list; \\`WebPage\\` otherwise. */\n pageType?: 'WebPage' | 'CollectionPage'\n /** \\`@id\\` of the site's WebSite node kept in the head chrome, when there is one. */\n websiteId?: string\n /** \\`@id\\` of the node the kept head says publishes the site — the Article's publisher. */\n publisherId?: string\n /** false: the site's addresses end without a slash (\\`/hello\\`), as the source's did. */\n trailingSlash?: boolean\n /** \\`@id\\` of every structured-data node the head chrome keeps — not repeated from \\`schema\\`. */\n headLdIds?: string[]\n /** \\`article\\` on entry pages, \\`website\\` on lists and static pages. */\n type?: 'article' | 'website'\n /** ISO 8601 — structured data only; the displayed date stays a mark. */\n publishedAt?: string\n modifiedAt?: string\n author?: string\n /** The author's page, for the Article author's \\`url\\`. */\n authorUrl?: string\n /** On an author's archive: who the page is about — ProfilePage + Person. */\n profile?: AuthorProfile\n /** This page's own feed (an archive's), site-root-relative: \\`<link rel=\"alternate\" type=\"application/rss+xml\">\\`. */\n feed?: { path: string; title?: string }\n siteName?: string\n locale?: string\n /** The page is kept out of search: \\`<meta name=\"robots\" content=\"noindex\">\\`. */\n noindex?: boolean\n nofollow?: boolean\n /** Site-wide robots directives from the template head (\\`max-image-preview:large\\`, …), printed on every page. */\n robotsDefault?: string[]\n /** Share-card values the source set by hand, over the derived ones. */\n openGraph?: SocialOverride\n /** Unset values follow Open Graph. */\n twitter?: TwitterOverride\n /** The source SEO plugin's JSON-LD for this page — printed instead of the generated graph. */\n schema?: unknown\n}\n\nconst {\n title = '',\n headline,\n description,\n canonical,\n image,\n imageMeta,\n alternates = [],\n breadcrumbs,\n pageType,\n websiteId,\n publisherId,\n trailingSlash = true,\n headLdIds = [],\n type = 'website',\n publishedAt,\n modifiedAt,\n author,\n authorUrl,\n profile,\n feed,\n siteName,\n locale,\n noindex = false,\n nofollow = false,\n robotsDefault = [],\n openGraph,\n twitter,\n schema,\n} = Astro.props\n\nconst site = Astro.site\nconst url = canonical ? absoluteUrl(canonical, site) : absoluteUrl(pagePath(Astro.url.pathname, trailingSlash), site)\nconst imageUrl = absoluteUrl(image, site)\nconst imageTags = imageUrl ? imageMetaTags(imageMeta) : {}\n// hreflang needs absolute URLs; without \\`site\\` there are none to give.\nconst hreflang = alternates.flatMap((a) => {\n const href = absoluteUrl(pagePath(a.path, trailingSlash), site)\n return href ? [{ lang: a.lang, href }] : []\n})\n// og:locale takes ll_RR; a language without a region has no such form and is left out.\nconst ogLang = ogLocale(locale)\nconst localeAlternates = [...new Set(hreflang.map((a) => ogLocale(a.lang)))].filter((l): l is string => Boolean(l) && l !== ogLang)\nconst desc = seoDescription(description)\nconst robots = [noindex && 'noindex', nofollow && 'nofollow', ...robotsDefault].filter(Boolean).join(', ')\n// Share cards: what the source set by hand, else what the page says.\nconst ogTitle = openGraph?.title || title\nconst ogDesc = seoDescription(openGraph?.description) ?? desc\nconst ogImage = absoluteUrl(openGraph?.image, site) ?? imageUrl\n// The measurements describe \\`image\\`; an override is another file.\nconst ogImageTags = ogImage === imageUrl ? imageTags : {}\nconst twTitle = twitter?.title || ogTitle\nconst twDesc = seoDescription(twitter?.description) ?? ogDesc\nconst twImage = absoluteUrl(twitter?.image, site) ?? ogImage\nconst twCard = twitter?.card === 'summary' || twitter?.card === 'summary_large_image' ? twitter.card : twImage ? 'summary_large_image' : 'summary'\nconst article = type === 'article'\nconst structured = sourceStructuredData(schema, headLdIds) ?? pageStructuredData({\n url,\n site,\n title,\n headline,\n description: desc,\n image: imageUrl,\n imageMeta: imageUrl ? imageMeta : undefined,\n locale,\n article,\n pageType,\n publishedAt,\n modifiedAt,\n author,\n authorUrl,\n profile,\n trailingSlash,\n siteName,\n websiteId,\n publisherId,\n breadcrumbs,\n})\n---\n<title>{title}</title>\n{robots && <meta name=\"robots\" content={robots} />}\n{desc && <meta name=\"description\" content={desc} />}\n{url && <link rel=\"canonical\" href={url} />}\n{hreflang.map((a) => <link rel=\"alternate\" hreflang={a.lang} href={a.href} />)}\n{feed && <link rel=\"alternate\" type=\"application/rss+xml\" title={feed.title} href={feed.path} />}\n<meta property=\"og:type\" content={type} />\n{ogTitle && <meta property=\"og:title\" content={ogTitle} />}\n{ogDesc && <meta property=\"og:description\" content={ogDesc} />}\n{url && <meta property=\"og:url\" content={url} />}\n{ogImage && <meta property=\"og:image\" content={ogImage} />}\n{ogImageTags.width && <meta property=\"og:image:width\" content={ogImageTags.width} />}\n{ogImageTags.height && <meta property=\"og:image:height\" content={ogImageTags.height} />}\n{ogImageTags.type && <meta property=\"og:image:type\" content={ogImageTags.type} />}\n{siteName && <meta property=\"og:site_name\" content={siteName} />}\n{ogLang && <meta property=\"og:locale\" content={ogLang} />}\n{localeAlternates.map((l) => <meta property=\"og:locale:alternate\" content={l} />)}\n{article && publishedAt && <meta property=\"article:published_time\" content={publishedAt} />}\n{article && modifiedAt && <meta property=\"article:modified_time\" content={modifiedAt} />}\n{article && author && <meta property=\"article:author\" content={author} />}\n<meta name=\"twitter:card\" content={twCard} />\n{twTitle && <meta name=\"twitter:title\" content={twTitle} />}\n{twDesc && <meta name=\"twitter:description\" content={twDesc} />}\n{twImage && <meta name=\"twitter:image\" content={twImage} />}\n{structured && <script is:inline type=\"application/ld+json\" set:html={jsonLd(structured)} />}\n`\n","// The site's RSS feed and its /llms.txt (MG-15 §3–4).\n//\n// WordPress serves every site's newest posts as RSS at /feed/, and readers,\n// aggregators and newsletter tools subscribe to it; Yoast writes an llms.txt\n// that tells a language model what the site holds. A migration that drops\n// the first breaks every subscription, and one that never writes the second\n// leaves the site harder for AI search to read than it was.\n//\n// Both are Astro endpoints over the data files the entry pages are built\n// from, and address each entry the way its page is addressed — the route\n// pattern filled with the entry's parameters — so neither can list a page\n// the site does not build, or miss one it does.\n//\n// The feed is built at /feed.xml, not /feed/: a static build writes an\n// endpoint at /feed/ as a file named `feed`, which no host serves at /feed/.\n// The theme's head link to the feed is pointed at the new address, and /feed/\n// gets a 301 to it in the redirect config and the host's redirect file.\n\nimport type { ProjectIR, RouteModel } from '@contentrain/types'\nimport { DEFAULT_COLLECTION } from './types.js'\nimport { patternToPagePath } from './util.js'\n\n/** Where the feed is built. */\nexport const FEED_PATH = '/feed.xml'\n/** WordPress's feed address, redirected (301) to FEED_PATH. */\nexport const FEED_REDIRECT_FROM = '/feed/'\n/** Items in the feed — WordPress's default `posts_per_rss`. */\nexport const FEED_ITEMS = 10\n/** Links per llms.txt section. A long archive is summarised by its newest pages. */\nexport const LLMS_LINKS = 100\n\n/** A collection some route builds one page per entry of, and the pattern it builds them at. */\nexport interface LinkSource {\n collection: string\n pattern: string\n}\n\n/**\n * The collections the site builds entry pages for in its default language,\n * each with the first route that does: posts first, pages next, the rest by\n * name. A route in another language is left out — the feed and llms.txt\n * describe the site as it is in its default one.\n */\nexport function linkSources(routes: RouteModel[], siteLocale: string): LinkSource[] {\n const byCollection = new Map<string, string>()\n for (const route of routes) {\n if (route.kind !== 'single' && route.collection === undefined) continue\n if ((route.locale ?? siteLocale) !== siteLocale) continue\n const pagePath = patternToPagePath(route.pattern)\n if (!pagePath || !pagePath.includes('[')) continue\n const collection = route.collection ?? DEFAULT_COLLECTION\n if (!byCollection.has(collection)) byCollection.set(collection, route.pattern)\n }\n const rank = (c: string) => (c === DEFAULT_COLLECTION ? 0 : c === 'pages' ? 1 : 2)\n return [...byCollection]\n .map(([collection, pattern]) => ({ collection, pattern }))\n .toSorted((a, b) => rank(a.collection) - rank(b.collection) || (a.collection < b.collection ? -1 : a.collection > b.collection ? 1 : 0))\n}\n\n/** An archive route (a category, a tag, an author) whose pages get a feed each, as WordPress's do. */\nexport interface ArchiveFeedSource {\n routeId: string\n pattern: string\n query: string\n}\n\n/**\n * Archive routes in the site's default language whose every page is an\n * address with a feed: a query route with parameters and no page number —\n * `/category/:term*`, `/tag/:term`, `/author/:author`. The paginated\n * continuation (`/category/:term/page/:page`) has no feed of its own.\n */\nexport function archiveFeedSources(routes: RouteModel[], siteLocale: string): ArchiveFeedSource[] {\n const out: ArchiveFeedSource[] = []\n for (const route of routes) {\n if (!route.query || (route.locale ?? siteLocale) !== siteLocale) continue\n const pagePath = patternToPagePath(route.pattern)\n if (!pagePath || !pagePath.includes('[')) continue\n if (route.pattern.split('/').some((seg) => /^:page\\*?$/.test(seg))) continue\n out.push({ routeId: route.id, pattern: route.pattern, query: route.query })\n }\n return out\n}\n\n/** Where an archive route's feed endpoint lives: its page path with `/feed.xml` appended. */\nexport function archiveFeedFile(pattern: string): string | null {\n const pagePath = patternToPagePath(pattern)\n return pagePath ? `src/pages/${pagePath.replace(/\\.astro$/, '')}/feed.xml.ts` : null\n}\n\n/**\n * The feed of every page an archive route builds — the newest posts it lists,\n * at their own addresses — at `<archive>/feed.xml`. Its entries are the\n * route's own first result set, so the feed lists what the archive page does.\n */\nexport function archiveFeedEndpoint(source: ArchiveFeedSource, postPattern: string, input: FeedInput): string {\n const locale = JSON.stringify(input.siteLocale)\n const file = archiveFeedFile(source.pattern)!\n const up = '../'.repeat(file.split('/').length - 2)\n const slash = input.trailingSlash === false ? ', false, false' : ''\n return `// Archive feed for route ${source.routeId} (${source.pattern}) — emitted by @contentrain/emitter-astro.\n// One feed per page the archive builds, at <archive>/feed.xml, as WordPress serves\n// one at <archive>/feed/: the ${FEED_ITEMS} newest posts the archive lists.\nimport type { APIRoute, GetStaticPaths } from 'astro'\nimport data from '${up}data/queries/${source.query}.json'\nimport { absoluteUrl, entryAddress, entryLinks, rssFeed, type EmittedQueryPage } from '${up}lib/fill'\n\nexport const getStaticPaths = (() => (data as EmittedQueryPage[]).map((page) => ({ params: page.params, props: { page } }))) satisfies GetStaticPaths\n\nexport const GET: APIRoute = ({ props, site }) => {\n const page = (props as { page: EmittedQueryPage }).page\n const address = entryAddress(${JSON.stringify(source.pattern)}, page.params) ?? '/'\n const items = entryLinks(page.items, ${JSON.stringify(postPattern)}, site, ${locale}, ${locale}${slash}).slice(0, ${FEED_ITEMS})\n const body = rssFeed({\n title: page.title || ${JSON.stringify(siteTitle(input.ir))},\n link: absoluteUrl(address, site) ?? '',\n self: absoluteUrl(address + 'feed.xml', site) ?? '',\n description: page.description ?? ${JSON.stringify(input.description ?? '')},\n language: ${locale},\n items,\n })\n return new Response(body, { headers: { 'Content-Type': 'application/rss+xml; charset=utf-8' } })\n}\n`\n}\n\nconst sectionName = (collection: string) => collection.charAt(0).toUpperCase() + collection.slice(1).replace(/[-_]+/g, ' ')\n\nfunction siteTitle(ir: ProjectIR): string {\n if (ir.site.title?.trim()) return ir.site.title.trim()\n try {\n return new URL(ir.site.url).host\n } catch {\n return ir.site.url\n }\n}\n\nexport interface FeedInput {\n ir: ProjectIR\n siteLocale: string\n /** The site's tagline, for the channel description and the llms.txt summary. */\n description?: string\n /** false: entries are addressed without a trailing slash. */\n trailingSlash?: boolean\n}\n\n/** `src/pages/feed.xml.ts`: the newest posts as RSS 2.0. */\nexport function feedEndpoint(source: LinkSource, input: FeedInput): string {\n const locale = JSON.stringify(input.siteLocale)\n return `// RSS feed at ${FEED_PATH} — emitted by @contentrain/emitter-astro.\n// Built from the data the post pages are built from: the ${FEED_ITEMS} newest posts,\n// each at the address its own page has.\nimport type { APIRoute } from 'astro'\nimport data from '../data/${source.collection}.json'\nimport { entryLinks, rssFeed, type EmittedPost } from '../lib/fill'\n\nexport const GET: APIRoute = ({ site }) => {\n const items = entryLinks(data as EmittedPost[], ${JSON.stringify(source.pattern)}, site, ${locale}, ${locale}${input.trailingSlash === false ? ', false, false' : ''}).slice(0, ${FEED_ITEMS})\n const body = rssFeed({\n title: ${JSON.stringify(siteTitle(input.ir))},\n link: new URL('/', site).toString(),\n self: new URL(${JSON.stringify(FEED_PATH)}, site).toString(),\n description: ${JSON.stringify(input.description ?? '')},\n language: ${locale},\n items,\n })\n return new Response(body, { headers: { 'Content-Type': 'application/rss+xml; charset=utf-8' } })\n}\n`\n}\n\n/** `src/pages/llms.txt.ts`: a section of links per collection. */\nexport function llmsEndpoint(sources: LinkSource[], input: FeedInput): string {\n const locale = JSON.stringify(input.siteLocale)\n const imports = sources.map((s, i) => `import c${i} from '../data/${s.collection}.json'`)\n const sections = sources.map((s, i) =>\n ` { name: ${JSON.stringify(sectionName(s.collection))}, links: entryLinks(c${i} as EmittedPost[], ${JSON.stringify(s.pattern)}, site, ${locale}, ${locale}, true${input.trailingSlash === false ? ', false' : ''}).slice(0, ${LLMS_LINKS}) },`)\n return `// /llms.txt (llmstxt.org) — emitted by @contentrain/emitter-astro.\n// What a language model reads to find its way around the site: its name, its\n// tagline, and the newest ${LLMS_LINKS} pages of each kind, at their own addresses —\n// none the source kept out of search (noindex).\nimport type { APIRoute } from 'astro'\n${imports.join('\\n')}\nimport { entryLinks, llmsTxt, type EmittedPost } from '../lib/fill'\n\nexport const GET: APIRoute = ({ site }) => {\n const body = llmsTxt({\n title: ${JSON.stringify(siteTitle(input.ir))},\n description: ${JSON.stringify(input.description ?? '')},\n sections: [\n${sections.join('\\n')}\n ],\n })\n return new Response(body, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } })\n}\n`\n}\n\nconst FEED_LINK_RE = /<link\\b(?=[^>]*\\brel\\s*=\\s*[\"']?[^\"'>]*\\balternate\\b)(?=[^>]*\\btype\\s*=\\s*[\"']?application\\/(?:rss|atom)\\+xml)[^>]*>/gi\nconst HREF_RE = /(\\bhref\\s*=\\s*)([\"'])([^\"']*)\\2/i\n\n/** www. and the bare host are one site: a WordPress head often names the other. */\nconst siteHost = (url: URL) => url.host.toLowerCase().replace(/^www\\./, '')\n\n/**\n * The feed a head link names, when it is one of the site's own: `main` for\n * WordPress's main RSS feed (/feed/, /feed/rss2/, ?feed=rss2), `other` for\n * the rest it serves (comments, a category, Atom). A feed on another host —\n * FeedBurner, a newsletter tool — is not the site's to build or to break.\n */\nfunction ownFeed(href: string, type: string, site: URL): 'main' | 'other' | undefined {\n let url: URL\n try {\n url = new URL(href, site)\n } catch {\n return undefined\n }\n if (siteHost(url) !== siteHost(site)) return undefined\n const path = url.pathname.replace(/\\/+$/, '')\n const query = url.searchParams.get('feed')\n const main = path === '/feed' || path === '/feed/rss2' || path === '/feed/rss' || (path === '' && (query === 'rss2' || query === 'rss'))\n return main && /rss\\+xml/i.test(type) ? 'main' : 'other'\n}\n\nexport interface FeedLinkResult {\n html: string\n /** Links now pointing at FEED_PATH. */\n rewritten: number\n /**\n * The site's other feed links, removed: a category's, a tag's or an\n * author's is the template page's own (each archive page links its feed\n * itself), and a comments or Atom feed is not built.\n */\n other: number\n}\n\n/** Point the head's link to the site's main feed at the one the build writes, and remove the template page's others. */\nexport function rewriteFeedLinks(html: string, siteUrl: string): FeedLinkResult {\n let site: URL\n try {\n site = new URL(siteUrl)\n } catch {\n return { html, rewritten: 0, other: 0 }\n }\n let rewritten = 0\n let other = 0\n const out = html.replace(FEED_LINK_RE, (tag) => {\n const href = HREF_RE.exec(tag)\n const type = /\\btype\\s*=\\s*[\"']?([^\"'\\s>]+)/i.exec(tag)?.[1] ?? ''\n const kind = href ? ownFeed(href[3] ?? '', type, site) : undefined\n if (kind === 'other') {\n other++\n return ''\n }\n if (kind !== 'main') return tag\n rewritten++\n return tag.replace(HREF_RE, `$1$2${FEED_PATH}$2`)\n })\n return { html: out, rewritten, other }\n}\n","// Layout families → Astro layouts.\n//\n// Chrome travels as DATA, not as Astro template source: the compiler must\n// never parse browser-tolerated theme markup. And the body chrome is injected\n// as ONE fragment: real themes nest the content container deep inside the\n// chrome, so splitting it into before/after halves produces unbalanced\n// fragments that the parser silently \"repairs\" (measured cost: 36 vs 100).\n// Content splices in at CHROME_BODY_SLOT before the single injection.\n//\n// Root attributes travel with the chrome for the same reason: themes key their\n// container rules off `<body class=\"wp-singular single …\">` and `<html class=\"js\n// wf-…\">`, so a page with perfect content and empty root attributes loses its\n// whole layout (measured: 36.4 vs 100).\n\nimport type { ComponentDef, LayoutFamily } from '@contentrain/types'\nimport { CHROME_BODY_SLOT, CHROME_COMPONENT_CLOSE, CHROME_COMPONENT_OPEN } from '@contentrain/types'\nimport type { ChromeComponentRef } from './chrome.js'\nimport { balanceWarning } from './balance.js'\nimport { bodySeoLeaks, headLdIdsOf, publisherIdOf, stripSeoTags, websiteIdOf } from './seo.js'\nimport { rewriteFeedLinks } from './feed.js'\nimport { pascalCase, stableJson } from './util.js'\n\nexport interface FamilyGenResult {\n files: Record<string, string>\n warnings: string[]\n}\n\n/** A component the family's chrome mounts at a `<!--@@component:ID@@-->` marker. */\n/** A safe identifier for a query id used as a module-local variable name. */\nfunction queryVar(id: string): string {\n return `q_${id.replace(/[^a-zA-Z0-9]/g, '_')}`\n}\n\nexport interface MountRef {\n id: string\n name: string\n variant: string\n /** QueryBinding id when the placement binds this region to a query. */\n query?: string\n}\n\nconst escapeRe = (s: string) => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\nconst MARKER_RE = new RegExp(`${escapeRe(CHROME_COMPONENT_OPEN)}([^@\\\\s]+)${escapeRe(CHROME_COMPONENT_CLOSE)}`, 'g')\n\n/** Component ids the chrome names, in order of first appearance. */\nexport function componentMarkers(html: string): string[] {\n return [...new Set([...html.matchAll(MARKER_RE)].map((m) => m[1] ?? ''))]\n}\n\nexport interface FamilyOptions {\n /** Emit `<Seo>` and take the template page's SEO tags out of the head chrome. */\n seo?: boolean\n /** `og:site_name`, from `ProjectIR.site.title`. */\n siteName?: string\n /**\n * Query ids a placement may bind to — the ones the caller verified exist in\n * the content. A binding outside this set is ignored rather than emitting an\n * import of a data file that was never supplied.\n */\n boundQueries?: ReadonlySet<string>\n /** Run the page's content through the build-time image pass (src/lib/optimize-images.ts). */\n images?: boolean\n /** false: the site's addresses end without a slash — the Seo component names pages that way. */\n trailingSlash?: boolean\n /** The site URL, when the build writes the site feed: the head's link to the main feed is pointed at it. */\n feedSite?: string\n}\n\nexport function familyFiles(\n family: LayoutFamily,\n lang: string,\n components: ChromeComponentRef[] = [],\n definitions: Map<string, ComponentDef> = new Map(),\n /** Component ids found in the CONTENT bodies rendered through this family (a form inside a page's post_content). */\n bodyMarkers: Iterable<string> = [],\n options: FamilyOptions = {},\n): FamilyGenResult {\n const name = pascalCase(family.id)\n // Header/footer regions are emitted as shared components (see chrome.ts) and\n // rendered as siblings of the body fragment; the layout injects the rest.\n const chunks = (family.chrome ?? []).filter((c) => c.position !== 'header' && c.position !== 'footer')\n const warnings: string[] = []\n // The template page's title, description, canonical, og/twitter tags and\n // per-page JSON-LD are the emitter's to render per entry; two of each is\n // worse than none.\n const seoOn = options.seo !== false\n for (const chunk of family.chrome ?? []) {\n if ((chunk.position === 'header' || chunk.position === 'footer') && componentMarkers(chunk.html).length) {\n warnings.push(`family ${family.id}: ${chunk.position} chrome carries a component marker — components mount in body chrome only; the marker renders as a comment`)\n }\n }\n const joined = (position: string) =>\n chunks.filter((c) => c.position === position).map((c) => c.html).join('\\n')\n\n const bodyChunks = chunks.filter((c) => c.position === 'body')\n let body: string\n if (bodyChunks.length) {\n body = bodyChunks.map((c) => c.html).join('\\n')\n if (!body.includes(CHROME_BODY_SLOT)) {\n warnings.push(`family ${family.id}: body chrome has no ${CHROME_BODY_SLOT} marker — content appended at the end`)\n body += CHROME_BODY_SLOT\n }\n } else {\n // Legacy pair: compose into one string with the slot between the halves.\n body = `${joined('before_body')}${CHROME_BODY_SLOT}${joined('after_body')}`\n }\n if (seoOn) {\n const leaked = bodySeoLeaks(body)\n if (leaked.length) {\n warnings.push(`family ${family.id}: body chrome carries head-only ${leaked.join(', ')} — a page then serves the template's copy alongside the emitter's; lift them into the head chunk or drop them at capture`)\n }\n }\n\n const unbalanced = balanceWarning(body)\n if (unbalanced) {\n warnings.push(`family ${family.id}: body chrome is not balanced (${unbalanced}) — the browser will repair it and the page loses its layout`)\n }\n\n // Component mount points. A marker names a ComponentDef; the layout imports\n // that component and renders it there. A marker nobody defined is dropped\n // with a warning rather than left as a mystery comment in every page.\n // Markers live in the chrome (the theme's comments region) or in the content\n // body itself (a contact form inside a page's post_content); the layout splits\n // the COMPOSED html, so both mount the same way. The mount table just has to\n // know every id that can appear.\n const placements = new Map((family.components ?? []).map((p) => [p.component, p]))\n const mounts: MountRef[] = []\n const mount = (id: string, def: ComponentDef) => {\n if (mounts.some((m) => m.id === id)) return\n const placement = placements.get(id)\n const variant = placement?.variant ?? def.variants?.[0]?.key ?? 'default'\n const query = placement?.query && (options.boundQueries?.has(placement.query) ?? false) ? placement.query : undefined\n mounts.push({ id, name: pascalCase(id), variant, ...(query ? { query } : {}) })\n }\n for (const id of componentMarkers(body)) {\n const def = definitions.get(id)\n if (!def) {\n warnings.push(`family ${family.id}: component marker \"${id}\" has no component definition — marker dropped`)\n body = body.split(`${CHROME_COMPONENT_OPEN}${id}${CHROME_COMPONENT_CLOSE}`).join('')\n continue\n }\n mount(id, def)\n }\n for (const id of new Set(bodyMarkers)) {\n const def = definitions.get(id)\n if (!def) {\n warnings.push(`family ${family.id}: content bodies carry component marker \"${id}\" with no component definition — rendered as a comment`)\n continue\n }\n mount(id, def)\n }\n for (const placement of family.components ?? []) {\n if (!mounts.some((m) => m.id === placement.component)) {\n warnings.push(`family ${family.id}: component placement \"${placement.component}\" has no marker in the body chrome or in any content body — not mounted`)\n }\n }\n\n let head = joined('head')\n let robotsDefault: string[] = []\n if (seoOn) {\n const stripped = stripSeoTags(head)\n head = stripped.html\n robotsDefault = stripped.robots\n if (stripped.removed.length) {\n const kept = stripped.kept.length ? `; kept the site-wide ${stripped.kept.join(', ')} from its structured data` : ''\n warnings.push(`family ${family.id}: removed the template page's ${stripped.removed.join(', ')} from head chrome — these describe one page, and the emitter renders title, description, canonical, social tags and Article data per page${kept}`)\n }\n }\n\n if (options.feedSite) {\n const feed = rewriteFeedLinks(head, options.feedSite)\n head = feed.html\n if (feed.other) {\n warnings.push(`family ${family.id}: removed ${feed.other} feed links from the head chrome — the template page's own archive feed, or a comments or Atom feed the site does not build; each archive page links its own feed`)\n }\n }\n\n const files: Record<string, string> = {}\n files[`src/data/chrome/${family.id}.json`] = stableJson({\n head,\n body,\n html_attrs: family.root_attrs?.html ?? {},\n body_attrs: family.root_attrs?.body ?? {},\n })\n\n const cssLinks = (family.css.files ?? [])\n .map((f) => `<link rel=\"stylesheet\" href=\"/styles/legacy/${f.split('/').pop()}\" />`)\n .join('\\n')\n\n const siteNameProp = options.siteName ? ` siteName={${JSON.stringify(options.siteName)}}` : ''\n const trailingSlashProp = options.trailingSlash === false ? ' trailingSlash={false}' : ''\n const publisherId = seoOn ? publisherIdOf(head) : undefined\n const publisherIdProp = publisherId ? ` publisherId={${JSON.stringify(publisherId)}}` : ''\n const websiteId = seoOn ? websiteIdOf(head) : undefined\n const websiteIdProp = websiteId ? ` websiteId={${JSON.stringify(websiteId)}}` : ''\n const robotsProp = robotsDefault.length ? ` robotsDefault={${JSON.stringify(robotsDefault)}}` : ''\n const headLdIds = seoOn ? headLdIdsOf(head) : []\n const headLdIdsProp = headLdIds.length ? ` headLdIds={${JSON.stringify(headLdIds)}}` : ''\n const imported = [...new Set([...components.map((c) => c.name), ...mounts.map((m) => m.name)])]\n const componentImports = [\n ...(seoOn ? [`import Seo from '../components/Seo.astro'`] : []),\n ...imported.map((n) => `import ${n} from '../components/${n}.astro'`),\n ].join('\\n')\n const mountType = [...new Set(mounts.map((m) => `typeof ${m.name}`))].join(' | ')\n // A placement may bind its region to a query instead of to cloned markup.\n // The LAYOUT resolves that data, not the component: a component file is\n // shared by id across families, while a placement belongs to one family, so\n // two families binding the same component to different queries cannot both\n // be served by one file. Binding here is what lets them.\n const queryMounts = mounts.filter((m) => m.query)\n const queryImports = [...new Set(queryMounts.map((m) => m.query!))]\n .map((q) => `import ${queryVar(q)} from '../data/queries/${q}.json'`)\n .join('\\n')\n const queryRender = queryMounts.length\n ? `${[...new Set(queryMounts.map((m) => m.query!))].map(q => `const ${queryVar(q)}Html = renderQuery(${queryVar(q)} as EmittedQueryPage[], ${JSON.stringify(q)})`).join('\\n')}\\n`\n : ''\n const mountTable = mounts.length\n ? `// Components the chrome mounts at <!--@@component:ID@@--> markers; the page's\n// entry address travels to each so a comments thread knows which entry it is.\n// A mount with \\`html\\` renders a query's results rather than cloned markup.\nconst mounts: Record<string, { Mount: ${mountType}; variant: string; html?: string } | undefined> = {\n${mounts.map((m) => ` ${JSON.stringify(m.id)}: { Mount: ${m.name}, variant: ${JSON.stringify(m.variant)}${m.query ? `, html: ${queryVar(m.query)}Html` : ''} },`).join('\\n')}\n}\nconst parts = splitComponents(html).map((part) => ({\n html: part.html,\n Mount: part.component ? mounts[part.component]?.Mount : undefined,\n variant: part.component ? (mounts[part.component]?.variant ?? 'default') : 'default',\n listHtml: part.component ? mounts[part.component]?.html : undefined,\n}))\n`\n : ''\n const bodyRender = mounts.length\n ? ` {parts.map(({ html: part, Mount, variant, listHtml }) => (Mount ? <Mount entry={entry} variant={variant} html={listHtml} lang={htmlAttrs.lang} /> : <Fragment set:html={part} />))}`\n : ` <Fragment set:html={html} />`\n const renderRefs = (position: 'header' | 'footer') =>\n components\n .filter((c) => c.position === position)\n .map((c) => ` <${c.name} marks={marks} />`)\n .join('\\n')\n\n files[`src/layouts/${name}.astro`] = `---\n// Family: ${family.id}${family.name ? ` (${family.name})` : ''} — emitted by @contentrain/emitter-astro\nimport chrome from '../data/chrome/${family.id}.json'\nimport { cssHref, fillAttrs, renderTemplate, composeBody${mounts.length ? ', splitComponents' : ''}${queryMounts.length ? ', renderQuery, type EmittedQueryPage' : ''}${seoOn ? ', type SeoInput' : ''} } from '../lib/fill'\n${options.images ? `import { optimizeHtmlImages } from '../lib/optimize-images'\\n` : ''}${queryImports}${queryImports ? '\\n' : ''}\n${componentImports ? `${componentImports}\\n` : ''}\ninterface Props {\n title?: string\n marks?: Record<string, unknown>\n /** Page content as an HTML string; slot children are used when absent. */\n body?: string\n /** Stylesheets only this page loads (page-builder sites emit CSS per page). */\n css?: string[]\n /** Document language — a multilingual site's routes each pass their own. */\n lang?: string\n /** Content-store address of the page's entry — mounted components (comments) key on it. */\n entry?: { model_id: string; entry_id: string; locale?: string }${seoOn ? `\n /** Per-page SEO — see src/components/Seo.astro. */\n seo?: SeoInput` : ''}\n}\nconst { title = '', marks = {}, body, css = [], lang = ${JSON.stringify(lang)}${mounts.length ? ', entry' : ''}${seoOn ? ', seo' : ''} } = Astro.props\nconst head = renderTemplate(chrome.head, marks)\n// Root attributes carry the theme's layout hooks; values may hold @@marks@@.\n// An explicit lang from the source wins over the project default.\nconst htmlAttrs = { lang, ...fillAttrs(chrome.html_attrs, marks) }\nconst bodyAttrs = fillAttrs(chrome.body_attrs, marks)\n${options.images\n ? `// Content images get srcset, WebP, size and lazy loading at build time; the\n// theme chrome around them is left as the source had it.\nconst content = await optimizeHtmlImages(body ?? (Astro.slots.has('default') ? await Astro.slots.render('default') : ''))`\n : `const content = body ?? (Astro.slots.has('default') ? await Astro.slots.render('default') : '')`}\n// Split at the marker FIRST, then fill marks per side — filling first would\n// eat the @@body@@ inside the marker and silently drop the content.\nconst html = composeBody(chrome.body, marks, content)\n${queryRender}${mountTable}---\n<!doctype html>\n<html {...htmlAttrs}>\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n${cssLinks\n .split('\\n')\n .filter(Boolean)\n .map((l) => ` ${l}`)\n .join('\\n')}\n {css.map((file) => (\n <link rel=\"stylesheet\" href={cssHref(file)} />\n ))}\n <Fragment set:html={head} />\n${seoOn\n ? ` <Seo title={title} locale={lang}${siteNameProp}${websiteIdProp}${publisherIdProp}${trailingSlashProp}${robotsProp}${headLdIdsProp} {...(seo ?? {})} />`\n : ` <title>{title}</title>`}\n </head>\n <body {...bodyAttrs}>\n${renderRefs('header') ? `${renderRefs('header')}\\n` : ''}${bodyRender}\n${renderRefs('footer') ? `${renderRefs('footer')}\\n` : ''} </body>\n</html>\n`\n return { files, warnings }\n}\n","// The runtime-component client, emitted as `src/lib/embed.ts`.\n//\n// Kept as source text (like the fill runtime in scaffold.ts) so the generated\n// site depends on nothing but Astro. The text is type-checked and executed by\n// this package's tests — `embed.test.ts` writes it to disk and imports it, and\n// `typecheck.test.ts` runs tsc over it with Astro's strict settings — so the\n// code that ships is the code that was tested, escaping included.\n\nexport const EMBED_TS = `// Emitted by @contentrain/emitter-astro — the runtime-component client.\n//\n// Comments and forms are the two regions of a migrated site that need a live\n// service. This module is the browser side of the provider's PUBLIC API\n// (Studio: /api/forms/v1 and /api/comments/v1) — the same contract that\n// @contentrain/query/cdn implements, inlined so the generated site depends on\n// nothing but Astro. No credential travels with a request: the endpoints are\n// unauthenticated by design, and a page cannot keep a secret.\n//\n// Rendering helpers are pure (strings in, HTML out, escaped by default) so the\n// mount functions at the bottom are the only code that touches the DOM.\n\nexport interface Runtime {\n base_url: string\n project_id: string\n}\n\nexport interface EntryRef {\n model_id: string\n entry_id: string\n locale?: string\n}\n\nexport class EmbedError extends Error {\n status: number\n /** The API's machine code (\\`data.code\\`), when the body carries one — e.g. \\`payment_required\\`. */\n code?: string\n constructor(status: number, message: string, code?: string) {\n super(message)\n this.name = 'EmbedError'\n this.status = status\n if (code) this.code = code\n }\n}\n\n/**\n * The workspace's subscription is inactive (Studio answers 402\n * \\`payment_required\\` on the public forms and comments endpoints). Not the\n * visitor's problem and not transient: no retry helps until the owner updates\n * billing.\n */\nexport function isPaymentRequired(error: unknown): boolean {\n return error instanceof EmbedError && (error.status === 402 || error.code === 'payment_required')\n}\n\n/**\n * Take a widget off the page without a word to the visitor — the message is\n * written for the workspace owner, who finds it in the console. The rest of\n * the page is untouched.\n */\nexport function hideUnavailable(host: HTMLElement, what: 'form' | 'comments'): void {\n host.innerHTML = ''\n host.hidden = true\n // A site stylesheet that sets display on the element would override hidden.\n host.style.display = 'none'\n console.debug('[contentrain] ' + what + ' hidden: the workspace subscription is inactive (402 payment_required). The workspace owner needs to update billing in Contentrain Studio.')\n}\n\n/**\n * Visitor-facing text. The component passes the page's text from the site's\n * ui-strings dictionary (\\`data-strings\\`); these are the defaults it falls\n * back to, key for key.\n */\nexport const strings = {\n loading: 'Loading…',\n send: 'Send',\n sending: 'Sending…',\n commentsTitle: 'Comments',\n noComments: 'No comments yet.',\n closed: 'Comments are closed.',\n postComment: 'Post comment',\n reply: 'Reply',\n replyingTo: 'Replying to',\n cancel: 'Cancel',\n loadMore: 'Load more comments',\n moderator: 'Moderator',\n pending: 'Thank you — your comment is awaiting moderation.',\n posted: 'Your comment has been posted.',\n name: 'Name',\n email: 'Email',\n emailNote: '(never shown)',\n website: 'Website',\n comment: 'Comment',\n failed: 'Something went wrong. Please try again.',\n honeypot: 'Leave this field empty',\n}\n\nexport const esc = (value: unknown): string =>\n String(value ?? '')\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n\n// ─── Transport ───\n\n/** \\`base\\` without a trailing slash, segments URL-encoded, empty query values dropped. */\nexport function publicUrl(base: string, segments: string[], query?: Record<string, string | number | undefined>): string {\n const path = segments.map((s) => encodeURIComponent(s)).join('/')\n const params = new URLSearchParams()\n for (const [key, value] of Object.entries(query ?? {})) {\n if (value !== undefined && value !== '') params.set(key, String(value))\n }\n const qs = params.toString()\n return base.replace(/\\\\/+$/, '') + '/' + path + (qs ? '?' + qs : '')\n}\n\nasync function failure(res: Response): Promise<EmbedError> {\n const text = await res.text().catch(() => '')\n let message = text || 'Request failed'\n let code: string | undefined\n try {\n const parsed = JSON.parse(text) as { message?: unknown; statusMessage?: unknown; data?: { code?: unknown } }\n const m = parsed.message ?? parsed.statusMessage\n if (typeof m === 'string' && m) message = m\n if (typeof parsed.data?.code === 'string') code = parsed.data.code\n } catch {\n // not JSON — the raw text is the message\n }\n return new EmbedError(res.status, message, code)\n}\n\nexport async function getJson<T>(url: string): Promise<T> {\n const res = await fetch(url)\n if (!res.ok) throw await failure(res)\n return (await res.json()) as T\n}\n\nexport async function postJson<T>(url: string, body: unknown): Promise<T> {\n const res = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n })\n if (!res.ok) throw await failure(res)\n return (await res.json()) as T\n}\n\nexport const formsRoot = (rt: Runtime): string => rt.base_url.replace(/\\\\/+$/, '') + '/api/forms/v1'\nexport const commentsRoot = (rt: Runtime): string => rt.base_url.replace(/\\\\/+$/, '') + '/api/comments/v1'\n\n/** The hidden input Cloudflare Turnstile adds to the enclosing form. */\nexport const CAPTCHA_FIELD = 'cf-turnstile-response'\n\n// ─── Forms ───\n\nexport interface FieldDef {\n type: string\n required?: boolean\n label?: string\n options?: string[]\n min?: number\n max?: number\n pattern?: string\n default?: unknown\n}\n\nexport interface FormConfig {\n modelId: string\n locale: string\n /** Exposed fields only, keyed by field id. */\n fields: Record<string, FieldDef>\n captcha: 'turnstile' | null\n captchaSiteKey: string | null\n successMessage?: string\n honeypotField: string | null\n}\n\nexport interface FieldError {\n field: string\n message: string\n}\n\nexport interface FormSubmitResult {\n success: boolean\n message?: string\n errors?: FieldError[]\n}\n\n/** The documented request body: values under \\`data\\`, control fields beside it. */\nexport interface FormPayload {\n data: Record<string, unknown>\n captchaToken?: string\n _hp?: string\n}\n\nexport function fetchFormConfig(rt: Runtime, model: string): Promise<FormConfig> {\n return getJson<FormConfig>(publicUrl(formsRoot(rt), [rt.project_id, model, 'config']))\n}\n\nexport function submitForm(rt: Runtime, model: string, payload: FormPayload): Promise<FormSubmitResult> {\n return postJson<FormSubmitResult>(publicUrl(formsRoot(rt), [rt.project_id, model, 'submit']), payload)\n}\n\nfunction coerce(def: FieldDef, value: string): unknown {\n switch (def.type) {\n case 'number':\n case 'integer':\n case 'decimal':\n case 'percent':\n case 'rating':\n return value === '' ? '' : Number(value)\n case 'boolean':\n return value === 'on' || value === 'true'\n default:\n return value\n }\n}\n\n/**\n * Form entries (\\`new FormData(form)\\`) → request body. Only exposed fields\n * reach \\`data\\`; the captcha token and the honeypot value travel beside it, so\n * a control field can never collide with a model field. Files are skipped —\n * uploads are not part of the public contract.\n */\nexport function formPayload(\n entries: Iterable<[string, unknown]>,\n config: { fields: Record<string, FieldDef>; honeypotField: string | null },\n): FormPayload {\n const payload: FormPayload = { data: {} }\n for (const [name, raw] of entries) {\n if (typeof raw !== 'string') continue\n if (name === CAPTCHA_FIELD) {\n if (raw) payload.captchaToken = raw\n continue\n }\n if (config.honeypotField && name === config.honeypotField) {\n payload._hp = raw\n continue\n }\n const def = config.fields[name]\n if (!def) continue\n payload.data[name] = coerce(def, raw)\n }\n return payload\n}\n\nexport function labelFor(id: string, def: FieldDef): string {\n if (def.label) return def.label\n const words = id.replace(/[_-]+/g, ' ').trim()\n return words.charAt(0).toUpperCase() + words.slice(1)\n}\n\n/** One input for one exposed field, chosen by field type. */\nexport function fieldControl(id: string, def: FieldDef, prefix = 'cr-field'): string {\n const common =\n ' id=\"' + esc(prefix + '-' + id) + '\" name=\"' + esc(id) + '\"' +\n (def.required ? ' required' : '') +\n (def.pattern ? ' pattern=\"' + esc(def.pattern) + '\"' : '')\n const range =\n (def.min !== undefined ? ' min=\"' + esc(def.min) + '\"' : '') +\n (def.max !== undefined ? ' max=\"' + esc(def.max) + '\"' : '')\n const length = def.max !== undefined ? ' maxlength=\"' + esc(def.max) + '\"' : ''\n switch (def.type) {\n case 'text':\n case 'markdown':\n case 'richtext':\n case 'code':\n return '<textarea' + common + length + ' rows=\"5\"></textarea>'\n case 'select': {\n const blank = def.required ? '' : '<option value=\"\"></option>'\n const options = (def.options ?? []).map((o) => '<option value=\"' + esc(o) + '\">' + esc(o) + '</option>').join('')\n return '<select' + common + '>' + blank + options + '</select>'\n }\n case 'boolean':\n return '<input type=\"checkbox\"' + common + ' />'\n case 'number':\n case 'integer':\n case 'rating':\n return '<input type=\"number\"' + common + range + (def.type === 'integer' ? ' step=\"1\"' : '') + ' />'\n case 'decimal':\n case 'percent':\n return '<input type=\"number\" step=\"any\"' + common + range + ' />'\n case 'email':\n return '<input type=\"email\"' + common + length + ' />'\n case 'url':\n return '<input type=\"url\"' + common + length + ' />'\n case 'phone':\n return '<input type=\"tel\"' + common + length + ' />'\n case 'date':\n return '<input type=\"date\"' + common + ' />'\n case 'datetime':\n return '<input type=\"datetime-local\"' + common + ' />'\n case 'color':\n return '<input type=\"color\"' + common + ' />'\n default:\n return '<input type=\"text\"' + common + length + ' />'\n }\n}\n\nexport function honeypotHtml(field: string | null): string {\n if (!field) return ''\n return (\n '<p class=\"cr-hp\" aria-hidden=\"true\" style=\"position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden\">' +\n '<label>' + esc(strings.honeypot) + ' <input type=\"text\" name=\"' + esc(field) + '\" tabindex=\"-1\" autocomplete=\"off\" /></label></p>'\n )\n}\n\nexport function captchaHtml(captcha: 'turnstile' | null, siteKey: string | null): string {\n if (captcha !== 'turnstile' || !siteKey) return ''\n return '<div class=\"cf-turnstile\" data-sitekey=\"' + esc(siteKey) + '\"></div>'\n}\n\nexport function errorsHtml(errors: FieldError[]): string {\n return (\n '<ul class=\"cr-errors\" role=\"alert\">' +\n errors.map((e) => '<li data-field=\"' + esc(e.field) + '\">' + esc(e.field) + ': ' + esc(e.message) + '</li>').join('') +\n '</ul>'\n )\n}\n\n/** The whole form for a public form config: one control per exposed field, honeypot, captcha, submit. */\nexport function formHtml(config: FormConfig, prefix = 'cr-field'): string {\n const fields = Object.entries(config.fields)\n .map(([id, def]) => {\n const control = fieldControl(id, def, prefix)\n const label = '<label for=\"' + esc(prefix + '-' + id) + '\">' + esc(labelFor(id, def)) + (def.required ? ' <span aria-hidden=\"true\">*</span>' : '') + '</label>'\n return '<p class=\"cr-field cr-field--' + esc(def.type) + '\">' + (def.type === 'boolean' ? control + ' ' + label : label + control) + '</p>'\n })\n .join('')\n return (\n '<form class=\"cr-form\" method=\"post\" data-model=\"' + esc(config.modelId) + '\">' +\n fields +\n honeypotHtml(config.honeypotField) +\n captchaHtml(config.captcha, config.captchaSiteKey) +\n '<p class=\"cr-actions\"><button type=\"submit\">' + esc(strings.send) + '</button></p>' +\n '<div class=\"cr-status\" aria-live=\"polite\"></div>' +\n '</form>'\n )\n}\n\n// ─── Comments ───\n\nexport interface CommentAuthor {\n name: string\n url: string | null\n isModerator: boolean\n}\n\nexport interface PublicComment {\n id: string\n parentId: string | null\n depth: number\n author: CommentAuthor\n /** Plain text. */\n body: string\n type: 'comment' | 'pingback' | 'trackback'\n createdAt: string\n replies: PublicComment[]\n}\n\nexport interface ThreadConfig {\n closed: boolean\n requireApproval: boolean\n requireEmail: boolean\n maxDepth: number\n maxBodyLength: number\n captcha: 'turnstile' | null\n captchaSiteKey: string | null\n honeypotField: string | null\n}\n\nexport interface CommentThread {\n entry: { modelId: string; entryId: string; locale: string }\n config: ThreadConfig\n comments: PublicComment[]\n total: number\n page: number\n limit: number\n}\n\nexport interface CommentSubmitBody {\n author: { name: string; email?: string; url?: string }\n body: string\n /** null for a root comment — sent explicitly, as the provider's own fixture does. */\n parentId: string | null\n captchaToken?: string\n _hp?: string\n}\n\nexport interface CommentSubmitResult {\n success: boolean\n status?: 'pending' | 'approved'\n comment?: PublicComment\n errors?: FieldError[]\n}\n\nexport interface ThreadQuery {\n page?: number\n limit?: number\n sort?: 'oldest' | 'newest'\n}\n\nexport function fetchThread(rt: Runtime, entry: EntryRef, query?: ThreadQuery): Promise<CommentThread> {\n const url = publicUrl(commentsRoot(rt), [rt.project_id, entry.model_id, entry.entry_id], {\n locale: entry.locale,\n page: query?.page,\n limit: query?.limit,\n sort: query?.sort,\n })\n return getJson<CommentThread>(url)\n}\n\nexport function submitComment(rt: Runtime, entry: EntryRef, body: CommentSubmitBody): Promise<CommentSubmitResult> {\n const url = publicUrl(commentsRoot(rt), [rt.project_id, entry.model_id, entry.entry_id], { locale: entry.locale })\n return postJson<CommentSubmitResult>(url, body)\n}\n\n/** Comment-form entries → the documented request body (inputs: author_name, author_email, author_url, body, parent_id). */\nexport function commentPayload(entries: Iterable<[string, unknown]>, honeypotField: string | null): CommentSubmitBody {\n const values: Record<string, string> = {}\n const payload: CommentSubmitBody = { author: { name: '' }, body: '', parentId: null }\n for (const [name, raw] of entries) {\n if (typeof raw !== 'string') continue\n if (name === CAPTCHA_FIELD) {\n if (raw) payload.captchaToken = raw\n continue\n }\n if (honeypotField && name === honeypotField) {\n payload._hp = raw\n continue\n }\n values[name] = raw\n }\n payload.author.name = (values.author_name ?? '').trim()\n if (values.author_email?.trim()) payload.author.email = values.author_email.trim()\n if (values.author_url?.trim()) payload.author.url = values.author_url.trim()\n payload.body = (values.body ?? '').trim()\n payload.parentId = values.parent_id?.trim() || null\n return payload\n}\n\n/** Plain text → paragraphs; blank lines separate paragraphs, single newlines break lines. */\nexport function bodyHtml(text: string): string {\n return text\n .split(/\\\\n{2,}/)\n .map((p) => p.trim())\n .filter(Boolean)\n .map((p) => '<p>' + esc(p).replace(/\\\\n/g, '<br />') + '</p>')\n .join('')\n}\n\nexport function hasMore(thread: CommentThread): boolean {\n return thread.page * thread.limit < thread.total\n}\n\n/** A word's first character (a whole code point, so an emoji or an accented letter stays intact). */\nfunction firstChar(word: string | undefined): string {\n return word ? Array.from(word)[0] ?? '' : ''\n}\n\n/**\n * Up to two initials of a name, for the comment's monogram. The provider sends\n * no avatar (no email hash leaves it), so a letter mark stands in for one.\n */\nexport function initials(name: string): string {\n const words = name.trim().split(/\\\\s+/).filter(Boolean)\n return (firstChar(words[0]) + (words.length > 1 ? firstChar(words[words.length - 1]) : '')).toLocaleUpperCase()\n}\n\n/** One comment with its replies. A reply button appears only while the thread is open and depth allows it. */\nexport function commentHtml(c: PublicComment, config: { closed: boolean; maxDepth: number }): string {\n const author = c.author.url\n ? '<a href=\"' + esc(c.author.url) + '\" rel=\"nofollow ugc noopener\" target=\"_blank\">' + esc(c.author.name) + '</a>'\n : esc(c.author.name)\n const badge = c.author.isModerator ? ' <span class=\"cr-moderator\">' + esc(strings.moderator) + '</span>' : ''\n const date = '<time datetime=\"' + esc(c.createdAt) + '\">' + esc(c.createdAt.slice(0, 10)) + '</time>'\n const canReply = !config.closed && c.depth < config.maxDepth\n const reply = canReply\n ? '<button type=\"button\" class=\"cr-reply\" data-parent=\"' + esc(c.id) + '\" data-author=\"' + esc(c.author.name) + '\">' + esc(strings.reply) + '</button>'\n : ''\n const replies = c.replies.length\n ? '<ol class=\"cr-replies\">' + c.replies.map((r) => commentHtml(r, config)).join('') + '</ol>'\n : ''\n return (\n '<li class=\"cr-comment cr-comment--' + esc(c.type) + '\" id=\"cr-comment-' + esc(c.id) + '\" data-depth=\"' + esc(c.depth) + '\">' +\n '<div class=\"cr-comment-meta\"><span class=\"cr-avatar\" aria-hidden=\"true\">' + esc(initials(c.author.name)) + '</span>' +\n '<span class=\"cr-comment-author\">' + author + '</span>' + badge + ' ' + date + '</div>' +\n '<div class=\"cr-comment-body\">' + bodyHtml(c.body) + '</div>' +\n (reply ? '<div class=\"cr-comment-actions\">' + reply + '</div>' : '') +\n replies +\n '</li>'\n )\n}\n\nexport function threadHtml(thread: CommentThread): string {\n if (!thread.comments.length) return '<p class=\"cr-empty\">' + esc(strings.noComments) + '</p>'\n return '<ol class=\"cr-comment-list\">' + thread.comments.map((c) => commentHtml(c, thread.config)).join('') + '</ol>'\n}\n\n/** The comment form; a reply carries its parent in a hidden input. */\nexport function commentFormHtml(config: ThreadConfig, prefix = 'cr-c'): string {\n const emailLabel = esc(strings.email) + (config.requireEmail ? ' <span aria-hidden=\"true\">*</span>' : '') + ' <small>' + esc(strings.emailNote) + '</small>'\n return (\n '<form class=\"cr-comment-form\" method=\"post\">' +\n '<input type=\"hidden\" name=\"parent_id\" value=\"\" />' +\n '<p class=\"cr-replying\" hidden><span class=\"cr-replying-to\"></span> <button type=\"button\" class=\"cr-cancel-reply\">' + esc(strings.cancel) + '</button></p>' +\n '<p class=\"cr-field\"><label for=\"' + prefix + '-body\">' + esc(strings.comment) + ' <span aria-hidden=\"true\">*</span></label>' +\n '<textarea id=\"' + prefix + '-body\" name=\"body\" required maxlength=\"' + esc(config.maxBodyLength) + '\" rows=\"5\"></textarea></p>' +\n '<p class=\"cr-field\"><label for=\"' + prefix + '-name\">' + esc(strings.name) + ' <span aria-hidden=\"true\">*</span></label>' +\n '<input type=\"text\" id=\"' + prefix + '-name\" name=\"author_name\" required maxlength=\"120\" autocomplete=\"name\" /></p>' +\n '<p class=\"cr-field\"><label for=\"' + prefix + '-email\">' + emailLabel + '</label>' +\n '<input type=\"email\" id=\"' + prefix + '-email\" name=\"author_email\"' + (config.requireEmail ? ' required' : '') + ' maxlength=\"254\" autocomplete=\"email\" /></p>' +\n '<p class=\"cr-field\"><label for=\"' + prefix + '-url\">' + esc(strings.website) + '</label>' +\n '<input type=\"url\" id=\"' + prefix + '-url\" name=\"author_url\" maxlength=\"2048\" autocomplete=\"url\" /></p>' +\n honeypotHtml(config.honeypotField) +\n captchaHtml(config.captcha, config.captchaSiteKey) +\n '<p class=\"cr-actions\"><button type=\"submit\">' + esc(strings.postComment) + '</button></p>' +\n '<div class=\"cr-status\" aria-live=\"polite\"></div>' +\n '</form>'\n )\n}\n\n// ─── Turnstile (explicit render, so widgets added after load still render) ───\n\ninterface TurnstileApi {\n render: (el: Element, options: { sitekey: string }) => string\n reset: (id?: string) => void\n}\n\nconst TURNSTILE_SRC = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=__crTurnstileReady'\nconst widgets = new WeakMap<Element, string>()\nconst waiting: Element[] = []\n\nfunction turnstileApi(): TurnstileApi | undefined {\n return (globalThis as { turnstile?: TurnstileApi }).turnstile\n}\n\nfunction renderWidget(api: TurnstileApi, el: Element): void {\n if (widgets.has(el)) return\n widgets.set(el, api.render(el, { sitekey: el.getAttribute('data-sitekey') ?? '' }))\n}\n\n/** Render every \\`.cf-turnstile\\` under \\`root\\`, loading the Turnstile script once on first use. */\nexport function renderCaptcha(root: ParentNode): void {\n const els = Array.from(root.querySelectorAll('.cf-turnstile'))\n if (!els.length) return\n const api = turnstileApi()\n if (api) {\n for (const el of els) renderWidget(api, el)\n return\n }\n waiting.push(...els)\n if (document.querySelector('script[data-cr-turnstile]')) return\n ;(globalThis as Record<string, unknown>).__crTurnstileReady = () => {\n const ready = turnstileApi()\n if (!ready) return\n for (const el of waiting.splice(0)) renderWidget(ready, el)\n }\n const script = document.createElement('script')\n script.src = TURNSTILE_SRC\n script.async = true\n script.defer = true\n script.setAttribute('data-cr-turnstile', '1')\n document.head.appendChild(script)\n}\n\nexport function resetCaptcha(root: ParentNode): void {\n const api = turnstileApi()\n if (!api) return\n for (const el of Array.from(root.querySelectorAll('.cf-turnstile'))) api.reset(widgets.get(el))\n}\n\n// ─── Mounting (the only DOM code) ───\n\n/** ui-strings dictionary key → the \\`strings\\` entry it sets. */\nexport const STRING_KEYS: ReadonlyArray<readonly [string, keyof typeof strings]> = [\n ['common.loading', 'loading'],\n ['common.failed', 'failed'],\n ['form.send', 'send'],\n ['form.sending', 'sending'],\n ['form.honeypot', 'honeypot'],\n ['comments.title', 'commentsTitle'],\n ['comments.empty', 'noComments'],\n ['comments.closed', 'closed'],\n ['comments.post', 'postComment'],\n ['comments.reply', 'reply'],\n ['comments.replying_to', 'replyingTo'],\n ['comments.cancel', 'cancel'],\n ['comments.load_more', 'loadMore'],\n ['comments.moderator', 'moderator'],\n ['comments.pending', 'pending'],\n ['comments.posted', 'posted'],\n ['comments.field.name', 'name'],\n ['comments.field.email', 'email'],\n ['comments.field.email_note', 'emailNote'],\n ['comments.field.website', 'website'],\n ['comments.field.comment', 'comment'],\n]\n\n/**\n * Take the page's interface text from the host element. A value that is not a\n * non-empty string leaves the default; text that cannot be parsed changes\n * nothing, because a mount that fails over a label is worse than an English one.\n */\nexport function applyStrings(host: HTMLElement): void {\n const raw = host.dataset.strings\n if (!raw) return\n let text: unknown\n try {\n text = JSON.parse(raw)\n } catch {\n return\n }\n if (!text || typeof text !== 'object') return\n for (const [key, name] of STRING_KEYS) {\n const value = (text as Record<string, unknown>)[key]\n if (typeof value === 'string' && value !== '') strings[name] = value\n }\n}\n\nexport function runtimeOf(host: HTMLElement): Runtime {\n return { base_url: host.dataset.baseUrl ?? '', project_id: host.dataset.project ?? '' }\n}\n\nfunction entryOf(host: HTMLElement): EntryRef {\n const locale = host.dataset.locale\n return { model_id: host.dataset.model ?? '', entry_id: host.dataset.entry ?? '', ...(locale ? { locale } : {}) }\n}\n\nfunction setStatus(form: HTMLFormElement, html: string): void {\n const status = form.querySelector('.cr-status')\n if (status) status.innerHTML = html\n}\n\nfunction busy(form: HTMLFormElement, on: boolean): void {\n const button = form.querySelector<HTMLButtonElement>('button[type=\"submit\"]')\n if (!button) return\n button.disabled = on\n if (on) {\n button.dataset.label = button.textContent ?? ''\n button.textContent = strings.sending\n } else {\n button.textContent = button.dataset.label ?? button.textContent\n }\n}\n\nfunction entriesOf(form: HTMLFormElement): Array<[string, unknown]> {\n const out: Array<[string, unknown]> = []\n new FormData(form).forEach((value, key) => out.push([key, value]))\n return out\n}\n\n/** \\`<cr-form data-base-url data-project data-model>\\` → fetch the config, render, submit. */\nexport async function mountForm(host: HTMLElement): Promise<void> {\n applyStrings(host)\n const rt = runtimeOf(host)\n const model = host.dataset.model ?? ''\n // The page's own copy of the form (its fields as HTML) stays in view while Studio's form loads; without one, a\n // loading line holds the place.\n const drawn = host.querySelector('form')\n if (!drawn) host.innerHTML = '<p class=\"cr-loading\">' + esc(strings.loading) + '</p>'\n let config: FormConfig\n try {\n config = await fetchFormConfig(rt, model)\n } catch (error) {\n if (isPaymentRequired(error)) return hideUnavailable(host, 'form')\n const message = '<p class=\"cr-error\" role=\"alert\">' + esc(error instanceof Error ? error.message : strings.failed) + '</p>'\n if (drawn) drawn.insertAdjacentHTML('beforeend', message)\n else host.innerHTML = message\n return\n }\n host.innerHTML = formHtml(config)\n renderCaptcha(host)\n const form = host.querySelector('form')\n if (!form) return\n form.addEventListener('submit', async (event) => {\n event.preventDefault()\n busy(form, true)\n setStatus(form, '')\n try {\n const result = await submitForm(rt, model, formPayload(entriesOf(form), config))\n if (result.success) {\n host.innerHTML = '<p class=\"cr-success\" role=\"status\">' + esc(result.message ?? config.successMessage ?? '') + '</p>'\n return\n }\n setStatus(form, errorsHtml(result.errors ?? []))\n resetCaptcha(form)\n } catch (error) {\n if (isPaymentRequired(error)) return hideUnavailable(host, 'form')\n setStatus(form, '<p class=\"cr-error\" role=\"alert\">' + esc(error instanceof Error ? error.message : strings.failed) + '</p>')\n resetCaptcha(form)\n } finally {\n busy(form, false)\n }\n })\n}\n\n/** \\`<cr-comments data-base-url data-project data-model data-entry data-locale>\\` → thread + form. */\nexport async function mountComments(host: HTMLElement): Promise<void> {\n applyStrings(host)\n const rt = runtimeOf(host)\n const entry = entryOf(host)\n host.innerHTML = '<p class=\"cr-loading\">' + esc(strings.loading) + '</p>'\n let thread: CommentThread\n try {\n thread = await fetchThread(rt, entry)\n } catch (error) {\n if (isPaymentRequired(error)) return hideUnavailable(host, 'comments')\n host.innerHTML = '<p class=\"cr-error\" role=\"alert\">' + esc(error instanceof Error ? error.message : strings.failed) + '</p>'\n return\n }\n\n host.innerHTML =\n '<section class=\"cr-comments-section\">' +\n '<h2 class=\"cr-comments-title\">' + esc(strings.commentsTitle) + ' <span class=\"cr-count\">(' + esc(thread.total) + ')</span></h2>' +\n '<div class=\"cr-thread\">' + threadHtml(thread) + '</div>' +\n (hasMore(thread) ? '<p class=\"cr-more\"><button type=\"button\" class=\"cr-load-more\">' + esc(strings.loadMore) + '</button></p>' : '') +\n (thread.config.closed ? '<p class=\"cr-closed\">' + esc(strings.closed) + '</p>' : commentFormHtml(thread.config)) +\n '</section>'\n renderCaptcha(host)\n\n const threadEl = host.querySelector('.cr-thread')\n const form = host.querySelector<HTMLFormElement>('form.cr-comment-form')\n let page = thread.page\n\n host.addEventListener('click', async (event) => {\n const target = event.target as HTMLElement | null\n if (!target) return\n const replyButton = target.closest<HTMLElement>('.cr-reply')\n if (replyButton && form) {\n const parent = form.querySelector<HTMLInputElement>('input[name=\"parent_id\"]')\n if (parent) parent.value = replyButton.dataset.parent ?? ''\n const replying = form.querySelector<HTMLElement>('.cr-replying')\n const to = form.querySelector('.cr-replying-to')\n if (to) to.textContent = strings.replyingTo + ' ' + (replyButton.dataset.author ?? '')\n if (replying) replying.hidden = false\n replyButton.closest('.cr-comment')?.appendChild(form)\n form.querySelector<HTMLTextAreaElement>('textarea[name=\"body\"]')?.focus()\n return\n }\n if (target.closest('.cr-cancel-reply') && form) {\n const parent = form.querySelector<HTMLInputElement>('input[name=\"parent_id\"]')\n if (parent) parent.value = ''\n const replying = form.querySelector<HTMLElement>('.cr-replying')\n if (replying) replying.hidden = true\n host.querySelector('.cr-comments-section')?.appendChild(form)\n return\n }\n const more = target.closest<HTMLButtonElement>('.cr-load-more')\n if (more && threadEl) {\n more.disabled = true\n try {\n const next = await fetchThread(rt, entry, { page: page + 1 })\n page = next.page\n const list = threadEl.querySelector('.cr-comment-list')\n if (list) list.insertAdjacentHTML('beforeend', next.comments.map((c) => commentHtml(c, next.config)).join(''))\n if (!hasMore(next)) more.closest('.cr-more')?.remove()\n } catch (error) {\n if (isPaymentRequired(error)) return hideUnavailable(host, 'comments')\n // Anything else: the button comes back for another try.\n } finally {\n more.disabled = false\n }\n }\n })\n\n form?.addEventListener('submit', async (event) => {\n event.preventDefault()\n busy(form, true)\n setStatus(form, '')\n try {\n const result = await submitComment(rt, entry, commentPayload(entriesOf(form), thread.config.honeypotField))\n if (!result.success) {\n setStatus(form, errorsHtml(result.errors ?? []))\n resetCaptcha(form)\n return\n }\n const parentId = result.comment?.parentId ?? null\n if (result.status === 'approved' && result.comment && threadEl) {\n const html = commentHtml(result.comment, thread.config)\n const parent = parentId ? threadEl.querySelector('#cr-comment-' + parentId) : null\n if (parent) {\n let replies = parent.querySelector(':scope > .cr-replies')\n if (!replies) {\n parent.insertAdjacentHTML('beforeend', '<ol class=\"cr-replies\"></ol>')\n replies = parent.querySelector(':scope > .cr-replies')\n }\n replies?.insertAdjacentHTML('beforeend', html)\n } else {\n const list = threadEl.querySelector('.cr-comment-list')\n if (list) list.insertAdjacentHTML('beforeend', html)\n else threadEl.innerHTML = '<ol class=\"cr-comment-list\">' + html + '</ol>'\n }\n }\n form.reset()\n host.querySelector('.cr-comments-section')?.appendChild(form)\n const replying = form.querySelector<HTMLElement>('.cr-replying')\n if (replying) replying.hidden = true\n resetCaptcha(form)\n setStatus(form, '<p class=\"cr-success\" role=\"status\">' + esc(result.status === 'approved' ? strings.posted : strings.pending) + '</p>')\n } catch (error) {\n if (isPaymentRequired(error)) return hideUnavailable(host, 'comments')\n setStatus(form, '<p class=\"cr-error\" role=\"alert\">' + esc(error instanceof Error ? error.message : strings.failed) + '</p>')\n resetCaptcha(form)\n } finally {\n busy(form, false)\n }\n })\n}\n`\n","// The generated site's interface text — what its comments and forms say to a\n// visitor — lives in the site's content store, not in its code.\n//\n// A migrated Turkish site whose comment form says \"Send\" is a defect the\n// customer can see and cannot fix without editing TypeScript. As a dictionary\n// in `.contentrain/`, the text is content: the migration writes it in the\n// source site's language, the customer edits it in Studio or through MCP, and a\n// rebuild picks it up with no re-emit.\n//\n// The emitter owns the key list and the English defaults, so the site still\n// works — in English — when the dictionary is absent. It never writes the\n// dictionary itself: which words a site uses is a content decision.\n\nimport type { ModelDefinition } from '@contentrain/types'\nimport { DICTIONARY_TITLE_FIELD } from '@contentrain/types'\n\n/** Where the dictionary lives in an emitted project, relative to its root. */\nexport const UI_STRINGS_DIR = '.contentrain/content/site/ui-strings'\n\n/** Every key the runtime components read, with the text they show without a dictionary. */\nexport const UI_STRING_DEFAULTS = {\n 'comments.cancel': 'Cancel',\n 'comments.closed': 'Comments are closed.',\n 'comments.empty': 'No comments yet.',\n 'comments.field.comment': 'Comment',\n 'comments.field.email': 'Email',\n 'comments.field.email_note': '(never shown)',\n 'comments.field.name': 'Name',\n 'comments.field.website': 'Website',\n 'comments.load_more': 'Load more comments',\n 'comments.moderator': 'Moderator',\n 'comments.noscript': 'Comments need JavaScript.',\n 'comments.pending': 'Thank you — your comment is awaiting moderation.',\n 'comments.post': 'Post comment',\n 'comments.posted': 'Your comment has been posted.',\n 'comments.replying_to': 'Replying to',\n 'comments.reply': 'Reply',\n 'comments.title': 'Comments',\n 'common.failed': 'Something went wrong. Please try again.',\n 'common.loading': 'Loading…',\n 'form.honeypot': 'Leave this field empty',\n 'form.noscript': 'This form needs JavaScript.',\n 'form.send': 'Send',\n 'form.sending': 'Sending…',\n} as const\n\nexport type UiStringKey = keyof typeof UI_STRING_DEFAULTS\n\n/**\n * The dictionary model a producer creates in the site's store. Exported so the\n * migration and the emitter cannot disagree about its id, kind or domain.\n */\nexport const UI_STRINGS_MODEL: ModelDefinition = {\n id: 'ui-strings',\n name: 'Site interface text',\n kind: 'dictionary',\n domain: 'site',\n i18n: true,\n title_field: DICTIONARY_TITLE_FIELD,\n description: 'What the site’s comments and forms say to visitors, per language. Missing keys fall back to English.',\n}\n\n/**\n * A project-relative directory safe to put in a glob, or undefined. The value\n * lands in source the build evaluates, so anything that could leave the\n * project or change the pattern is refused rather than escaped.\n */\nexport function uiStringsDir(dir: string | undefined): string | undefined {\n const value = (dir ?? UI_STRINGS_DIR).replace(/^\\/+|\\/+$/g, '')\n if (!value || value.split('/').some((s) => s === '..' || s === '.' || s === '') || !/^[\\w.@-]+(?:\\/[\\w.@-]+)*$/.test(value)) return undefined\n return value\n}\n\n/**\n * `src/lib/ui-strings.ts` for an emitted project: reads every locale file of\n * the dictionary at build time and resolves a page's text over the defaults.\n *\n * `import.meta.glob` returns an empty map when nothing matches, so a project\n * without the dictionary still builds. It does not stay quiet about it: a page\n * in a language other than English with no dictionary, or a dictionary missing\n * keys, is reported once in the build log.\n */\nexport function uiStringsSource(dir: string): string {\n const pattern = `/${dir}/*.json`\n return `// Emitted by @contentrain/emitter-astro — the site's interface text.\n//\n// Comments and forms read their visitor-facing text from the ui-strings\n// dictionary in the content store (${dir}/{locale}.json), so it is\n// edited as content — in Studio or through MCP — and a rebuild applies it.\n// Keys the dictionary does not have show the English default below.\n\nconst DEFAULTS: Record<string, string> = ${JSON.stringify(UI_STRING_DEFAULTS, null, 2)}\n\nconst files = import.meta.glob<Record<string, unknown>>(${JSON.stringify(pattern)}, { eager: true, import: 'default' })\nconst dictionaries = new Map(\n Object.entries(files).map(([path, dictionary]) => [path.slice(path.lastIndexOf('/') + 1, -5).toLowerCase(), dictionary]),\n)\nconst reported = new Set<string>()\n\n/** The interface text for a page language: the dictionary's values over the defaults. */\nexport function uiStrings(lang: string | undefined): Record<string, string> {\n const locale = (lang || 'en').toLowerCase()\n // tr-TR reads tr.json when there is no tr-tr.json — the store keys by primary subtag.\n const name = dictionaries.has(locale) ? locale : (locale.split('-')[0] ?? locale)\n const dictionary = dictionaries.get(name)\n const text: Record<string, string> = { ...DEFAULTS }\n const missing: string[] = []\n for (const key of Object.keys(DEFAULTS)) {\n const value = dictionary?.[key]\n if (typeof value === 'string' && value !== '') text[key] = value\n else missing.push(key)\n }\n if (!name.startsWith('en') && missing.length && !reported.has(name)) {\n reported.add(name)\n console.warn(\n dictionary\n ? '[contentrain] ui-strings: ' + name + '.json has no text for ' + missing.join(', ') + ' — those show in English'\n : '[contentrain] ui-strings: no dictionary for \"' + name + '\" at ${dir}/' + name + '.json — comments and forms show English text',\n )\n }\n return text\n}\n`\n}\n","// Components.\n//\n// `runtime`-sourced comments and forms get REAL implementations: an Astro\n// component that mounts a custom element carrying the runtime binding and the\n// page's entry address, and a client script that fetches, renders and submits\n// against the provider's public API (src/lib/embed.ts). Everything else — and\n// every runtime component when no binding was given — is a `<cr-component>`\n// placeholder that marks the spot: the emitter cannot invent an ad slot, and a\n// comments form without a provider is a promise the page cannot keep.\n//\n// A component is mounted where the chrome carries its marker\n// (`<!--@@component:ID@@-->`, see layouts.ts); emitting the file alone would\n// leave it unused, which is not integration.\n\nimport type { ComponentDef, RuntimeBinding } from '@contentrain/types'\nimport { EMBED_TS } from './embed.js'\nimport { uiStringsSource } from './ui-strings.js'\nimport { pascalCase, stableJson } from './util.js'\n\nexport interface ComponentGenResult {\n files: Record<string, string>\n warnings: string[]\n}\n\n/** Component types this emitter implements against a runtime binding. */\nexport const RUNTIME_IMPLEMENTED = new Set<ComponentDef['type']>(['comments', 'form'])\n\nexport function isRuntimeImplemented(c: ComponentDef, runtime: RuntimeBinding | undefined): boolean {\n if (!runtime || c.source !== 'runtime' || !RUNTIME_IMPLEMENTED.has(c.type)) return false\n return c.type !== 'form' || Boolean(c.model)\n}\n\nexport function componentFiles(\n components: ComponentDef[],\n runtime: RuntimeBinding | undefined,\n queryBound: ReadonlySet<string> = new Set(),\n uiStringsDir = '.contentrain/content/site/ui-strings',\n): ComponentGenResult {\n const files: Record<string, string> = {}\n const warnings: string[] = []\n let needsRuntime = false\n\n for (const c of components) {\n const name = pascalCase(c.id)\n if (isRuntimeImplemented(c, runtime)) {\n needsRuntime = true\n files[`src/components/${name}.astro`] = c.type === 'comments' ? commentsSource(c) : formSource(c)\n continue\n }\n if (c.source === 'runtime' && RUNTIME_IMPLEMENTED.has(c.type)) {\n warnings.push(\n !runtime\n ? `component ${c.id} (${c.type}): no runtime binding given — emitted as a placeholder; pass input.runtime to mount it`\n : `component ${c.id} (form): no model named — a form must say which collection it submits to; emitted as a placeholder`,\n )\n }\n files[`src/components/${name}.astro`] = placeholderSource(c, queryBound.has(c.id))\n }\n\n if (needsRuntime && runtime) {\n files['src/data/runtime.json'] = stableJson({ base_url: runtime.base_url, project_id: runtime.project_id })\n files['src/lib/embed.ts'] = EMBED_TS\n files['src/lib/ui-strings.ts'] = uiStringsSource(uiStringsDir)\n }\n return { files, warnings }\n}\n\nfunction header(c: ComponentDef, extra = ''): string {\n return `/**\n * cr-component: type=${c.type} source=${c.source}${c.name ? ` — ${c.name}` : ''}\n * Emitted by @contentrain/emitter-astro.${extra}\n */`\n}\n\nfunction placeholderSource(c: ComponentDef, queryBound: boolean): string {\n const variants = (c.variants ?? []).map((v) => v.key)\n return `---\n${header(\n c,\n `${c.source === 'runtime' ? '\\n * Placeholder — needs a live provider; see the migration handoff offers.' : ' Placeholder.'}${\n queryBound ? '\\n * A placement binds this region to a query; the layout renders it and passes the html.' : ''\n }${\n variants.length ? `\\n * Variants: ${variants.join(', ')}` : ''\n }`,\n)}\ninterface Props {\n variant?: string\n /** Content-store address of the page's entry; unused by a placeholder. */\n entry?: { model_id: string; entry_id: string; locale?: string }\n /**\n * Rendered list markup when a placement binds this region to a query. The\n * layout resolves the data — a component file is shared by id across\n * families, a placement belongs to one — so the region is editable here\n * while staying current there.\n */\n html?: string\n /** The page's language; unused by a placeholder. */\n lang?: string\n}\nconst { variant = ${JSON.stringify(variants[0] ?? 'default')}, html } = Astro.props\n---\n{html\n ? <div class=\"cr-query\" data-type=${JSON.stringify(c.type)} data-variant={variant} set:html={html} />\n : <cr-component data-type=${JSON.stringify(c.type)} data-variant={variant}></cr-component>}\n`\n}\n\nfunction commentsSource(c: ComponentDef): string {\n return `---\n${header(c, `\n * Mounted comments thread: reads the entry's approved comments from the runtime\n * provider's public API and posts new ones. Pending comments are never shown —\n * moderation happens on the provider. Renders nothing on a page without an\n * entry address (a list, a static page).`)}\nimport runtime from '../data/runtime.json'\nimport { uiStrings } from '../lib/ui-strings'\n\ninterface Props {\n /** Content-store address of the page's entry — the thread's key. */\n entry?: { model_id: string; entry_id: string; locale?: string }\n variant?: string\n /** Accepted for uniform mounting — the layout passes it to every mount; a thread renders its own markup. */\n html?: string\n /** The page's language — which ui-strings dictionary the thread's text comes from. */\n lang?: string\n}\nconst { entry, variant = 'default', lang } = Astro.props\nconst text = uiStrings(lang)\n---\n{entry && (\n <cr-comments\n class={\\`cr-comments cr-comments--\\${variant}\\`}\n data-base-url={runtime.base_url}\n data-project={runtime.project_id}\n data-model={entry.model_id}\n data-entry={entry.entry_id}\n data-locale={entry.locale}\n data-variant={variant}\n data-strings={JSON.stringify(text)}\n >\n <noscript><p class=\"cr-noscript\">{text['comments.noscript']}</p></noscript>\n </cr-comments>\n)}\n\n<script>\n import { mountComments } from '../lib/embed'\n\n class CrComments extends HTMLElement {\n connectedCallback() {\n void mountComments(this)\n }\n }\n if (!customElements.get('cr-comments')) customElements.define('cr-comments', CrComments)\n</script>\n`\n}\n\nfunction formSource(c: ComponentDef): string {\n return `---\n${header(c, `\n * Mounted public form for the ${JSON.stringify(c.model)} model: fetches the\n * exposed fields from the runtime provider, renders one control per field\n * (honeypot and captcha included when the model asks for them) and submits.\n * Submissions land on the provider for moderation; an approved one becomes a\n * content entry there.`)}\nimport runtime from '../data/runtime.json'\nimport { uiStrings } from '../lib/ui-strings'\n\ninterface Props {\n variant?: string\n /** Accepted for uniform mounting; a form is addressed by its model, not by the page's entry. */\n entry?: { model_id: string; entry_id: string; locale?: string }\n /** Accepted for uniform mounting — the layout passes it to every mount; a form renders its own markup. */\n html?: string\n /** The page's language — which ui-strings dictionary the form's text comes from. */\n lang?: string\n}\nconst { variant = 'default', lang } = Astro.props\nconst text = uiStrings(lang)\n---\n<cr-form\n class={\\`cr-form-mount cr-form-mount--\\${variant}\\`}\n data-base-url={runtime.base_url}\n data-project={runtime.project_id}\n data-model=${JSON.stringify(c.model)}\n data-variant={variant}\n data-strings={JSON.stringify(text)}\n>\n <noscript><p class=\"cr-noscript\">{text['form.noscript']}</p></noscript>\n</cr-form>\n\n<script>\n import { mountForm } from '../lib/embed'\n\n class CrForm extends HTMLElement {\n connectedCallback() {\n void mountForm(this)\n }\n }\n if (!customElements.get('cr-form')) customElements.define('cr-form', CrForm)\n</script>\n`\n}\n","// Header and footer chrome → shared Astro components.\n//\n// The body chrome stays one fragment because the content container is nested\n// deep inside it. The masthead and the colophon are the exception: they sit\n// outside the content path and repeat across families, so lifting them out\n// gives the project one address for the nav (where a jQuery-free menu replaces\n// the theme's) and one copy of the markup instead of N.\n//\n// Identity is by CONTENT, not by declared name: two families whose headers are\n// byte-identical share a component even if the producer named them differently,\n// and two different headers that claim the same name get separate files plus a\n// warning — never a silent swap, which would put one family's nav on another's\n// pages.\n\nimport type { LayoutFamily } from '@contentrain/types'\nimport { CHROME_BODY_SLOT } from '@contentrain/types'\nimport { balanceWarning } from './balance.js'\nimport { pascalCase, stableJson } from './util.js'\n\nexport type ChromePosition = 'header' | 'footer'\n\nexport interface ChromeComponentRef {\n name: string\n position: ChromePosition\n}\n\nexport interface ChromeRegistry {\n files: Record<string, string>\n /** Family id → the components its layout renders, in chunk order. */\n byFamily: Map<string, ChromeComponentRef[]>\n warnings: string[]\n}\n\nconst DEFAULT_NAME: Record<ChromePosition, string> = {\n header: 'SiteHeader',\n footer: 'SiteFooter',\n}\n\n/** A component name Astro can import: PascalCase and starting with a letter. */\nfunction componentName(raw: string): string {\n const pascal = pascalCase(raw)\n return /^[A-Za-z]/.test(pascal) ? pascal : `Chrome${pascal}`\n}\n\nfunction componentSource(name: string): string {\n return `---\n/**\n * Chrome component: ${name} — emitted by @contentrain/emitter-astro.\n * Markup travels as data so the Astro compiler never parses theme markup, and\n * fills the same \\`@@mark@@\\` placeholders the rest of the chrome does.\n */\nimport chunk from '../data/components/${name}.json'\nimport { renderTemplate } from '../lib/fill'\n\ninterface Props {\n /** Page marks — the region's own placeholders (site name, current-page class). */\n marks?: Record<string, unknown>\n}\nconst { marks = {} } = Astro.props\n---\n<Fragment set:html={renderTemplate(chunk.html, marks)} />\n`\n}\n\nexport function chromeComponents(families: LayoutFamily[]): ChromeRegistry {\n const files: Record<string, string> = {}\n const byFamily = new Map<string, ChromeComponentRef[]>()\n const warnings: string[] = []\n /** html → emitted component name, so identical regions collapse into one. */\n const byHtml = new Map<string, string>()\n /** emitted name → its html, so a name clash can be detected and renamed. */\n const byName = new Map<string, string>()\n\n for (const family of families) {\n const refs: ChromeComponentRef[] = []\n for (const chunk of family.chrome ?? []) {\n if (chunk.position !== 'header' && chunk.position !== 'footer') continue\n const position: ChromePosition = chunk.position\n const where = `family ${family.id}: ${position} chrome`\n\n if (chunk.html.includes(CHROME_BODY_SLOT)) {\n warnings.push(\n `${where} carries ${CHROME_BODY_SLOT} — content goes in the body chunk; the marker is rendered as a comment`,\n )\n }\n const unbalanced = balanceWarning(chunk.html)\n if (unbalanced) {\n warnings.push(\n `${where} is not balanced (${unbalanced}) — a lifted region must close what it opens; keep it in the body chunk`,\n )\n }\n\n const wanted = componentName(chunk.component ?? DEFAULT_NAME[position])\n let name = byHtml.get(chunk.html)\n if (name === undefined) {\n name = wanted\n for (let n = 2; byName.has(name) && byName.get(name) !== chunk.html; n++) {\n name = `${wanted}${n}`\n }\n if (name !== wanted) {\n warnings.push(\n `${where}: component name \"${wanted}\" is already used by different markup — emitted as \"${name}\"`,\n )\n }\n byHtml.set(chunk.html, name)\n byName.set(name, chunk.html)\n files[`src/data/components/${name}.json`] = stableJson({ html: chunk.html })\n files[`src/components/${name}.astro`] = componentSource(name)\n }\n refs.push({ name, position })\n }\n if (refs.length) byFamily.set(family.id, refs)\n }\n\n return { files, byFamily, warnings }\n}\n","// Legacy CSS quarantine (the fidelity layer).\n//\n// Migrated pages must render pixel-identically, and a utility framework's\n// preflight/reset would fight the theme's CSS. The truce is a cascade layer:\n// every legacy stylesheet is wrapped in `@layer legacy { … }`, and modern CSS\n// lives in its own layer — the two never meet in specificity.\n//\n// One CSS rule constrains the wrapping: `@import` (and `@charset`) must\n// precede all other statements, so a file cannot simply be wrapped whole.\n// Leading imports are hoisted above the wrapper and given `layer(legacy)`;\n// an import *after* other rules is already invalid CSS that browsers ignore,\n// so the file is left unwrapped and a warning recorded instead of silently\n// producing different invalidity.\n\nimport { LEGACY_CSS_LAYER } from '@contentrain/types'\n\nexport interface WrapCssResult {\n content: string\n warning?: string\n}\n\nconst LEADING = /^(\\s*(?:@charset[^;]+;|@import[^;]+;|\\/\\*[\\s\\S]*?\\*\\/)\\s*)+/\n\nexport function wrapLegacyCss(path: string, css: string): WrapCssResult {\n const rest = css.replace(LEADING, '')\n if (/@import\\s/.test(rest)) {\n return {\n content: css,\n warning: `${path}: @import after other rules — left unwrapped (fix the source stylesheet; mid-file imports are ignored by browsers)`,\n }\n }\n const leading = css.slice(0, css.length - rest.length)\n const hoisted = leading.replace(/@import\\s+([^;]+);/g, (full, spec: string) =>\n /\\blayer\\s*\\(/.test(spec) ? full : `@import ${spec.trim()} layer(${LEGACY_CSS_LAYER});`,\n )\n return { content: `${hoisted}@layer ${LEGACY_CSS_LAYER} {\\n${rest}\\n}\\n` }\n}\n","// The live site's own redirect rules → Astro's `redirects` config (AI-11).\n//\n// Only a plain one-to-one rule can be written: `match` absent or `url`, not a\n// regular expression, a site-root `from` without a query string, and a\n// status Astro serves as a redirect. Everything else is returned with its\n// reason as a rule to set up by hand at the host. Turning a pattern into a\n// literal `from` would produce a redirect that matches the wrong address, so\n// the emitter never guesses.\n//\n// A rule whose `from` is an address the migrated site builds a page at is\n// not written either: the page holds the content. Astro does not refuse the\n// pair — the redirect is written over the page and the build reports no\n// error, so the page would vanish silently.\n//\n// In a static build Astro serves a redirect as an HTML page with a\n// meta refresh, `noindex` and a canonical link to the target; the status\n// reaches crawlers only through a host adapter that writes the host's own\n// redirect file.\n\nimport type { RawRedirect, RouteModel } from '@contentrain/types'\nimport type { EmitContent } from './types.js'\nimport { DEFAULT_COLLECTION } from './types.js'\nimport { entryPath } from './alternates.js'\nimport { collectionItems } from './pages.js'\nimport { patternToPagePath } from './util.js'\n\n/** The statuses written to `astro.config`; any other is a manual rule. */\nexport const REDIRECT_STATUSES = [301, 302, 307, 308] as const\nexport type RedirectStatus = typeof REDIRECT_STATUSES[number]\n\n/** A rule the emitter did not write, with why. */\nexport interface ManualRedirect {\n redirect: RawRedirect\n reason: string\n}\n\nexport interface RedirectPlan {\n /** `from` → rule, as `astro.config` `redirects` takes it. Sorted by `from`. */\n config: Record<string, { status: RedirectStatus; destination: string }>\n /** The input rules written, in input order. */\n written: RawRedirect[]\n /** The rules to set up by hand at the host, in input order. */\n manual: ManualRedirect[]\n}\n\n/** Whitespace, a control character or DEL anywhere in the string. */\nconst hasControlOrSpace = (value: string): boolean =>\n [...value].some((char) => char.charCodeAt(0) <= 32 || char.charCodeAt(0) === 127)\n\n/** `/old/` and `/old` are one address in the directory build format. */\nfunction addressKey(path: string): string {\n return path === '/' ? '/' : `${path.replace(/\\/+$/, '')}/`\n}\n\n/**\n * Every address the emitted site builds a page at, as `addressKey`s, with the\n * route that owns it. Single and collection routes build one page per entry,\n * query routes one per result set, and a route without parameters one page.\n */\nexport function builtAddresses(routes: RouteModel[], content: EmitContent): Map<string, string> {\n const out = new Map<string, string>()\n const put = (path: string | null, routeId: string) => {\n if (path !== null && !out.has(addressKey(path))) out.set(addressKey(path), routeId)\n }\n for (const route of routes) {\n const pagePath = patternToPagePath(route.pattern)\n if (!pagePath) continue\n if (!pagePath.includes('[')) {\n put(entryPath(route.pattern, {}), route.id)\n continue\n }\n if (route.kind === 'single' || route.collection !== undefined) {\n for (const post of collectionItems(content, route.collection ?? DEFAULT_COLLECTION)) {\n put(entryPath(route.pattern, { ...post.params, slug: post.slug }), route.id)\n }\n }\n if (route.query) {\n for (const page of content.queries?.[route.query] ?? []) put(entryPath(route.pattern, page.params), route.id)\n }\n }\n return out\n}\n\n/** The decoded path for a literal Astro redirect key, or why `from` cannot be one. */\nfunction checkFrom(from: string, hostOnly = false): { path: string } | { reason: string } {\n if (!from.startsWith('/') || from.startsWith('//')) return { reason: 'from is not a site-root path' }\n if (/[?#]/.test(from)) return { reason: 'from has a query string or fragment — a static redirect matches the path only' }\n let path: string\n try {\n path = decodeURI(from)\n }\n catch {\n return { reason: 'from is not a valid percent-encoded path' }\n }\n if (hasControlOrSpace(path) || path.includes('\\\\')) return { reason: 'from contains whitespace, control characters or a backslash' }\n // Astro reads brackets in a route as parameters.\n if (/[[\\]]/.test(path)) return { reason: 'from contains [ or ], which Astro reads as a route parameter' }\n const segments = path.split('/').filter(Boolean)\n if (segments.some((s) => s === '.' || s === '..')) return { reason: 'from contains a . or .. segment' }\n // The directory build writes a redirect as `<from>/index.html`; a host\n // serving `/old.php` looks for that file, not the directory. A host file\n // matches the address itself, so a host-only rule may name a file.\n if (!hostOnly && /\\.[a-z0-9]{1,5}$/i.test(segments.at(-1) ?? '')) return { reason: 'from ends in a file name — the static build serves it as a directory, not at this address' }\n return { path }\n}\n\n/**\n * A path the build writes a file at: the emitter's own endpoints (robots,\n * llms.txt, every feed, @astrojs/sitemap's files), the 404 page, and the\n * directories of Astro's bundles and the legacy stylesheets. A host file\n * answers before the filesystem — a forced Netlify rule and every vercel.json\n * redirect — so a host-only rule here would shadow the file. `builtAddresses`\n * knows pages only. Compared without a trailing slash (a host file gets both\n * forms) and in lower case (Netlify matches rules case-insensitively).\n * WordPress's own `/sitemap.xml` and `/sitemap_index.xml` are not the build's:\n * a 301 from them is one a migration wants.\n */\nfunction buildOutputFile(path: string): boolean {\n const p = (path.replace(/\\/+$/, '') || '/').toLowerCase()\n return p === '/robots.txt' || p === '/llms.txt' || p === '/404.html' || p.endsWith('/feed.xml')\n || /^\\/sitemap-(?:index|\\d+)\\.xml$/.test(p) || /^\\/(?:_astro|styles)(?:\\/|$)/.test(p)\n}\n\nfunction checkTo(to: string): boolean {\n if (hasControlOrSpace(to)) return false\n if (to.startsWith('/')) return !to.startsWith('//')\n try {\n const url = new URL(to)\n return url.protocol === 'https:' || url.protocol === 'http:'\n }\n catch {\n return false\n }\n}\n\n/**\n * Split the rules into what `astro.config` serves and what the host has to.\n * `built` is `builtAddresses` of the emitted routes. `hostOnly`: the rules go\n * to host files alone (`EmitInput.hostRedirects`), so the checks that hold\n * only for a static redirect page (a `from` naming a file) are left out.\n */\nexport function planRedirects(redirects: RawRedirect[], built: Map<string, string>, options: { hostOnly?: boolean } = {}): RedirectPlan {\n const config: RedirectPlan['config'] = {}\n const written: RawRedirect[] = []\n const manual: ManualRedirect[] = []\n const claimed = new Set<string>()\n const builtLower = new Map([...built].map(([address, route]) => [address.toLowerCase(), [address, route] as const]))\n\n for (const redirect of redirects) {\n const skip = (reason: string) => manual.push({ redirect, reason })\n if (redirect.regex) { skip('from is a regular expression'); continue }\n if (redirect.match !== undefined && redirect.match !== 'url') { skip(`match \"${redirect.match}\" is a pattern, not one address`); continue }\n const status = redirect.status ?? 301\n if (!(REDIRECT_STATUSES as readonly number[]).includes(status)) { skip(`status ${status} is not one of ${REDIRECT_STATUSES.join('/')}`); continue }\n const from = checkFrom(redirect.from, options.hostOnly)\n if ('reason' in from) { skip(from.reason); continue }\n // Only a host-only rule can name a file (see checkFrom).\n if (options.hostOnly && buildOutputFile(from.path)) { skip('from is a file the build writes — the host file would serve the redirect over it'); continue }\n if (!checkTo(redirect.to)) { skip('to is not a site-root path or an http(s) URL'); continue }\n const key = addressKey(from.path)\n const owner = built.get(key)\n if (owner !== undefined) { skip(`the migrated site builds a page at ${key} (route ${owner}) — the page is kept`); continue }\n // Netlify matches rules case-insensitively, and macOS and Windows file\n // systems cannot hold /About/index.html beside /about/index.html: either\n // way the redirect would be served over the page, or loop into it.\n const caseTwin = builtLower.get(key.toLowerCase())\n if (caseTwin) { skip(`from differs only in letter case from the page at ${caseTwin[0]} (route ${caseTwin[1]}) — a case-insensitive host or file system would serve the redirect over it; the page is kept`); continue }\n if (claimed.has(key)) { skip(`another rule already redirects ${key}`); continue }\n if (!/^https?:/.test(redirect.to)) {\n let target: string\n try {\n target = decodeURI(redirect.to.replace(/[?#].*$/, ''))\n }\n catch {\n target = redirect.to\n }\n if (addressKey(target) === key) { skip('to is the same address as from'); continue }\n }\n claimed.add(key)\n config[from.path] = { status: status as RedirectStatus, destination: redirect.to }\n written.push(redirect)\n }\n\n const sorted = Object.fromEntries(Object.entries(config).toSorted(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))\n return { config: sorted, written, manual }\n}\n\n/** The `redirects` line for astro.config.mjs, or null when there is nothing to write. */\nexport function astroRedirectsConfig(config: RedirectPlan['config']): string | null {\n const entries = Object.entries(config)\n if (!entries.length) return null\n const lines = entries.map(([from, rule]) => ` ${JSON.stringify(from)}: { status: ${rule.status}, destination: ${JSON.stringify(rule.destination)} },`)\n return [` redirects: {`, ...lines, ` },`].join('\\n')\n}\n\n/** Hosts whose own redirect file the emitter can write. */\nexport type RedirectHost = 'netlify' | 'cloudflare' | 'vercel'\n\n/**\n * Cloudflare Pages takes 2,000 static rules, Vercel 2,048 redirects; each\n * rule is written twice (with and without its trailing slash).\n */\nexport const HOST_RULE_LIMIT = 2000\n\nexport interface HostRedirectFiles {\n /** Path → content, for the emitted project. */\n files: Record<string, string>\n /** Which host each written file is for, e.g. `public/_redirects (netlify)`. */\n written: string[]\n /** Config keys a host file cannot express, left to the meta-refresh fallback. */\n skipped: string[]\n /**\n * Config keys left out of a Cloudflare or Vercel file because it reached\n * that host's rule limit — served by the meta-refresh fallback only there.\n */\n over_limit: string[]\n}\n\n/** Netlify reads `:name` and `*` in a rule as placeholders. */\nconst HOST_PATTERN_CHAR = /(?:^|\\/):|\\*/\n\n/** path-to-regexp (vercel.json `source`): escape what it would read as syntax. */\nconst escapePathPattern = (s: string): string => s.replace(/[:()*+?{}\\\\]/g, (c) => `\\\\${c}`)\n\n/** A path as a host matches it: percent-encoded, with and without the trailing slash. */\nfunction hostSources(path: string): string[] {\n const encoded = encodeURI(path)\n if (encoded === '/') return ['/']\n const bare = encoded.replace(/\\/+$/, '')\n return [bare, `${bare}/`]\n}\n\n/**\n * Real HTTP redirects for the host the site deploys to. A static Astro build\n * serves `redirects` as meta-refresh pages; the status code only exists if the\n * host answers the request itself, from its own redirect file. The\n * meta-refresh pages stay as the fallback for a host without one.\n *\n * - `netlify` — `public/_redirects`, forced (`301!`): the build writes a page\n * at every redirected path, and Netlify serves an existing file before an\n * unforced rule.\n * - `cloudflare` — `public/_redirects`, unforced: Cloudflare Pages has no `!`\n * and applies its rules before static assets.\n * - `vercel` — `vercel.json` `redirects`, which Vercel applies before the\n * filesystem.\n *\n * Without a host both the Netlify file and `vercel.json` are written; a site\n * on Cloudflare Pages should name its host.\n */\nexport function hostRedirectFiles(config: RedirectPlan['config'], host?: RedirectHost): HostRedirectFiles {\n const entries = Object.entries(config)\n const skipped = entries.filter(([from]) => HOST_PATTERN_CHAR.test(from)).map(([from]) => from)\n const rules = entries.filter(([from]) => !skipped.includes(from))\n if (!rules.length) return { files: {}, written: [], skipped, over_limit: [] }\n // Netlify has no rule limit; Cloudflare's and Vercel's files stop at theirs.\n const limited = rules.slice(0, Math.floor(HOST_RULE_LIMIT / 2))\n const overLimit = host === 'netlify' ? [] : rules.slice(limited.length).map(([from]) => from)\n\n const files: Record<string, string> = {}\n const written: string[] = []\n const redirectsFile = (force: boolean, list: typeof rules) => [\n '# Emitted by @contentrain/emitter-astro — the source site\\'s redirects, as real HTTP redirects.',\n ...list.flatMap(([from, rule]) => hostSources(from).map((source) => `${source} ${rule.destination} ${rule.status}${force ? '!' : ''}`)),\n '',\n ].join('\\n')\n if (host === undefined || host === 'netlify') {\n files['public/_redirects'] = redirectsFile(true, rules)\n written.push('public/_redirects (netlify)')\n }\n if (host === 'cloudflare') {\n files['public/_redirects'] = redirectsFile(false, limited)\n written.push('public/_redirects (cloudflare)')\n }\n if (host === undefined || host === 'vercel') {\n const redirects = limited.flatMap(([from, rule]) => hostSources(from).map((source) => ({\n source: escapePathPattern(source),\n destination: rule.destination,\n statusCode: rule.status,\n })))\n files['vercel.json'] = `${JSON.stringify({ redirects }, null, 2)}\\n`\n written.push('vercel.json (vercel)')\n }\n return { files, written, skipped, over_limit: overLimit }\n}\n","// Only published content is built.\n//\n// The content store keeps every entry the source had — drafts, posts waiting\n// for review, posts scheduled for later (WordPress `future`, imported as\n// `published` with a `publish_at` ahead), password-protected and private posts.\n// Only a public entry whose status is `published` and whose `publish_at`, if it\n// has one, has come gets a page, a\n// sitemap line, a feed item, an llms.txt link or a card in a list. The rest\n// stay in the store for the editor and are held back here, before anything is\n// emitted — so every later step (pages, data files, sitemap, feed, redirects,\n// hreflang) sees the published site and nothing else.\n//\n// `publish_at` is compared with the emit time (`EmitOptions.now`). The data\n// files carry no unpublished entry, so a scheduled post goes live with the\n// next emit after its time, not by rebuilding the same files.\n//\n// A published page can link to an entry that is held back. The link would\n// 404; it is kept as its text instead, and each one is reported.\n\nimport type { RouteModel } from '@contentrain/types'\nimport type { EmitContent, EmitPost, QueryPage, WithheldEntry, WithheldLink } from './types.js'\nimport { DEFAULT_COLLECTION } from './types.js'\nimport { entryPath } from './alternates.js'\nimport { collectionItems } from './pages.js'\nimport { patternToPagePath } from './util.js'\nimport { builtAddresses } from './redirects.js'\n\n/**\n * Why an entry is not built, or null when it is. `now` is epoch ms. With\n * `requireStatus` an entry that carries no status is held back: the producer\n * said it passes every entry's status, so a missing one is a gap, not a\n * published entry.\n */\nexport function holdReason(post: Pick<EmitPost, 'status' | 'publish_at' | 'visibility'>, now: number, requireStatus = false): WithheldEntry['reason'] | null {\n // Password-protected and private content is never built, whatever its status\n // says — and a visibility this does not know is not taken for public.\n if (post.visibility !== undefined && post.visibility !== 'public') return 'visibility'\n if (post.status === undefined && requireStatus) return 'status_missing'\n if (post.status !== undefined && post.status !== 'published') return 'status'\n if (post.publish_at === undefined) return null\n const at = Date.parse(post.publish_at)\n // A schedule that cannot be read is not a schedule that has come.\n if (Number.isNaN(at)) return 'publish_at_invalid'\n return at > now ? 'scheduled' : null\n}\n\nconst bareHost = (host: string) => host.replace(/^www\\./, '').toLowerCase()\n\n/** `/a/b` and `/a/b/` are one address. */\nconst addressKey = (path: string) => (path === '/' ? '/' : `${path.replace(/\\/+$/, '')}/`)\n\nexport interface PublishedContent {\n content: EmitContent\n withheld: WithheldEntry[]\n links: WithheldLink[]\n /** The addresses held-back entries would have had and no published entry builds (`/a/b/`). */\n gone: Set<string>\n}\n\n/**\n * The held-back address an href points at, or null: a site-root path, or an\n * absolute URL on the site's own host (`www.` either way).\n */\nexport function withheldTarget(href: string, gone: Set<string>, siteUrl?: string): string | null {\n if (!gone.size) return null\n let site: URL | undefined\n try {\n site = siteUrl ? new URL(siteUrl) : undefined\n } catch {\n site = undefined\n }\n let url: URL\n try {\n url = new URL(href, site ?? 'http://site.invalid')\n } catch {\n return null\n }\n const own = site ? bareHost(url.host) === bareHost(site.host) : url.host === 'site.invalid'\n if (!own || !/^https?:$/.test(url.protocol)) return null\n let path = url.pathname\n try {\n path = decodeURI(path)\n } catch { /* keep the encoded path */ }\n return gone.has(addressKey(path)) ? path : null\n}\n\n/**\n * The content with every unpublished entry removed — from its collection and\n * from the list pages that carry it as an item — and links to them unwrapped.\n * Content with nothing held back is returned as given.\n */\nexport function publishedContent(routes: RouteModel[], content: EmitContent, now: number, siteUrl?: string, requireStatus = false): PublishedContent {\n const withheld: WithheldEntry[] = []\n const keep = (collection: string) => (post: EmitPost) => {\n const reason = holdReason(post, now, requireStatus)\n if (!reason) return true\n withheld.push({ collection, slug: post.slug, ...(post.status ? { status: post.status } : {}), ...(post.visibility ? { visibility: post.visibility } : {}), ...(post.publish_at ? { publish_at: post.publish_at } : {}), reason })\n return false\n }\n const collections = content.collections\n ? Object.fromEntries(Object.entries(content.collections).map(([name, posts]) => [name, posts.filter(keep(name))]))\n : undefined\n const isLive = (post: EmitPost) => holdReason(post, now, requireStatus) === null\n // `posts` is read only when `collections.posts` is absent; filtered either way, reported once.\n const posts = content.posts?.filter(content.collections?.[DEFAULT_COLLECTION] ? isLive : keep(DEFAULT_COLLECTION))\n const queries = content.queries\n ? Object.fromEntries(Object.entries(content.queries).map(([id, pages]) => [id, pages.map((page) => (page.items.every(isLive) ? page : { ...page, items: page.items.filter(isLive) }))]))\n : undefined\n const listedOnly = Object.values(content.queries ?? {}).flat().flatMap((p) => p.items).some((item) => !isLive(item))\n if (!withheld.length && !listedOnly) return { content, withheld, links: [], gone: new Set() }\n\n const filtered: EmitContent = {\n ...content,\n ...(posts ? { posts } : {}),\n ...(collections ? { collections } : {}),\n ...(queries ? { queries } : {}),\n }\n\n // The addresses the held-back entries would have had, less any a published\n // entry still builds (a published translation, a page on the same path).\n const built = builtAddresses(routes, filtered)\n const gone = new Set<string>()\n for (const route of routes) {\n const pagePath = patternToPagePath(route.pattern)\n if (!pagePath || !pagePath.includes('[') || (route.kind !== 'single' && route.collection === undefined)) continue\n const collection = route.collection ?? DEFAULT_COLLECTION\n for (const post of collectionItems(content, collection)) {\n const path = entryPath(route.pattern, { ...post.params, slug: post.slug })\n if (path && !isLive(post) && !built.has(addressKey(path))) gone.add(addressKey(path))\n }\n }\n\n const links: WithheldLink[] = []\n const unlink = (html: string | undefined, page: string): string | undefined => {\n if (!html || !gone.size) return html\n return html.replace(ANCHOR_RE, (anchor, double: string | undefined, single: string | undefined, inner: string) => {\n const to = withheldTarget((double ?? single ?? '').replace(/&amp;/g, '&'), gone, siteUrl)\n if (!to) return anchor\n links.push({ page, href: to })\n return inner\n })\n }\n const unlinkPost = (post: EmitPost, page: string): EmitPost => {\n const body = unlink(post.body, page) ?? post.body\n const excerpt_html = unlink(post.excerpt_html, page)\n return body === post.body && excerpt_html === post.excerpt_html ? post : { ...post, body, ...(excerpt_html !== undefined ? { excerpt_html } : {}) }\n }\n const pageOf = (collection: string, post: EmitPost) => {\n const route = routes.find((r) => (r.kind === 'single' || r.collection !== undefined) && (r.collection ?? DEFAULT_COLLECTION) === collection)\n return (route && entryPath(route.pattern, { ...post.params, slug: post.slug })) ?? `${collection}/${post.slug}`\n }\n if (filtered.collections) {\n filtered.collections = Object.fromEntries(Object.entries(filtered.collections).map(([name, list]) => [name, list.map((p) => unlinkPost(p, pageOf(name, p)))]))\n }\n if (filtered.posts) filtered.posts = filtered.posts.map((p) => unlinkPost(p, pageOf(DEFAULT_COLLECTION, p)))\n if (filtered.queries) {\n filtered.queries = Object.fromEntries(Object.entries(filtered.queries).map(([id, pages]) => [id, pages.map((page: QueryPage) => {\n const route = routes.find((r) => r.query === id)\n const at = (route && entryPath(route.pattern, page.params)) ?? `query ${id}`\n return { ...page, items: page.items.map((item) => unlinkPost(item, at)) }\n })]))\n }\n return { content: filtered, withheld, links, gone }\n}\n\n// One anchor with its href and its content. Anchors do not nest, so the first\n// closing tag is this one's.\nconst ANCHOR_RE = /<a\\b[^>]*?\\shref\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)')[^>]*>([\\s\\S]*?)<\\/a\\s*>/gi\n\n/** The report lines for what was held back. */\nexport function withheldWarnings(withheld: WithheldEntry[], links: WithheldLink[]): string[] {\n const out: string[] = []\n if (withheld.length) {\n const count = (reason: WithheldEntry['reason']) => withheld.filter((w) => w.reason === reason).length\n const parts = [\n count('visibility') && `${count('visibility')} password-protected or private`,\n count('status_missing') && `${count('status_missing')} with no status (options.requireStatus)`,\n count('status') && `${count('status')} not published (draft, in review, rejected or archived)`,\n count('scheduled') && `${count('scheduled')} scheduled for later — built by the first emit after their publish_at`,\n count('publish_at_invalid') && `${count('publish_at_invalid')} with a publish_at that is not a date`,\n ].filter(Boolean)\n out.push(`unpublished: ${withheld.length} entries not built — ${parts.join('; ')}. They stay in the content store; EmitResult.withheld lists each`)\n }\n if (links.length) {\n const sample = links.slice(0, 5).map((l) => `${l.href} (on ${l.page})`).join(', ')\n out.push(`unpublished: ${links.length} links from published pages to unpublished entries kept as plain text — ${sample}${links.length > 5 ? ' …' : ''}`)\n }\n return out\n}\n","// A page's SEO as the source's plugin held it (`RawSeo.entries[address]`,\n// what the WordPress Bridge exports) → the fields an `EmitPost` / `QueryPage`\n// carries. The emitter renders; choosing among plugins and reading their\n// output is done here once, so every producer reading a Bridge export maps it\n// the same way.\n\nimport type { RawSeoEntry, SeoProvider } from '@contentrain/types'\nimport { SEO_PROVIDERS } from '@contentrain/types'\nimport type { SocialOverride, TwitterOverride } from './types.js'\n\n/** The SEO fields of one page, ready to spread into an `EmitPost` or `QueryPage`. */\nexport interface SeoFields {\n /** For an `EmitPost`; a `QueryPage`'s `title` is already its document title. */\n seo_title?: string\n description?: string\n canonical?: string\n /** Kept out of search. */\n noindex?: boolean\n nofollow?: boolean\n open_graph?: SocialOverride\n twitter?: TwitterOverride\n schema?: unknown\n}\n\nexport interface SeoFromRawOptions {\n /** The plugin the live site's head came from (`RawSeo.serving`). Its block is read first. */\n serving?: SeoProvider | 'wordpress-core'\n /**\n * The page's own absolute address. A canonical equal to it is left out — the\n * Seo component already points a page at itself — so only a canonical that\n * sends the page somewhere else becomes an override.\n */\n url?: string\n}\n\n/**\n * A plugin's unrendered template, in its own syntax: Yoast `%%title%%`,\n * SEOPress `%%post_title%%`, Rank Math `%title%` / `%customfield(name)%`,\n * AIOSEO `#post_title`. Printed as-is it would be a literal\n * `%%title%% %%sep%%` in the search result, so such a value is dropped and\n * the page's own falls in. AIOSEO's tags are matched by name, not as any\n * `#word`: a title can hold a hashtag.\n */\nconst TEMPLATE_RE: Record<SeoProvider, RegExp> = {\n yoast: /%%[a-z0-9_-]+%%/i,\n seopress: /%%[a-z0-9_-]+%%/i,\n rank_math: /%[a-z_]+(?:\\([^)]*\\))?%/i,\n aioseo: /#(?:(?:alt|archive|attachment|author|category|current|custom_field|page|parent|post|search|separator|site|tax|taxonomy)_[a-z0-9_-]+|categories|permalink|tagline)\\b/i,\n}\n\n/**\n * Whether a string holds a template token. Percent-encoding is decoded first\n * when it decodes: `%E4%B8%AD` in a CJK URL is text, not Rank Math's `%AD%`.\n * A real token never decodes (`%ti…` is no escape), so it is tested as it is.\n */\nfunction hasToken(value: string, template: RegExp): boolean {\n let decoded = value\n try {\n decoded = decodeURI(value)\n } catch {\n // Not decodable — as a template token is not: test the string itself.\n }\n return template.test(decoded)\n}\n\nconst text = (value: string | undefined, template: RegExp | undefined): string | undefined => {\n const v = value?.trim()\n if (!v) return undefined\n return template && hasToken(v, template) ? undefined : v\n}\n\nconst addressKey = (url: string): string => url.trim().replace(/^https?:\\/\\//i, '//').replace(/\\/+(?=[?#]|$)/, '')\n\nfunction social(value: TwitterOverride | undefined, template: RegExp | undefined): TwitterOverride | undefined {\n if (!value) return undefined\n const out: TwitterOverride = {}\n const title = text(value.title, template)\n const description = text(value.description, template)\n const image = value.image?.trim()\n const card = value.card?.trim()\n if (title) out.title = title\n if (description) out.description = description\n if (image) out.image = image\n if (card) out.card = card\n return Object.keys(out).length ? out : undefined\n}\n\n/** Share-card values: the rendered ones, each missing one from the block. */\nfunction merged(rendered: TwitterOverride | undefined, stored: TwitterOverride | undefined): TwitterOverride | undefined {\n if (!rendered) return stored\n if (!stored) return rendered\n return { ...stored, ...rendered }\n}\n\n/**\n * One page's SEO fields from the blocks each plugin holds for it. The serving\n * plugin's block wins; without one the first plugin (Yoast, Rank Math,\n * AIOSEO, SEOPress) that has data does. Its values are read, in order, from:\n *\n * 1. the block itself when the running plugin rendered it (`resolved`);\n * 2. `rendered` — the exporter's own rendering of the plugin's templates, for\n * a plugin that is not the one serving the head. Final text: a variable it\n * could not render is already out of the string, and only listed in\n * `unresolved` for a person to read;\n * 3. the stored values, with the plugin's unrendered templates dropped.\n *\n * 2 and 3 are taken field by field: a value the rendering does not carry\n * falls back to the block's stored literal.\n *\n * Whatever the source, a string that still holds a template token in the\n * plugin's syntax is dropped: a token is never printed. The same holds for\n * the JSON-LD: a resolved block's graph is the plugin's rendering; any other\n * block's graph — the exporter's rendered nodes, else its own top-level graph\n * — is read node by node, and a node still holding a token is left out.\n *\n * Robots come from `robots_served` — what the page actually carried after\n * WordPress and the plugin reconciled their settings — then `rendered.robots`,\n * then the plugin's own `robots`.\n */\nexport function seoFromRawEntry(\n blocks: Partial<Record<SeoProvider, RawSeoEntry>> | undefined,\n options: SeoFromRawOptions = {},\n): SeoFields {\n if (!blocks) return {}\n const order = [\n ...(options.serving && options.serving !== 'wordpress-core' ? [options.serving] : []),\n ...SEO_PROVIDERS,\n ]\n const provider = order.find((p) => blocks[p] !== undefined)\n if (!provider) return {}\n const entry = blocks[provider]!\n // A block the running plugin rendered is final text; anything else is\n // checked for its plugin's template tokens.\n const template = entry.resolved === true ? undefined : TEMPLATE_RE[provider]\n const rendered = entry.resolved === true ? undefined : entry.rendered\n const out: SeoFields = {}\n\n // Field by field: what the exporter rendered, else the block's own value —\n // a stored literal still counts where the rendering has nothing to say, and\n // a stored template is still dropped by `text`.\n const title = text(rendered?.title, template) ?? text(entry.title, template)\n if (title) out.seo_title = title\n const description = text(rendered?.description, template) ?? text(entry.description, template)\n if (description) out.description = description\n\n const canonical = (rendered?.canonical ?? entry.canonical)?.trim()\n if (canonical && !(options.url && addressKey(canonical) === addressKey(options.url))) out.canonical = canonical\n\n const served = entry.robots_served?.map((d) => d.trim().toLowerCase())\n const robots = rendered?.robots ?? entry.robots\n const noindex = served ? served.includes('noindex') : robots?.index === 'noindex'\n const nofollow = served ? served.includes('nofollow') : robots?.follow === 'nofollow'\n if (noindex) out.noindex = true\n if (nofollow) out.nofollow = true\n\n const og = merged(social(rendered?.open_graph, template), social(entry.open_graph, template))\n if (og) out.open_graph = og\n // The card is a setting, not rendered text: it comes from the block.\n const tw = merged(social(rendered?.twitter, template), social(entry.twitter, template))\n if (tw) out.twitter = tw\n\n // The plugin's rendered graph; else the stored schema nodes the exporter\n // rendered (Rank Math), as a graph of their own.\n // JSON-LD: the graph the running plugin rendered, from a resolved block\n // only. Otherwise a block's top-level graph may be the stored nodes, still\n // holding templates, so the exporter's rendering is read instead — node by\n // node, and a node that still holds a template token is left out.\n if (entry.resolved === true) {\n const graph = entry.schema?.graph\n if (graph && typeof graph === 'object') out.schema = graph\n } else if (template) {\n // The rendering; without one, the block's own graph — both node by node.\n const top = entry.schema?.graph\n const source = Array.isArray(rendered?.schema?.graph)\n ? rendered.schema.graph\n : ldNodes(top)\n const nodes = source.filter((node) => !holdsToken(node, template))\n if (nodes.length) out.schema = { '@context': 'https://schema.org', '@graph': nodes }\n }\n return out\n}\n\n/** A JSON-LD value's nodes: a @graph object's graph, a node array, or one node. */\nfunction ldNodes(value: unknown): unknown[] {\n if (!value || typeof value !== 'object') return []\n if (Array.isArray(value)) return value\n const graph = (value as { '@graph'?: unknown })['@graph']\n return Array.isArray(graph) ? graph : [value]\n}\n\n/** Whether any string anywhere in a JSON value holds a template token. */\nfunction holdsToken(value: unknown, template: RegExp): boolean {\n if (typeof value === 'string') return hasToken(value, template)\n if (Array.isArray(value)) return value.some((v) => holdsToken(v, template))\n if (value && typeof value === 'object') return Object.values(value).some((v) => holdsToken(v, template))\n return false\n}\n","// @contentrain/emitter-astro — the open half of the migration pipeline.\n//\n// A ProjectIR (route model, layout families, component variants, query\n// bindings, design tokens) plus prepared content goes in; a complete Astro\n// project comes out as a pure file map. The analysis that *produces* a good\n// ProjectIR is hard and lives elsewhere; rendering one is deliberately\n// boring — which is exactly why it can be open, portable, and replaceable\n// by community emitters for other frameworks.\n\nimport type { RouteModel } from '@contentrain/types'\nimport type { EmitInput, EmitPost, EmitResult, RawRedirect } from './types.js'\nimport { DEFAULT_COLLECTION } from './types.js'\nimport { scaffoldFiles } from './scaffold.js'\nimport { imagesEnabled } from './images.js'\nimport { componentMarkers, familyFiles } from './layouts.js'\nimport { collectionItems, routeFiles, sharedCollectionPage } from './pages.js'\nimport { componentFiles, isRuntimeImplemented } from './components.js'\nimport { chromeComponents } from './chrome.js'\nimport { SEO_COMPONENT } from './seo.js'\nimport { entryPath, withAlternates } from './alternates.js'\nimport { UI_STRINGS_DIR, uiStringsDir } from './ui-strings.js'\nimport { wrapLegacyCss } from './css.js'\nimport { stableJson, patternToPagePath } from './util.js'\nimport { noindexPaths } from './noindex.js'\nimport { publishedContent, withheldTarget, withheldWarnings } from './published.js'\nimport { astroRedirectsConfig, builtAddresses, HOST_RULE_LIMIT, hostRedirectFiles, planRedirects } from './redirects.js'\nimport type { ManualRedirect } from './redirects.js'\nimport { FEED_PATH, FEED_REDIRECT_FROM, archiveFeedEndpoint, archiveFeedFile, archiveFeedSources, feedEndpoint, linkSources, llmsEndpoint, type LinkSource } from './feed.js'\n\n/**\n * A supplied trail the build will not print — the same rule as \\`validTrail\\`\n * in the emitted runtime, which a test holds this to.\n */\nfunction badTrail(trail: EmitPost['breadcrumbs']): boolean {\n return trail !== undefined && !(trail.length > 0 && trail.every((c) =>\n typeof c?.name === 'string' && c.name.trim() !== '' && typeof c.path === 'string' && !c.path.includes(String.fromCharCode(92)) && ![...c.path].some((char) => char.charCodeAt(0) <= 32) && /^\\/(?!\\/)/.test(c.path)))\n}\n\n/**\n * A source JSON-LD value the Seo component cannot print (see\n * `sourceStructuredData`): not an object, or no node in it names a type.\n */\nfunction badSchema(schema: unknown): boolean {\n if (schema === undefined) return false\n if (!schema || typeof schema !== 'object') return true\n const graph = (schema as { '@graph'?: unknown })['@graph']\n const nodes: unknown[] = Array.isArray(schema) ? schema : Array.isArray(graph) ? graph : [schema]\n return !nodes.some((n) => {\n const type = n && typeof n === 'object' && !Array.isArray(n) ? (n as Record<string, unknown>)['@type'] : undefined\n return (Array.isArray(type) ? type : [type]).some((t) => typeof t === 'string')\n })\n}\n\n/** Redirect config sorted by `from`, the order astro.config and the host files list it in. */\nfunction sortedConfig<T>(config: Record<string, T>): Record<string, T> {\n return Object.fromEntries(Object.entries(config).toSorted(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))\n}\n\n/** A redirect's from as its config key: percent-encoding decoded, as planRedirects writes it. */\nfunction decodedPath(from: string): string {\n try {\n return decodeURI(from)\n } catch {\n return from\n }\n}\n\nexport function emitAstroProject(input: EmitInput): EmitResult {\n const { ir } = input\n const warnings: string[] = []\n const files: Record<string, string> = {}\n const add = (batch: Record<string, string>) => {\n for (const [path, content] of Object.entries(batch)) {\n if (files[path] !== undefined && files[path] !== content) {\n // Pages are reported by the route loop below, which knows *which two\n // routes* collided and what is lost. Saying \"duplicate file\" here as\n // well would bury that under the vaguer message.\n if (!path.startsWith('src/pages/')) {\n warnings.push(`duplicate file with different content: ${path} — keeping the first`)\n }\n continue\n }\n files[path] = content\n }\n }\n\n // Only published entries are built: drafts, entries in review and ones\n // scheduled for later are held back before anything reads the content.\n let now = Date.now()\n if (input.options?.now !== undefined) {\n const at = Date.parse(input.options.now)\n if (Number.isNaN(at)) warnings.push(`options.now \"${input.options.now}\" is not a date — publish_at is compared with the time of this emit`)\n else now = at\n }\n const published = publishedContent(ir.routes, input.content ?? {}, now, ir.site.url, input.options?.requireStatus === true)\n const content = published.content\n warnings.push(...withheldWarnings(published.withheld, published.links))\n // A rule to a held-back entry would redirect to a page the build does not\n // have. It goes back to the producer with the rest of the manual rules.\n const withheldManual: ManualRedirect[] = []\n const toPublished = (rules: RawRedirect[] | undefined) => rules?.filter((redirect) => {\n const target = withheldTarget(redirect.to, published.gone, ir.site.url)\n if (target) withheldManual.push({ redirect, reason: `to ${target}, an entry that is not published — the build has no page there` })\n return !target\n })\n const siteRedirects = toPublished(input.redirects)\n const hostOnlyRedirects = toPublished(input.hostRedirects)\n if (withheldManual.length) {\n warnings.push(`unpublished: ${withheldManual.length} redirects point at unpublished entries — not written (EmitResult.redirects.manual has each)`)\n }\n\n // The site's own redirect rules. Only plain ones become config; the rest,\n // and any on an address this site builds a page at, go back to the producer.\n const siteLang = ir.site.locales?.[0] ?? 'en'\n // The feed and llms.txt name every page by its absolute address.\n const linkable = ir.site.url ? linkSources(ir.routes, siteLang) : []\n const feedSource = input.options?.feed !== false ? linkable.find((s) => s.collection === DEFAULT_COLLECTION) : undefined\n const llmsOn = input.options?.llms !== false && linkable.length > 0\n // Every archive page (a category, a tag, an author) gets its feed, as in WordPress.\n const archiveFeeds = feedSource ? archiveFeedSources(ir.routes, siteLang) : []\n const archiveFeedRoutes = new Set(archiveFeeds.map((a) => a.routeId))\n if (!ir.site.url && (input.options?.feed !== false || input.options?.llms !== false)) {\n warnings.push('site.url is empty — no RSS feed and no llms.txt are built; both name pages by absolute address')\n }\n const redirectPlan = siteRedirects ? planRedirects(siteRedirects, builtAddresses(ir.routes, content)) : undefined\n if (redirectPlan?.manual.length) {\n warnings.push(`redirects: ${redirectPlan.manual.length} of ${siteRedirects!.length} rules not written to astro.config — set them up at the host (EmitResult.redirects.manual has each with its reason)`)\n }\n\n // Per-page SEO is on unless the producer owns those tags itself.\n const seo = input.options?.seo !== false\n const noindex = noindexPaths(ir.routes, content)\n // Readers subscribed to WordPress's /feed/ follow a 301 to the feed the\n // build writes — a real one from the host's redirect file; a static\n // redirect page, which feed readers do not follow, is only the fallback.\n // A rule the source itself holds for /feed/ wins.\n // The feed addresses WordPress served: /feed/ and each archive page's\n // <archive>/feed/, 301 to the feeds the build writes. They pass the same\n // checks as the source's rules — never over a page the site builds, never\n // a case twin of one — and a source rule for the same address wins.\n const built = builtAddresses(ir.routes, content)\n const sourceConfig = redirectPlan?.config ?? {}\n const claimed = new Set(Object.keys(sourceConfig).map((from) => from.replace(/\\/+$/, '').toLowerCase()))\n const feedRules: RawRedirect[] = []\n const addFeedRule = (from: string, to: string) => {\n const key = from.replace(/\\/+$/, '').toLowerCase()\n if (claimed.has(key)) return\n claimed.add(key)\n feedRules.push({ from, to, status: 301 })\n }\n if (feedSource) addFeedRule(FEED_REDIRECT_FROM, FEED_PATH)\n for (const source of archiveFeeds) {\n for (const page of content.queries?.[source.query] ?? []) {\n const address = entryPath(source.pattern, page.params)\n if (address) addFeedRule(`${address}feed/`, `${address}feed.xml`)\n }\n }\n const feedPlan = planRedirects(feedRules, built)\n if (feedPlan.manual.length) {\n warnings.push(`feed: ${feedPlan.manual.length} feed redirects not written — ${feedPlan.manual.map((m) => `${m.redirect.from} (${m.reason})`).join('; ')}`)\n }\n const redirectConfig = { ...sourceConfig, ...feedPlan.config }\n const hasRedirects = Object.keys(redirectConfig).length > 0\n add(scaffoldFiles(ir, input.options ?? {}, noindex, hasRedirects ? astroRedirectsConfig(sortedConfig(redirectConfig)) : null, input.runtime))\n // Host-only rules: the same checks, then an address the site's own rules or\n // the feeds already redirect is theirs. Never in astro.config.\n const hostOnlyPlan = hostOnlyRedirects ? planRedirects(hostOnlyRedirects, built, { hostOnly: true }) : undefined\n const hostOnlyConfig: typeof redirectConfig = {}\n const hostOnlyRule = new Map<string, RawRedirect>()\n const hostOnlyManual: ManualRedirect[] = [...(hostOnlyPlan?.manual ?? [])]\n if (hostOnlyPlan) {\n const byFrom = new Map(hostOnlyPlan.written.map((rule) => [decodedPath(rule.from), rule]))\n for (const [from, rule] of Object.entries(hostOnlyPlan.config)) {\n const redirect = byFrom.get(from)!\n const key = from.replace(/\\/+$/, '').toLowerCase()\n if (claimed.has(key)) {\n hostOnlyManual.push({ redirect, reason: `another rule already redirects ${from}` })\n continue\n }\n claimed.add(key)\n hostOnlyConfig[from] = rule\n hostOnlyRule.set(from, redirect)\n }\n }\n // Host files have a rule limit and keep the first rules: the source's own\n // come first, the feeds' after, so an overflow drops a feed redirect.\n // Among the feeds, /feed/ first, then the archives in their data order.\n const feedOrder = new Map(feedRules.map((rule, index) => [decodedPath(rule.from), index]))\n const feedKeys = Object.keys(feedPlan.config).toSorted((a, b) => (feedOrder.get(a) ?? 0) - (feedOrder.get(b) ?? 0))\n // Host-only rules come last: past a host's limit they are the ones left out.\n const hostOrder = { ...sortedConfig(sourceConfig), ...Object.fromEntries(feedKeys.map((key) => [key, feedPlan.config[key]!])), ...sortedConfig(hostOnlyConfig) }\n const hostRedirects = Object.keys(hostOrder).length ? hostRedirectFiles(hostOrder, input.options?.redirectHost) : undefined\n // A host-only rule the host file cannot hold has no meta-refresh page behind\n // it — it would not be served at all — so it is set up by hand.\n const hostOnly = (from: string) => hostOnlyRule.has(from)\n for (const from of hostRedirects?.skipped ?? []) {\n if (hostOnly(from)) hostOnlyManual.push({ redirect: hostOnlyRule.get(from)!, reason: 'from contains \":\" or \"*\", which host redirect files read as a pattern' })\n }\n // Past a limited file: with its host named nothing serves the rule, so it is\n // manual; with no host named the Netlify file still holds it, so it is\n // written, and named in host_over_limit for the Vercel file it is missing from.\n const namedHost = input.options?.redirectHost\n const hostOnlyOver = (hostRedirects?.over_limit ?? []).filter(hostOnly)\n if (namedHost !== undefined) {\n for (const from of hostOnlyOver) hostOnlyManual.push({ redirect: hostOnlyRule.get(from)!, reason: `left out of ${hostRedirects!.written.join(', ')} at its rule limit (${HOST_RULE_LIMIT / 2} rules) — a host-only rule has no meta-refresh page to fall back on` })\n }\n else if (hostOnlyOver.length) {\n warnings.push(`hostRedirects: ${hostOnlyOver.length} host-only rules not written to vercel.json at its rule limit (${HOST_RULE_LIMIT / 2} rules) — public/_redirects (Netlify) has them; on Vercel they are not served. Name the host (options.redirectHost) to have them reported as manual`)\n }\n const unservedHostOnly = new Set(hostOnlyManual.map((m) => m.redirect))\n if (hostOnlyManual.length) {\n warnings.push(`hostRedirects: ${hostOnlyManual.length} of ${hostOnlyRedirects!.length} host-only rules not written — set them up at the host (EmitResult.redirects.manual has each with its reason)`)\n }\n if (hostRedirects) {\n add(hostRedirects.files)\n const overLimit = hostRedirects.over_limit.filter((from) => !hostOnly(from))\n const skippedPatterns = hostRedirects.skipped.filter((from) => !hostOnly(from))\n if (overLimit.length) {\n const limitedFiles = hostRedirects.written.filter((f) => !f.includes('(netlify)')).join(', ')\n const feedOver = overLimit.filter((from) => from in feedPlan.config).length\n const which = feedOver ? ` (${feedOver} of them feed redirects — /feed/ and archive feeds come after the site's own rules)` : ''\n warnings.push(`redirects: ${overLimit.length} rules not written to ${limitedFiles}${which} — ${HOST_RULE_LIMIT / 2} rules (each path with and without its slash) fill Cloudflare Pages' 2,000 static and Vercel's 2,048 redirect limit. They are served by the meta-refresh fallback only; move them to the host's dynamic rules for a real status`)\n }\n if (skippedPatterns.length) {\n warnings.push(`redirects: ${skippedPatterns.length} rules contain \":\" or \"*\", which host redirect files read as patterns — served by the meta-refresh fallback only: ${skippedPatterns.join(', ')}`)\n }\n }\n\n // Without trailing slashes the build writes /hello.html; Vercel serves it\n // at /hello only with cleanUrls (Netlify and Cloudflare Pages do by default).\n if (input.options?.trailingSlash === false && (input.options.redirectHost ?? 'vercel') === 'vercel') {\n const vercel = files['vercel.json'] ? JSON.parse(files['vercel.json']) as Record<string, unknown> : {}\n files['vercel.json'] = `${JSON.stringify({ cleanUrls: true, trailingSlash: false, ...vercel }, null, 2)}\\n`\n }\n\n if (noindex.length && !seo) {\n warnings.push(`${noindex.length} noindex pages: options.seo is false, so no robots meta is emitted — the producer's head must carry it (they are still left out of the sitemap)`)\n }\n if (seo) {\n add({ 'src/components/Seo.astro': SEO_COMPONENT })\n if (!ir.site.url) {\n warnings.push('site.url is empty — canonical links and absolute og:url/og:image are omitted; set it so search engines and share cards resolve')\n }\n }\n if (input.options?.sitemap !== false && !ir.site.url) {\n warnings.push('site.url is empty — no sitemap is generated and robots.txt names none; set it so search engines can find every page')\n }\n\n const familiesById = new Map(ir.families.map((f) => [f.id, f]))\n const lang = ir.site.locales?.[0] ?? 'en'\n // Header/footer regions first: families that share one share the component.\n const chrome = chromeComponents(ir.families)\n add(chrome.files)\n warnings.push(...chrome.warnings)\n const definitions = new Map((ir.components ?? []).map((c) => [c.id, c]))\n // A component marker can sit inside a post's own body (a form in a page's\n // content). The layout that renders that body must mount it, so collect the\n // ids per family from the collections its routes render.\n const bodyMarkersByFamily = new Map<string, Set<string>>()\n for (const route of ir.routes) {\n if (route.kind !== 'single' && route.collection === undefined) continue\n const posts = collectionItems(content, route.collection ?? DEFAULT_COLLECTION)\n const ids = bodyMarkersByFamily.get(route.family) ?? new Set<string>()\n for (const post of posts) for (const id of componentMarkers(post.body)) ids.add(id)\n if (ids.size) bodyMarkersByFamily.set(route.family, ids)\n }\n // A placement may bind its region to a query. Validated before the families\n // are emitted, because the layout emits an import for the query's data file:\n // an import of a file the producer never supplied is a build error with no\n // explanation in it. A binding that does not check out is dropped here, so\n // the region stays the cloned markup the warning says it stays.\n const boundQueries = new Set<string>()\n const queryBound = new Set<string>()\n for (const family of ir.families) {\n for (const placement of family.components ?? []) {\n if (!placement.query) continue\n const pages = content.queries?.[placement.query]\n if (!pages) {\n warnings.push(\n `family ${family.id}: placement \"${placement.component}\" binds query \"${placement.query}\", `\n + 'which is not in input.content.queries — binding dropped, region left as cloned markup',\n )\n continue\n }\n if (pages.length !== 1) {\n warnings.push(\n `family ${family.id}: placement \"${placement.component}\" binds query \"${placement.query}\", `\n + `which has ${pages.length} result sets — a page region has no route parameter to choose between `\n + 'them, so the build stops rather than rendering one of them everywhere',\n )\n } else if (!pages[0]!.item_template && !pages[0]!.sections?.length) {\n warnings.push(\n `family ${family.id}: placement \"${placement.component}\" binds query \"${placement.query}\", `\n + 'which has no item_template or sections — plain fallback list rendered (not fidelity)',\n )\n }\n // The layout imports this file. A route that renders the same query also\n // writes it, but a region is not a route: bound without one, nothing else\n // would, and the import would resolve to nothing.\n add({ [`src/data/queries/${placement.query}.json`]: stableJson(pages) })\n boundQueries.add(placement.query)\n queryBound.add(placement.component)\n }\n }\n\n for (const family of ir.families) {\n const fam = familyFiles(family, lang, chrome.byFamily.get(family.id), definitions, bodyMarkersByFamily.get(family.id) ?? [], { seo, siteName: ir.site.title, boundQueries, images: imagesEnabled(input.options ?? {}), feedSite: feedSource ? ir.site.url : undefined, trailingSlash: input.options?.trailingSlash })\n add(fam.files)\n warnings.push(...fam.warnings)\n }\n\n for (const css of input.css ?? []) {\n const wrapped = wrapLegacyCss(css.path, css.content)\n if (wrapped.warning) warnings.push(wrapped.warning)\n const base = css.path.split('/').pop() ?? css.path\n add({ [`public/styles/legacy/${base}`]: wrapped.content })\n }\n const providedCss = new Set((input.css ?? []).map((c) => c.path.split('/').pop() ?? c.path))\n const missingCss = (refs: string[] | undefined, owner: string) => {\n for (const ref of refs ?? []) {\n if (!providedCss.has(ref.split('/').pop() ?? ref)) {\n warnings.push(`${owner}: css file \"${ref}\" not provided in input.css`)\n }\n }\n }\n for (const family of ir.families) missingCss(family.css.files, `family ${family.id}`)\n const allPosts = [\n ...(content.posts ?? []),\n ...Object.values(content.collections ?? {}).flat(),\n ]\n for (const post of allPosts) missingCss(post.css, `post ${post.slug}`)\n for (const [queryId, queryPages] of Object.entries(content.queries ?? {})) {\n for (const qp of queryPages) missingCss(qp.css, `query ${queryId}`)\n }\n\n // Two routes can want the same Astro page. WordPress serves posts and pages\n // from the same root and tells them apart in the database; a static\n // generator cannot, so `/:slug` for posts and `/:slug` for pages resolve to\n // one file. Left to `add`, the second route's pages simply vanish — every\n // page of it, with a warning that names a file rather than a route, so the\n // producer learns a file was duplicated and not that a section of the site\n // is missing.\n //\n // The pages are unrecoverable at this point either way: the emitter cannot\n // know which route should own the path. What it can do is refuse to produce\n // a site that is quietly wrong.\n // A breadcrumb trail with a bad crumb prints nothing at build; say so here.\n const trailOwners: Array<[string, EmitPost[] | undefined]> = [\n ...Object.entries(content.collections ?? {}),\n ...(content.posts ? [[DEFAULT_COLLECTION, content.posts] as [string, EmitPost[]]] : []),\n ]\n for (const [name, posts] of trailOwners) {\n const bad = (posts ?? []).filter((p) => badTrail(p.breadcrumbs)).length\n if (bad) warnings.push(`collection ${name}: ${bad} breadcrumb trails dropped — every crumb needs a name and a site-root-relative path`)\n const badLd = (posts ?? []).filter((p) => badSchema(p.schema)).length\n if (badLd) warnings.push(`collection ${name}: ${badLd} schema values are not JSON-LD objects — those pages print the generated structured data instead`)\n }\n for (const [queryId, queryPages] of Object.entries(content.queries ?? {})) {\n const bad = queryPages.filter((qp) => badTrail(qp.breadcrumbs)).length\n if (bad) warnings.push(`query ${queryId}: ${bad} breadcrumb trails dropped — every crumb needs a name and a site-root-relative path`)\n const badLd = queryPages.filter((qp) => badSchema(qp.schema)).length\n if (badLd) warnings.push(`query ${queryId}: ${badLd} schema values are not JSON-LD objects — those pages print the generated structured data instead`)\n }\n\n // hreflang alternates ride on the entry data, so they are attached before the\n // routes write it. The Seo component renders them; without it the producer\n // owns the head, alternates included.\n let routeContent = content\n if (seo) {\n const alternates = withAlternates(ir.routes, routeContent, lang)\n routeContent = alternates.content\n warnings.push(...alternates.warnings)\n }\n const routeGroups = new Map<string, RouteModel[]>()\n for (const route of ir.routes) {\n const path = patternToPagePath(route.pattern)\n if (path) routeGroups.set(path, [...(routeGroups.get(path) ?? []), route])\n }\n const sharedPages = new Map<string, string>()\n for (const [path, group] of routeGroups) {\n if (!group.every(route => familiesById.has(route.family))) continue\n const page = sharedCollectionPage(group, routeContent, lang, seo)\n if (page) sharedPages.set(`src/pages/${path}`, page)\n }\n const pageOwner = new Map<string, RouteModel>()\n const collisions: { path: string, first: RouteModel, second: RouteModel }[] = []\n for (const route of ir.routes) {\n const result = routeFiles(route, familiesById.get(route.family), routeContent, lang, seo, archiveFeedRoutes.has(route.id))\n for (const path of sharedPages.keys()) delete result.files[path]\n for (const path of Object.keys(result.files)) {\n if (!path.startsWith('src/pages/')) continue\n const owner = pageOwner.get(path)\n if (owner === undefined) {\n pageOwner.set(path, route)\n continue\n }\n // Any second claim on a page path is a collision, even when the two\n // routes would render the same thing: Astro serves one file per path, so\n // one of the two routes does not exist in the built site. Comparing the\n // emitted bytes instead would be a weaker test that happens to agree —\n // the page carries its route id in a comment, so two routes never\n // produce identical output anyway.\n collisions.push({ path, first: owner, second: route })\n }\n add(result.files)\n warnings.push(...result.warnings)\n }\n\n for (const [path, page] of sharedPages) files[path] = page\n\n for (const { path, first, second } of collisions) {\n warnings.push(\n `route ${second.id}: pattern \"${second.pattern}\" emits the same Astro page as route ${first.id} `\n + `(\"${first.pattern}\") — ${path}. Every page of \"${second.id}\" would be dropped, so the page now `\n + `fails the build instead. Give one route a distinct pattern, or expand the narrower one into literal routes.`,\n )\n files[path] = collisionPage(path, first, second)\n }\n\n const stringsDir = uiStringsDir(input.options?.uiStrings?.dir)\n if (stringsDir === undefined) {\n warnings.push(`options.uiStrings.dir \"${input.options?.uiStrings?.dir}\" is not a project-relative directory — using ${UI_STRINGS_DIR}`)\n }\n const components = componentFiles(ir.components ?? [], input.runtime, queryBound, stringsDir ?? UI_STRINGS_DIR)\n add(components.files)\n warnings.push(...components.warnings)\n // Comments and forms speak the page's language only if the dictionary has\n // it. The build reports a missing file too; saying it here puts it in the\n // same report as everything else the producer has to act on.\n if (components.files['src/lib/ui-strings.ts']) {\n const pageLangs = [...new Set([lang, ...ir.routes.map((r) => r.locale).filter((l): l is string => Boolean(l))])]\n .filter((l) => !l.toLowerCase().startsWith('en'))\n const declared = input.options?.uiStrings?.locales\n const uncovered = declared ? pageLangs.filter((l) => !declared.includes(l) && !declared.includes(l.split('-')[0]!)) : pageLangs\n if (uncovered.length) {\n warnings.push(\n `ui-strings: no dictionary ${declared ? 'declared' : 'declared in options.uiStrings.locales'} for ${uncovered.join(', ')} — `\n + `comments and forms on those pages show English text unless ${stringsDir ?? UI_STRINGS_DIR}/{locale}.json exists at build`,\n )\n }\n }\n\n // A mounted comments thread is keyed by the post's entry address; a post\n // without one renders no thread. Say so once per collection, not per page.\n const commentsMounted = (ir.components ?? []).some((c) => isRuntimeImplemented(c, input.runtime) && c.type === 'comments')\n if (commentsMounted) {\n const collections: Record<string, EmitPost[]> = { ...content.collections }\n if (content.posts && !collections[DEFAULT_COLLECTION]) collections[DEFAULT_COLLECTION] = content.posts\n for (const [name, posts] of Object.entries(collections)) {\n const unbound = posts.filter((p) => !p.entry).length\n if (unbound) warnings.push(`collection ${name}: ${unbound} of ${posts.length} posts carry no entry address — the comments component renders nothing on those pages`)\n }\n }\n\n // Endpoints over the data files the routes wrote; a collection whose file\n // was not written (its route was dropped) has no pages to list.\n const feedInput = { ir, siteLocale: lang, description: input.options?.siteDescription, trailingSlash: input.options?.trailingSlash !== false }\n const written = (source: LinkSource) => files[`src/data/${source.collection}.json`] !== undefined\n if (feedSource) {\n if (written(feedSource)) {\n add({ 'src/pages/feed.xml.ts': feedEndpoint(feedSource, feedInput) })\n } else {\n warnings.push(`feed: no posts data file was written, so no feed is built — the head's feed link points at ${FEED_PATH}, which 404s`)\n }\n }\n for (const source of archiveFeeds) {\n const file = archiveFeedFile(source.pattern)\n if (file && feedSource && files[`src/data/queries/${source.query}.json`] !== undefined) {\n add({ [file]: archiveFeedEndpoint(source, feedSource.pattern, feedInput) })\n }\n }\n if (llmsOn) {\n const listed = linkable.filter(written)\n if (listed.length) add({ 'src/pages/llms.txt.ts': llmsEndpoint(listed, feedInput) })\n }\n\n return {\n files,\n warnings,\n ...(redirectPlan || hostOnlyPlan\n ? {\n redirects: {\n written: [...(redirectPlan?.written ?? []), ...[...hostOnlyRule.values()].filter((rule) => !unservedHostOnly.has(rule))],\n manual: [...(redirectPlan?.manual ?? []), ...hostOnlyManual, ...withheldManual],\n host_files: hostRedirects?.written ?? [],\n // A host-only rule past a named host's limit is in `manual` instead: nothing serves it.\n host_over_limit: (hostRedirects?.over_limit ?? []).filter((from) => !hostOnly(from) || namedHost === undefined),\n },\n }\n : {}),\n ...(published.withheld.length || published.links.length ? { withheld: { entries: published.withheld, links: published.links } } : {}),\n }\n}\n\n/**\n * Replaces a page two routes both claim. The emitter cannot choose between\n * them, so the page states the conflict and throws — `astro build` stops with\n * the two route ids in the message, instead of shipping a site that is missing\n * a section nobody was told about.\n */\nfunction collisionPage(path: string, first: RouteModel, second: RouteModel): string {\n const message = `Route collision: \"${first.id}\" (${first.pattern}) and \"${second.id}\" (${second.pattern}) `\n + `both resolve to ${path}. A static build cannot serve both from one file — give one route a distinct `\n + `pattern, or expand the narrower one into literal routes.`\n const thrown = `throw new Error(${JSON.stringify(message)})`\n // On a dynamic path the throw has to live in `getStaticPaths`. Astro collects\n // paths before it renders anything, so a throw in the frontmatter would never\n // run — the build would fail on the missing `getStaticPaths` instead, with\n // Astro's generic message in place of the one that names the two routes.\n // On a static path the opposite holds: exporting `getStaticPaths` at all is\n // itself an error (\"only supported in dynamic routes\").\n return path.includes('[')\n ? `---\\nexport function getStaticPaths() {\\n ${thrown}\\n}\\n---\\n`\n : `---\\n${thrown}\\n---\\n`\n}\n\n/** Write an EmitResult to disk. Separate from emit so the core stays pure. */\nexport async function writeEmit(result: EmitResult, dir: string): Promise<void> {\n const { mkdir, writeFile } = await import('node:fs/promises')\n const { dirname, join } = await import('node:path')\n const dirs = new Set(Object.keys(result.files).map((p) => dirname(join(dir, p))))\n await Promise.all([...dirs].map((d) => mkdir(d, { recursive: true })))\n await Promise.all(\n Object.entries(result.files).map(([path, content]) => writeFile(join(dir, path), content, 'utf8')),\n )\n}\n\nexport type {\n EmitInput,\n EmitResult,\n EmitContent,\n EmitPost,\n QueryPage,\n EmitCssFile,\n EmitOptions,\n EmitTermRef,\n AuthorProfile,\n Breadcrumb,\n ImageMeta,\n SocialOverride,\n TwitterOverride,\n RuntimeBinding,\n RawRedirect,\n EntrySourceRef,\n WithheldEntry,\n WithheldLink,\n} from './types.js'\nexport { wrapLegacyCss } from './css.js'\nexport { FEED_ITEMS, FEED_PATH, LLMS_LINKS, linkSources, rewriteFeedLinks } from './feed.js'\nexport type { FeedLinkResult, LinkSource } from './feed.js'\nexport { pascalCase, patternToPagePath, stableJson } from './util.js'\nexport { componentMarkers } from './layouts.js'\nexport type { MountRef } from './layouts.js'\nexport { isRuntimeImplemented, RUNTIME_IMPLEMENTED } from './components.js'\nexport { bodySeoLeaks, publisherIdOf, stripSeoTags, websiteIdOf, SEO_COMPONENT } from './seo.js'\nexport { seoFromRawEntry } from './seo-entry.js'\nexport type { SeoFields, SeoFromRawOptions } from './seo-entry.js'\nexport { entryPath, withAlternates } from './alternates.js'\nexport type { Alternate, AlternatesResult } from './alternates.js'\nexport type { StripResult } from './seo.js'\nexport { EMBED_TS } from './embed.js'\nexport { UI_STRING_DEFAULTS, UI_STRINGS_DIR, UI_STRINGS_MODEL } from './ui-strings.js'\nexport type { UiStringKey } from './ui-strings.js'\nexport { checkBalance, balanceWarning } from './balance.js'\nexport { noindexPaths } from './noindex.js'\nexport { holdReason, publishedContent } from './published.js'\nexport type { PublishedContent } from './published.js'\nexport { astroRedirectsConfig, builtAddresses, hostRedirectFiles, planRedirects, REDIRECT_STATUSES } from './redirects.js'\nexport type { HostRedirectFiles, ManualRedirect, RedirectHost, RedirectPlan, RedirectStatus } from './redirects.js'\nexport type { BalanceReport } from './balance.js'\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["addressKey","addressKey"],"sources":["../src/types.ts","../src/images.ts","../src/util.ts","../src/pages.ts","../src/alternates.ts","../src/noindex.ts","../src/scaffold.ts","../src/balance.ts","../src/seo.ts","../src/feed.ts","../src/layouts.ts","../src/embed.ts","../src/ui-strings.ts","../src/components.ts","../src/chrome.ts","../src/css.ts","../src/redirects.ts","../src/published.ts","../src/seo-entry.ts","../src/index.ts"],"sourcesContent":["import type { ContentStatus, EntrySourceRef, ProjectIR, RawRedirect, RuntimeBinding } from '@contentrain/types'\n\n// ─── Emit input ───\n\n/**\n * One piece of content ready to render: body already HTML, assets already\n * rewritten to local paths, dates already formatted in the site's observed\n * formats. The emitter renders; it does not fetch, localize, or format —\n * those are producer concerns, and keeping them out is what makes this\n * package portable.\n */\n/** A term as a list card prints it: its name and the archive it links to. */\nexport interface EmitTermRef {\n name: string\n link?: string\n}\n\nexport interface EmitPost {\n slug: string\n title: string\n /** Rendered, asset-rewritten HTML body. */\n body: string\n /**\n * Route parameters for this post beyond `slug` — the date parts of a\n * `/%year%/%monthnum%/%day%/%postname%/` permalink, a post id, anything the\n * route pattern names. Without them every post but the template one would be\n * generated at the wrong address.\n */\n params?: Record<string, string>\n /**\n * Stylesheets only this page loads (page-builder sites emit CSS per page).\n * Family-level stylesheets stay on `LayoutFamily.css.files`.\n */\n css?: string[]\n /** The site's date-format catalog applied to this post's date (index-aligned with `date{n}` marks). */\n dates?: string[]\n author?: string\n /**\n * The author's page — their archive on the site, absolute or site-root-\n * relative. The Article's author gets it as `url`, the address the\n * archive's ProfilePage names its Person at, so the two are one person.\n */\n author_url?: string\n author_first?: string\n author_last?: string\n /**\n * Term names, or `{ name, link }` objects for themes whose term lists link\n * each term — a repeat block then reads `item_name` / `item_link`. The\n * `term{n}` and `terms` marks print the name either way.\n */\n terms?: Array<string | EmitTermRef>\n /** Local featured-image file names (largest first). */\n featured?: string[]\n excerpt?: string\n /** Excerpt as source HTML, for themes whose cards keep links and formatting. */\n excerpt_html?: string\n /**\n * Meta description. Falls back to `excerpt` with markup stripped — a card\n * and a description tag want the same sentence, so most producers set neither.\n */\n description?: string\n /**\n * Social image, absolute or site-root-relative. `featured` holds bare file\n * names whose serving path only the producer knows, so a crawler-usable\n * `og:image` needs this; a `featured` entry that is already a path is used.\n */\n image?: string\n /** What `image` is, for the `og:image:*` tags. Describes `image` only — ignored without it. */\n image_meta?: ImageMeta\n /** Canonical override — for a page that should point somewhere else. Default: this page's own address. */\n canonical?: string\n /**\n * The document title (`<title>`, and the default og/twitter title) when the\n * source's SEO plugin composed one apart from the entry's — Yoast's\n * \"Post – Site\". `title` stays the entry's own name: its marks, the Article\n * headline, the last breadcrumb. Default: `title`.\n */\n seo_title?: string\n /** Open Graph text and image the source set for this page, over the derived ones. */\n open_graph?: SocialOverride\n /** Twitter card values the source set for this page. Unset values follow Open Graph. */\n twitter?: TwitterOverride\n /**\n * The page's JSON-LD as the source's SEO plugin rendered it — a `@graph`\n * object, an array of nodes, or one node. Printed instead of the graph the\n * emitter would build, so FAQ, HowTo, Product and whatever else the plugin\n * described survive. The migrated site keeps the source addresses, so the\n * graph's `@id`s and URLs stay true; media URLs in it are printed as given\n * and are the producer's to rewrite. A WebSite `SearchAction` is dropped (a\n * static site has no `?s=` search), as is a copy of the head's WebSite node.\n */\n schema?: unknown\n /** ISO 8601, for Article structured data. `dates` holds display strings, which schema.org cannot read. */\n published_at?: string\n modified_at?: string\n /** Every author, for repeat blocks — `author` stays the first one. */\n authors?: string[]\n /** Producer-supplied extra marks, merged last. */\n marks?: Record<string, unknown>\n /** Locale of this entry, on a multilingual site — overrides the route's. */\n locale?: string\n /**\n * The trail to this page for BreadcrumbList — home first, the page itself\n * NOT included: the emitter appends it with the address it built, so the\n * last crumb cannot disagree with the page. Each path is site-root-relative.\n * Without a trail no BreadcrumbList is printed; a trail with an invalid\n * crumb is dropped with a warning.\n */\n breadcrumbs?: Breadcrumb[]\n /**\n * Content-store address of this post (model, entry id, locale) — what a\n * mounted `comments` component addresses its thread by. Only the tool that\n * wrote the content store knows it; without it the comments region mounts\n * as a placeholder.\n */\n entry?: EntrySourceRef\n /**\n * The source page was `noindex` (e.g. Yoast `robots.index`). The page gets\n * `<meta name=\"robots\" content=\"noindex\">` and is left out of the sitemap —\n * a migration must not make indexable what the site kept out of search.\n */\n noindex?: boolean\n /** The source page was `nofollow`: `<meta name=\"robots\" content=\"nofollow\">`. */\n nofollow?: boolean\n /**\n * The entry's workflow status in the content store (`EntryMeta.status`).\n * Only `published` is built; unset reads as published unless\n * `EmitOptions.requireStatus` is set. Any other status\n * keeps the entry out of the site — no page, sitemap line, feed item,\n * llms.txt link or list card — and `EmitResult.withheld` names it.\n */\n status?: ContentStatus\n /**\n * When a published entry goes live (`EntryMeta.publish_at`, ISO 8601) — a\n * WordPress post scheduled for later. Until then it is held back like a\n * draft; compared with `EmitOptions.now`. A value that is not a date holds\n * the entry back too.\n */\n publish_at?: string\n /**\n * Who may read the entry (the store's `visibility` field — WordPress\n * `public`, `private`, or `password` for a password-protected post). Anything\n * but `public` is never built, whatever `status` says: no page, sitemap\n * line, feed item, list card, share card or link to it. Unset reads as public.\n */\n visibility?: 'public' | 'private' | 'password' | (string & {})\n}\n\n/** An entry the build leaves out because it is not published yet. */\nexport interface WithheldEntry {\n collection: string\n slug: string\n status?: ContentStatus\n publish_at?: string\n visibility?: string\n /**\n * `visibility`: password-protected or private · `status`: not published ·\n * `status_missing`: no status, with `EmitOptions.requireStatus` ·\n * `scheduled`: publish_at is ahead · `publish_at_invalid`: publish_at is not a date.\n */\n reason: 'visibility' | 'status' | 'status_missing' | 'scheduled' | 'publish_at_invalid'\n}\n\n/** A link on a published page to a held-back entry, kept as its text. */\nexport interface WithheldLink {\n /** The page it was on (site-root path), or `<collection>/<slug>` when no route builds it. */\n page: string\n /** The address it pointed at. */\n href: string\n}\n\n/**\n * One block of a list: a template, optional wrapper, and how many items it takes.\n *\n * Themes commonly render the newest post as a big card and the rest as a grid —\n * two different templates in two different containers. One template per list\n * cannot express that: either everything becomes a big card, or the big card is\n * baked into the chrome and page 2 shows the wrong post.\n */\nexport interface ListSection {\n /** Item markup with `@@marks@@`. */\n template: string\n /** Markup around this section's items; `<!--@@items@@-->` marks where they go. */\n wrapper?: string\n /** Items this section takes, in order. Omitted = the remaining items. */\n count?: number\n}\n\n/** One static path of a list route: its params and the items its list renders. */\nexport interface QueryPage {\n params: Record<string, string>\n items: EmitPost[]\n /**\n * Page-level marks for the list page's chrome — a term's display NAME where\n * the route parameter only carries its slug, its description, its count.\n * Without these every category page would print the template category's name.\n */\n marks?: Record<string, unknown>\n /** Stylesheets only this list page loads. */\n css?: string[]\n /**\n * Item markup with `@@mark@@` placeholders (title, date{n}, author, excerpt,\n * feat, slug), extracted from the source list. When present, list pages\n * render each item by filling the template; when absent, a plain fallback\n * list is emitted and a warning recorded — a silent fallback would read as\n * fidelity when it is not.\n *\n * Shorthand for a single-section list; `sections` takes precedence.\n */\n item_template?: string\n /** Lists whose items are not all rendered alike — a big card then a grid. */\n sections?: ListSection[]\n /** Document title for this page (`<title>`), e.g. \"Category: News – Site\". */\n title?: string\n /** Meta description for this list page. */\n description?: string\n /** Social image, absolute or site-root-relative. */\n image?: string\n /** What `image` is, for the `og:image:*` tags. Ignored without `image`. */\n image_meta?: ImageMeta\n /** Canonical override; default is this page's own address. */\n canonical?: string\n /** The trail to this list page, itself excluded — see `EmitPost.breadcrumbs`. */\n breadcrumbs?: Breadcrumb[]\n /** See `EmitPost.noindex`. */\n noindex?: boolean\n /** See `EmitPost.nofollow`. */\n nofollow?: boolean\n /** See `EmitPost.open_graph`. */\n open_graph?: SocialOverride\n /** See `EmitPost.twitter`. */\n twitter?: TwitterOverride\n /** See `EmitPost.schema`. */\n schema?: unknown\n /**\n * On an author's archive: who it is about. The page becomes a ProfilePage\n * whose main entity is this Person, at the page's own address.\n */\n profile?: AuthorProfile\n}\n\n/** An author as their archive page presents them. */\nexport interface AuthorProfile {\n name: string\n /** The author's bio. */\n description?: string\n /** Avatar or photo, absolute or site-root-relative. */\n image?: string\n /** Profiles elsewhere (social accounts, a personal site) — schema.org `sameAs`; http(s) only. */\n same_as?: string[]\n}\n\n/**\n * Share-card values a page's source set by hand. Each one left out falls back\n * to the value derived from the page. `image` is absolute or site-root-relative.\n */\nexport interface SocialOverride {\n title?: string\n description?: string\n image?: string\n}\n\nexport interface TwitterOverride extends SocialOverride {\n /** `summary` or `summary_large_image`; anything else is ignored. */\n card?: string\n}\n\n/** One step of a breadcrumb trail. */\nexport interface Breadcrumb {\n name: string\n /** Site-root-relative (`/category/news/`). */\n path: string\n}\n\n/**\n * A social image's size and type. Share cards lay out before they fetch the\n * image; with `og:image:width` / `height` the first share renders at the right\n * aspect instead of as a placeholder. The producer measured the file — the\n * emitter cannot, it never sees it.\n */\nexport interface ImageMeta {\n /** Pixels. A value that is not a positive integer is left out. */\n width?: number\n height?: number\n /** MIME type, `image/…`. Anything else is left out. */\n type?: string\n}\n\n/** Default collection name when a `single` route does not name one. */\nexport const DEFAULT_COLLECTION = 'posts'\n\nexport interface EmitContent {\n /** The default (`posts`) collection — shorthand for `collections.posts`. */\n posts?: EmitPost[]\n /**\n * Content per collection, for sites with more than one kind of single page\n * (posts, pages, custom post types). A `single` route reads the collection\n * named by `RouteModel.collection`.\n */\n collections?: Record<string, EmitPost[]>\n /** QueryBinding id → the static paths (and items) that query produces. */\n queries?: Record<string, QueryPage[]>\n}\n\n/** A legacy stylesheet provided by the producer, referenced from `LayoutFamily.css.files`. */\nexport interface EmitCssFile {\n path: string\n content: string\n}\n\n/** A host the emitted site may optimize images from (Astro \\`image.remotePatterns\\`). */\nexport interface RemoteImagePattern {\n protocol?: 'https' | 'http'\n /** Exact host, or \\`*.example.com\\` for any subdomain. */\n hostname: string\n /** Path prefix, e.g. \\`/media/\\`. Optional. */\n pathname?: string\n}\n\nexport interface ImageOptions {\n /** Default true. \\`false\\` emits no image pass at all. */\n enabled?: boolean\n /** Hosts whose images are optimized. The runtime host (Studio's CDN) is always added. */\n remotePatterns?: RemoteImagePattern[]\n /** srcset widths in px. Default 480, 768, 1024, 1600; widths above the width the HTML declares are dropped. */\n widths?: number[]\n /** \\`sizes\\` for an image that has none. Default \\`(max-width: 768px) 100vw, 768px\\`. */\n sizes?: string\n}\n\nexport interface EmitOptions {\n /** Generated project's package name. Default: \"migrated-site\". */\n projectName?: string\n /** Include Tailwind 4 as the evolution layer (default true). */\n tailwind?: boolean\n /**\n * Emit per-page SEO — title, description, canonical, Open Graph, Twitter card\n * and Article structured data — and remove the template page's copies of those\n * tags from the head chrome. Default true: inheriting one page's canonical and\n * og:title on every page is worse than having neither. Set false to keep the\n * source head verbatim and own these tags in the producer.\n */\n seo?: boolean\n /**\n * Emit a sitemap and a `robots.txt` that points at it. Default true.\n *\n * The sitemap is generated by `@astrojs/sitemap` at build time rather than\n * computed here, because only the build knows the real set of URLs — every\n * dynamic route, every paginated page, whatever `getStaticPaths` returned. A\n * sitemap the emitter worked out on its own would be a second source of truth\n * that can disagree with the site it describes.\n *\n * Independent of `seo`: a producer that writes its own meta tags has not\n * thereby said it writes its own sitemap, and coupling the two would drop the\n * sitemap silently for exactly that producer.\n */\n sitemap?: boolean\n /**\n * Build-time image optimization for content bodies (\\`astro:assets\\`):\n * srcset, WebP, width/height and lazy loading for images on allowed hosts —\n * migrated media on the runtime host is always allowed — and lazy loading\n * with async decoding for every other image. Default on.\n */\n images?: ImageOptions\n /**\n * Build the site's RSS feed at `/feed.xml` — the newest posts, as WordPress\n * serves them at `/feed/` — and point the theme's head link at it. Default\n * true. Needs `site.url` (feed links are absolute) and a route that builds\n * the `posts` collection.\n */\n feed?: boolean\n /**\n * Build `/llms.txt` (llmstxt.org): the site's name, tagline and newest pages\n * of each collection, for language models. Default true. Needs `site.url`.\n */\n llms?: boolean\n /** The site's tagline (WordPress `blogdescription`): the feed's description and the llms.txt summary. */\n siteDescription?: string\n /**\n * Whether the source site's addresses end in a slash (WordPress\n * `RawRouting.trailing_slash`): `/hello/` or `/hello`. Default true. With\n * false the build writes files (`/hello.html`, which Netlify and Cloudflare\n * Pages serve at `/hello`, and Vercel with the `cleanUrls` this adds to\n * `vercel.json`), and canonical, og:url, hreflang, the feed and llms.txt\n * name every page without the slash — the migrated site keeps the form the\n * source was indexed under.\n */\n trailingSlash?: boolean\n /**\n * The site's interface-text dictionary (see \\`UI_STRINGS_MODEL\\`). Comments\n * and forms read it at build time from \\`dir\\` (default\n * \\`.contentrain/content/site/ui-strings\\`, project-relative).\n *\n * \\`locales\\` declares which languages the producer wrote. The emitter cannot\n * see the store, so this is how it can say at emit time that a page's\n * language has no dictionary; the build reports the same from the files.\n */\n uiStrings?: { dir?: string; locales?: string[] }\n /**\n * The host the site deploys to, for `EmitInput.redirects`: its own redirect\n * file is written so the rules answer with a real HTTP status (a static\n * Astro build serves them as meta-refresh pages, kept as the fallback).\n * Unset: Netlify `public/_redirects` and `vercel.json` are both written; a\n * Cloudflare Pages site must name its host (no forced rules there).\n */\n redirectHost?: 'netlify' | 'cloudflare' | 'vercel'\n /**\n * The moment `EmitPost.publish_at` is compared with, ISO 8601. Default: the\n * time of the emit. The data files hold only what is live then, so a\n * scheduled entry is built by the first emit after its time.\n */\n now?: string\n /**\n * Hold back every entry — in posts, collections and list items — that\n * carries no `EmitPost.status`, and say so. Default false: an entry with\n * no status is published, as before statuses existed. A producer that\n * reads statuses from the content store sets it, so an entry it failed to\n * give one fails closed instead of going out as published.\n */\n requireStatus?: boolean\n}\n\nexport interface EmitInput {\n ir: ProjectIR\n content?: EmitContent\n css?: EmitCssFile[]\n options?: EmitOptions\n /**\n * Where runtime components (comments, forms) talk to. Without it they are\n * emitted as placeholders and a warning names each one — a migration is\n * complete without any runtime offer being accepted.\n */\n runtime?: RuntimeBinding\n /**\n * The live site's redirect rules (`RawIR.redirects`). Plain one-to-one rules\n * are written to `astro.config` `redirects`; patterns, regular expressions,\n * other statuses and rules on an address the site builds a page at come\n * back in `EmitResult.redirects.manual` with the reason, to set up at the host.\n */\n redirects?: RawRedirect[]\n /**\n * Rules served only by the host's redirect file (`public/_redirects`,\n * `vercel.json`), never by `astro.config` — so no meta-refresh page is built\n * for them. For bulk rules the site does not need as pages: one per\n * WordPress attachment page runs to tens of thousands, and a static build\n * would write an HTML file for each. They pass the same checks as\n * `redirects`, and the site's own rules and the feed redirects win an\n * address they share. Without a fallback page, a rule the host file cannot\n * hold — a pattern, or past Cloudflare's or Vercel's rule limit, where these\n * go last — is returned in `EmitResult.redirects.manual`.\n */\n hostRedirects?: RawRedirect[]\n}\n\n// ─── Emit output ───\n\n/**\n * Pure result: every generated file as path → content. Writing to disk is a\n * separate, trivial step (`writeEmit`) so the core stays testable and usable\n * from any host — CLI, service, or another emitter wrapping this one.\n */\nexport interface EmitResult {\n files: Record<string, string>\n warnings: string[]\n /** Present when `EmitInput.redirects` or `hostRedirects` was given. */\n redirects?: {\n /** Rules written: `redirects` to astro.config and the host files, `hostRedirects` to the host files. */\n written: RawRedirect[]\n /** Rules not written, with why — to set up by hand at the host. */\n manual: Array<{ redirect: RawRedirect; reason: string }>\n /** The host redirect files written, e.g. `public/_redirects (netlify)`. Empty: meta-refresh only. */\n host_files: string[]\n /**\n * Rules (by `from`) left out of a Cloudflare / Vercel file at its rule\n * limit. A `redirects` rule keeps its meta-refresh page there. A\n * `hostRedirects` rule is here only with no host named (the Netlify file\n * holds it, `vercel.json` does not); past a named host's limit it is in `manual`.\n */\n host_over_limit: string[]\n }\n /**\n * Present when entries were held back (`EmitPost.status`, `publish_at`):\n * each one, and each link to one that a published page carried — kept as\n * its text. Redirects to a held-back entry are in `redirects.manual`.\n */\n withheld?: {\n entries: WithheldEntry[]\n links: WithheldLink[]\n }\n}\n\nexport type { ProjectIR, RawRedirect, RuntimeBinding, EntrySourceRef }\n","// Image optimization for migrated content (AI-10).\n//\n// Post bodies arrive as finished HTML and render through `set:html`, so\n// Astro's <Image> component cannot reach the images inside them. The\n// emitted site instead runs one build-time pass over each page's content:\n// every <img> on a host the site is allowed to optimize goes through\n// `astro:assets`' `getImage()` — resized to a srcset, re-encoded to WebP,\n// width/height filled in — and every <img>, optimized or not, gets\n// `loading` (the first one eager with `fetchpriority=\"high\"`, it is usually\n// the largest paint; the rest lazy) and `decoding=\"async\"`.\n//\n// Two emitted files:\n// - `src/lib/images.ts` — the pure rewrite, with the optimizer injected. No\n// Astro import, so it is tested and type-checked on its own.\n// - `src/lib/optimize-images.ts` — binds it to `getImage()` and the host\n// allow-list; the only file that imports `astro:assets`.\n\nimport type { EmitOptions, RemoteImagePattern, RuntimeBinding } from './types.js'\n\nexport const DEFAULT_IMAGE_WIDTHS = [480, 768, 1024, 1600]\nexport const DEFAULT_IMAGE_SIZES = '(max-width: 768px) 100vw, 768px'\n\nexport function imagesEnabled(options: EmitOptions): boolean {\n return options.images?.enabled !== false\n}\n\n/**\n * The hosts the site may optimize: the producer's list plus the runtime host,\n * because migrated media is rehosted there. Deduplicated, stable order.\n */\nexport function imagePatterns(options: EmitOptions, runtime?: RuntimeBinding): RemoteImagePattern[] {\n const out: RemoteImagePattern[] = [...(options.images?.remotePatterns ?? [])]\n if (runtime?.base_url) {\n try {\n const url = new URL(runtime.base_url)\n out.push({ protocol: url.protocol === 'http:' ? 'http' : 'https', hostname: url.hostname })\n }\n catch { /* an unparseable base_url adds no pattern */ }\n }\n const seen = new Set<string>()\n return out.filter((p) => {\n const key = `${p.protocol ?? 'https'}|${p.hostname}|${p.pathname ?? ''}`\n if (seen.has(key)) return false\n seen.add(key)\n return true\n })\n}\n\n/** The `image` block for astro.config.mjs — Astro enforces the same allow-list. */\nexport function astroImageConfig(patterns: RemoteImagePattern[]): string | null {\n if (patterns.length === 0) return null\n const list = patterns.map((p) => {\n const parts = [`protocol: ${JSON.stringify(p.protocol ?? 'https')}`, `hostname: ${JSON.stringify(p.hostname)}`]\n if (p.pathname) parts.push(`pathname: ${JSON.stringify(`${p.pathname.replace(/\\/?$/, '/')}**`)}`)\n return `{ ${parts.join(', ')} }`\n })\n return ` image: { remotePatterns: [${list.join(', ')}] },`\n}\n\nexport const IMAGES_TS = `// Emitted by @contentrain/emitter-astro — image pass over content HTML.\n// Pure: the optimizer is injected (see optimize-images.ts), so this file has\n// no Astro dependency and never breaks a page — an image the optimizer\n// cannot handle keeps its original markup.\n\nexport interface OptimizedImage {\n src: string\n srcset?: string\n width?: number\n height?: number\n}\n\nexport type ImageOptimizer = (\n src: string,\n hints: { width?: number; height?: number },\n) => Promise<OptimizedImage | null>\n\n// Quoted attribute values may hold a \">\" (alt=\"a > b\"), so a tag ends at the\n// first \">\" outside quotes. An unquoted value may hold a quote (alt=don't),\n// which a browser reads as part of the value; there the quote-aware match\n// would run on to the next quote and swallow the markup after the tag. A\n// match that reaches a \"<\" is that case, and the tag is read up to its first\n// \">\" instead.\nconst IMG_START = /<img\\\\b/gi\nconst IMG_QUOTED = /<img\\\\b(?:[^>\"']|\"[^\"]*\"|'[^']*')*>/y\nconst IMG_PLAIN = /<img\\\\b[^>]*>/y\n// An unquoted value runs to whitespace or \">\", quotes included, as a browser reads it.\nconst ATTR = /([^\\\\s=/>]+)(?:\\\\s*=\\\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\\\s>]+)))?/g\n\n/** Every <img> tag in the html, with where it starts. */\nfunction imgTags(html: string): Array<{ start: number; tag: string }> {\n const found: Array<{ start: number; tag: string }> = []\n for (const open of html.matchAll(IMG_START)) {\n const start = open.index ?? 0\n if (found.length && start < found[found.length - 1]!.start + found[found.length - 1]!.tag.length) continue\n IMG_QUOTED.lastIndex = start\n let tag = IMG_QUOTED.exec(html)?.[0]\n if (tag === undefined || tag.includes('<', 1)) {\n IMG_PLAIN.lastIndex = start\n tag = IMG_PLAIN.exec(html)?.[0]\n }\n if (tag !== undefined) found.push({ start, tag })\n }\n return found\n}\nconst SKIP_EXT = /\\\\.(svg|gif)(?:$|[?#])/i\n\n// Attribute values are kept as the source wrote them (entities and all) and\n// written back unchanged; only values this pass sets are escaped. Reading a\n// value decodes it: a WordPress src like \"photo.jpg?w=800&amp;ssl=1\" is the\n// URL \"photo.jpg?w=800&ssl=1\".\nfunction parseAttrs(tag: string): Array<[string, string | null]> {\n const inner = tag.replace(/^<img\\\\b/i, '').replace(/\\\\/?>$/, '')\n const attrs: Array<[string, string | null]> = []\n for (const m of inner.matchAll(ATTR)) {\n const name = m[1]\n if (!name) continue\n // Written back double-quoted, so a double quote inside another form is escaped.\n const value = m[2] ?? (m[3] ?? m[4])?.replace(/\"/g, '&quot;')\n attrs.push([name, value === undefined ? null : value])\n }\n return attrs\n}\n\nconst NAMED: Record<string, string> = { amp: '&', quot: '\"', apos: \"'\", lt: '<', gt: '>' }\n\nfunction decodeAttr(value: string): string {\n return value.replace(/&(?:#(\\\\d+)|#x([0-9a-f]+)|([a-z]+));/gi, (entity, dec, hex, name) => {\n if (dec || hex) {\n const code = dec ? Number(dec) : parseInt(hex, 16)\n return code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : entity\n }\n return NAMED[name.toLowerCase()] ?? entity\n })\n}\n\nfunction escapeAttr(value: string): string {\n return value.replace(/&/g, '&amp;').replace(/\"/g, '&quot;')\n}\n\nfunction serialize(attrs: Array<[string, string | null]>): string {\n return \\`<img\\${attrs.map(([n, v]) => (v === null ? \\` \\${n}\\` : \\` \\${n}=\"\\${v}\"\\`)).join('')}>\\`\n}\n\nfunction positiveInt(value: string | null | undefined): number | undefined {\n if (!value) return undefined\n const n = Number(value)\n return Number.isInteger(n) && n > 0 ? n : undefined\n}\n\n/**\n * Rewrite every <img> in the html. The first image is eager with high fetch\n * priority; the rest are lazy. Images the optimizer accepts get its src,\n * srcset and size. An existing loading or decoding attribute is respected.\n */\nexport async function rewriteImages(\n html: string,\n optimize: ImageOptimizer,\n options: { sizes: string },\n): Promise<string> {\n const found = imgTags(html)\n if (!found.length) return html\n const replacements: string[] = []\n for (let index = 0; index < found.length; index++) {\n const tag = found[index]!.tag\n const attrs = parseAttrs(tag)\n const get = (name: string) => {\n const raw = attrs.find(([n]) => n.toLowerCase() === name)?.[1]\n return raw == null ? raw : decodeAttr(raw)\n }\n const has = (name: string) => attrs.some(([n]) => n.toLowerCase() === name)\n const set = (name: string, value: string) => {\n const i = attrs.findIndex(([n]) => n.toLowerCase() === name)\n if (i >= 0) attrs[i] = [attrs[i]![0], escapeAttr(value)]\n else attrs.push([name, escapeAttr(value)])\n }\n const remove = (name: string) => {\n const i = attrs.findIndex(([n]) => n.toLowerCase() === name)\n if (i >= 0) attrs.splice(i, 1)\n }\n\n if (!has('loading')) {\n set('loading', index === 0 ? 'eager' : 'lazy')\n if (index === 0 && !has('fetchpriority')) set('fetchpriority', 'high')\n }\n if (!has('decoding')) set('decoding', 'async')\n\n const src = get('src')\n const keep = has('data-cr-keep') || !src || src.startsWith('data:') || SKIP_EXT.test(src)\n if (!keep) {\n try {\n const result = await optimize(src, { width: positiveInt(get('width')), height: positiveInt(get('height')) })\n if (result) {\n set('src', result.src)\n if (result.srcset) {\n set('srcset', result.srcset)\n if (!has('sizes')) set('sizes', options.sizes)\n } else {\n // The source's own srcset (WordPress's size variants) would win\n // over the optimized src in every browser that reads srcset.\n remove('srcset')\n remove('sizes')\n }\n if (result.width && result.height && !has('width') && !has('height')) {\n set('width', String(result.width))\n set('height', String(result.height))\n }\n }\n }\n catch {\n // Keep the original image: a failed optimization must not cost the page its picture.\n }\n }\n replacements.push(serialize(attrs))\n }\n let out = ''\n let at = 0\n found.forEach(({ start, tag }, i) => {\n out += html.slice(at, start) + replacements[i]\n at = start + tag.length\n })\n return out + html.slice(at)\n}\n`\n\nexport function optimizeImagesTs(patterns: RemoteImagePattern[], widths: number[], sizes: string): string {\n return `// Emitted by @contentrain/emitter-astro — binds the image pass to astro:assets.\n// Only hosts on this list are optimized; Astro enforces the same list\n// (image.remotePatterns in astro.config.mjs). Everything else keeps its URL\n// and still gets lazy loading and async decoding (see images.ts).\nimport { getImage } from 'astro:assets'\nimport { rewriteImages } from './images'\n\nconst PATTERNS: Array<{ protocol: string; hostname: string; pathname?: string }> = ${JSON.stringify(patterns.map((p) => ({ protocol: p.protocol ?? 'https', hostname: p.hostname, ...(p.pathname ? { pathname: p.pathname } : {}) })))}\nconst WIDTHS = ${JSON.stringify(widths.toSorted((a, b) => a - b))}\nconst SIZES = ${JSON.stringify(sizes)}\n\n// Astro downloads a remote image when the build writes its assets — after\n// this pass, outside its error handling — and one image that does not answer\n// 200 fails the whole astro build. So an image is only handed to getImage()\n// once it answered with an image here; anything else keeps its original URL.\n// One check per URL per build.\nconst CHECK_TIMEOUT_MS = 10_000\nconst checked = new Map<string, Promise<boolean>>()\n\nasync function answersWithImage(src: string, method: 'HEAD' | 'GET'): Promise<boolean | null> {\n try {\n const res = await fetch(src, { method, redirect: 'follow', signal: AbortSignal.timeout(CHECK_TIMEOUT_MS) })\n if (method === 'GET') await res.body?.cancel()\n // Some hosts refuse HEAD; ask again with GET.\n if (method === 'HEAD' && (res.status === 405 || res.status === 501 || res.status === 403)) return null\n return res.ok && (res.headers.get('content-type') ?? '').toLowerCase().startsWith('image/')\n }\n catch {\n return method === 'HEAD' ? null : false\n }\n}\n\nfunction reachable(src: string): Promise<boolean> {\n let pending = checked.get(src)\n if (!pending) {\n pending = answersWithImage(src, 'HEAD').then((ok) => ok ?? answersWithImage(src, 'GET').then((retry) => retry === true))\n checked.set(src, pending)\n }\n return pending\n}\n\nfunction allowed(src: string): boolean {\n let url: URL\n try {\n url = new URL(src)\n }\n catch {\n return false\n }\n return PATTERNS.some((p) =>\n url.protocol === \\`\\${p.protocol}:\\`\n && (p.hostname.startsWith('*.') ? url.hostname.endsWith(p.hostname.slice(1)) : url.hostname === p.hostname)\n && (!p.pathname || url.pathname.startsWith(p.pathname)))\n}\n\nexport async function optimizeHtmlImages(html: string): Promise<string> {\n if (!html.includes('<img')) return html\n return rewriteImages(html, async (src, hints) => {\n if (!allowed(src) || !(await reachable(src))) return null\n const sized = hints.width !== undefined && hints.height !== undefined\n const widths = WIDTHS.filter((w) => hints.width === undefined || w <= hints.width)\n const image = await getImage({\n src,\n format: 'webp',\n ...(sized ? { width: hints.width, height: hints.height } : { inferSize: true }),\n ...(widths.length ? { widths } : {}),\n })\n const width = Number(image.attributes.width)\n const height = Number(image.attributes.height)\n return {\n src: image.src,\n srcset: image.srcSet.attribute || undefined,\n ...(width > 0 && height > 0 ? { width, height } : {}),\n }\n }, { sizes: SIZES })\n}\n`\n}\n","/** \"f-archive\" → \"FArchive\", \"posts\" → \"Posts\" — Astro component file names. */\nexport function pascalCase(id: string): string {\n return id\n .split(/[^a-zA-Z0-9]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join('')\n}\n\n/**\n * Route pattern → Astro pages path.\n * `/` → `index.astro`; `/category/:term/page/:page` → `category/[term]/page/[page].astro`;\n * a trailing `*` on a parameter makes it a rest parameter — `/category/:term*`\n * → `category/[...term].astro`, which is what keeps a nested taxonomy address\n * (`/category/about-cc/events/`) from collapsing to its last segment.\n * Returns null for patterns Astro's file routing cannot express (recorded as a warning upstream).\n */\nexport function patternToPagePath(pattern: string): string | null {\n if (pattern === '/' || pattern === '') return 'index.astro'\n const segments = pattern.replace(/^\\/+|\\/+$/g, '').split('/')\n // Literal Unicode segments preserve the source permalink. Validate literals\n // separately from parameters so widening the alphabet cannot admit traversal\n // or turn a source filename containing brackets into an Astro parameter.\n if (segments.some((seg) => seg.startsWith(':')\n ? !/^:[a-zA-Z0-9_]+\\*?$/.test(seg)\n : seg === '.' || seg === '..' || !/^[\\p{L}\\p{M}\\p{N}._-]+$/u.test(seg))) return null\n const mapped = segments.map((seg) => {\n if (!seg.startsWith(':')) return seg\n const rest = seg.endsWith('*')\n const name = seg.slice(1, rest ? -1 : undefined)\n return rest ? `[...${name}]` : `[${name}]`\n })\n const last = mapped.pop()\n return [...mapped, `${last}.astro`].join('/')\n}\n\n/** Index of the rest parameter segment in a page path, or -1. */\nexport function restParamIndex(pagePath: string): number {\n return pagePath.split('/').findIndex((seg) => seg.startsWith('[...'))\n}\n\nconst sortDeep = (v: unknown): unknown => {\n if (Array.isArray(v)) return v.map(sortDeep)\n if (v && typeof v === 'object') {\n return Object.fromEntries(\n Object.entries(v as Record<string, unknown>)\n .toSorted(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([k, val]) => [k, sortDeep(val)]),\n )\n }\n return v\n}\n\n/** JSON with stable key order and trailing newline — deterministic output, stable diffs. */\nexport function stableJson(value: unknown): string {\n return `${JSON.stringify(sortDeep(value), null, 2)}\\n`\n}\n","// Routes → Astro pages.\n//\n// Three shapes, matching what a migrated site actually needs:\n// - `single`: one page per post, body injected with `set:html` inside the family layout.\n// - list kinds (`archive`/`term`/`author`/`date`/`front` with a query): static\n// paths come from the producer's query data; items render through the\n// extracted item template when one exists.\n// - static kinds (no query): the family chrome IS the page.\n//\n// Pagination is a route parameter (`/news/page/:page`), never a separate\n// family — the generated paths simply include the page number in params.\n\nimport type { RouteModel, LayoutFamily } from '@contentrain/types'\nimport type { EmitContent, EmitPost, QueryPage } from './types.js'\nimport { DEFAULT_COLLECTION } from './types.js'\nimport { pascalCase, patternToPagePath, restParamIndex, stableJson } from './util.js'\n\nexport interface PageGenResult {\n files: Record<string, string>\n warnings: string[]\n}\n\nexport function routeFiles(\n route: RouteModel,\n family: LayoutFamily | undefined,\n content: EmitContent,\n siteLocale: string,\n seo = true,\n /** The route's pages each have a feed at <page>/feed.xml (see feed.ts): the page links it. */\n archiveFeed = false,\n): PageGenResult {\n const warnings: string[] = []\n const files: Record<string, string> = {}\n const pagePath = patternToPagePath(route.pattern)\n if (!pagePath) {\n return { files, warnings: [`route ${route.id}: pattern \"${route.pattern}\" cannot be expressed as an Astro page — skipped`] }\n }\n if (!family) {\n return { files, warnings: [`route ${route.id}: family \"${route.family}\" not found — skipped`] }\n }\n const layout = pascalCase(family.id)\n const depth = pagePath.split('/').length - 1\n const up = '../'.repeat(depth + 1) || '../'\n const hasParams = pagePath.includes('[')\n\n // Astro matches a rest parameter greedily, so anything after it is ambiguous.\n const restAt = restParamIndex(pagePath)\n if (restAt !== -1 && restAt !== pagePath.split('/').length - 1) {\n warnings.push(\n `route ${route.id}: rest parameter is not the final segment of \"${route.pattern}\" — Astro matches rest parameters greedily; verify this route resolves`,\n )\n }\n\n // Collection-driven: `single` always, and any route that names a collection —\n // a site's pages and custom post types are per-entry routes too, they just are\n // not called \"single\".\n if (route.kind === 'single' || route.collection !== undefined) {\n const collection = route.collection ?? DEFAULT_COLLECTION\n const posts = collectionItems(content, collection)\n if (!posts.length) {\n warnings.push(`route ${route.id}: collection \"${collection}\" has no items — page emitted, data file empty`)\n }\n files[`src/data/${collection}.json`] = stableJson(posts)\n files[`src/pages/${pagePath}`] = singlePage(route, layout, up, collection, siteLocale, hasParams, seo)\n return { files, warnings }\n }\n\n const queryPages: QueryPage[] = route.query ? (content.queries?.[route.query] ?? []) : []\n if (route.query) {\n if (!queryPages.length) warnings.push(`route ${route.id}: query \"${route.query}\" has no data — page emitted, data file empty`)\n if (queryPages.some((p) => !p.item_template && !p.sections?.length)) {\n warnings.push(`route ${route.id}: item template missing on some pages — plain fallback list rendered (not fidelity)`)\n }\n files[`src/data/queries/${route.query}.json`] = stableJson(queryPages)\n files[`src/pages/${pagePath}`] = listPage(route, layout, up, hasParams, siteLocale, seo, archiveFeed)\n return { files, warnings }\n }\n\n files[`src/pages/${pagePath}`] = staticPage(route, layout, up, hasParams, warnings, siteLocale, seo)\n return { files, warnings }\n}\n\n/** Items of a named collection; `posts` also accepts the `content.posts` shorthand. */\nexport function collectionItems(content: EmitContent, collection: string): EmitPost[] {\n const named = content.collections?.[collection]\n if (named) return named\n return collection === DEFAULT_COLLECTION ? (content.posts ?? []) : []\n}\n\nfunction singlePage(route: RouteModel, layout: string, up: string, collection: string, siteLocale: string, hasParams: boolean, seo: boolean): string {\n const locale = JSON.stringify(route.locale ?? siteLocale)\n return `---\n// Route: ${route.id} (${route.pattern}) — collection: ${collection} — emitted by @contentrain/emitter-astro\nimport Layout from '${up}layouts/${layout}.astro'\nimport data from '${up}data/${collection}.json'\nimport { postMarks${seo ? ', postSeo' : ''}, type EmittedPost } from '${up}lib/fill'\n\n${hasParams ? `export function getStaticPaths() {\n // The JSON is data; its contract is EmittedPost. Inferring the type from the\n // file would type only the fields this site happens to carry, and \"astro\n // check\" (which the build runs first) would then reject post.params on a site\n // whose permalinks need none. The cast lives INSIDE getStaticPaths because\n // Astro hoists this function: it sees imports, not the component scope.\n const posts = data as EmittedPost[]\n // A post carries its own route parameters (date parts of a dated permalink,\n // a post id) — using the template post's would send every other post to the\n // wrong address.\n return posts.map((post) => ({ params: { ...(post.params ?? {}), slug: post.slug }, props: { post } }))\n}\ntype Props = { post: EmittedPost }\nconst { post } = Astro.props as Props` : `const posts = data as EmittedPost[]\nif (posts.length !== 1) throw new Error(${JSON.stringify(`Static content route requires exactly one entry: ${route.id}`)})\nconst post = posts[0]!`}\n---\n<Layout\n title={post.title}\n marks={postMarks(post)}\n body={post.body}\n css={post.css ?? []}\n lang={post.locale ?? ${locale}}\n entry={post.entry}${seo ? `\n seo={postSeo(post${route.kind === 'page' ? \", 'page'\" : ''})}` : ''}\n/>\n`\n}\n\nfunction listPage(route: RouteModel, layout: string, up: string, hasParams: boolean, siteLocale: string, seo: boolean, archiveFeed = false): string {\n const locale = JSON.stringify(route.locale ?? siteLocale)\n // Chain the fallbacks HERE, not in the emitted source: a route without a title\n // used to emit `page.title ?? \"\" ?? ''`, which \"astro check\" rejects as never\n // nullish — and the build runs check first, so the whole site failed to build.\n const routeTitle = route.title === undefined ? `''` : JSON.stringify(route.title)\n // The cast lives inside getStaticPaths: Astro hoists it above the component\n // scope, so it can read imports and nothing else.\n const paths = hasParams\n ? `export function getStaticPaths() {\n const pages = data as EmittedQueryPage[]\n return pages.map((page) => ({ params: page.params, props: { page } }))\n}\ntype Props = { page: EmittedQueryPage }\nconst { page } = Astro.props as Props`\n : `const page = (data as EmittedQueryPage[])[0] ?? { params: {}, items: [], title: undefined }`\n return `---\n// Route: ${route.id} (${route.pattern}) — emitted by @contentrain/emitter-astro\nimport Layout from '${up}layouts/${layout}.astro'\nimport data from '${up}data/queries/${route.query}.json'\nimport { ${archiveFeed && seo ? 'entryAddress, ' : ''}renderQueryPage, type EmittedQueryPage } from '${up}lib/fill'\n\n${paths}\n// A list renders in sections (a big card then a grid) — one template is the\n// single-section case. Without a template at all it falls back to a plain list\n// and the emitter has already warned: silence would read as fidelity.\nconst content = renderQueryPage(page)\n// Route params carry slugs; page marks carry what the chrome needs to SHOW\n// (a term's display name, description, count).\nconst marks = { ...page.params, ...(page.marks ?? {}) }\nconst title = page.title ?? ${routeTitle}\n---\n<Layout\n title={title || String(marks.term_name ?? page.params.term ?? '')}\n marks={marks}\n body={content}\n css={page.css ?? []}\n lang={${locale}}${seo ? `\n seo={{ description: page.description, image: page.image, imageMeta: page.image_meta, canonical: page.canonical, breadcrumbs: page.breadcrumbs, noindex: page.noindex, nofollow: page.nofollow, openGraph: page.open_graph, twitter: page.twitter, schema: page.schema, profile: page.profile,${archiveFeed ? ` feed: { path: (entryAddress(${JSON.stringify(route.pattern)}, page.params) ?? '/') + 'feed.xml', title: title || undefined },` : ''} type: 'website', pageType: 'CollectionPage' }}` : ''}\n/>\n`\n}\n\nfunction staticPage(route: RouteModel, layout: string, up: string, hasParams: boolean, warnings: string[], siteLocale: string, seo: boolean): string {\n if (hasParams) warnings.push(`route ${route.id}: parameterized route without a query — emitted with empty paths`)\n const paths = hasParams\n ? `export function getStaticPaths() {\n return []\n}\n`\n : ''\n return `---\n// Route: ${route.id} (${route.pattern}) — emitted by @contentrain/emitter-astro\nimport Layout from '${up}layouts/${layout}.astro'\n${paths}---\n<Layout title=${JSON.stringify(route.title ?? '')} marks={{}} lang={${JSON.stringify(route.locale ?? siteLocale)}}${seo ? ` seo={{ type: 'website' }}` : ''} />\n`\n}\n\n/** Share a catchall while preserving the original editable collections. */\nexport function sharedCollectionPage(routes: RouteModel[], content: EmitContent, siteLocale: string, seo = true): string | undefined {\n const pattern = routes[0]?.pattern\n if (!pattern || routes.length < 2 || !/^\\/(?:[^:*]+\\/)*:slug\\*$/.test(pattern)\n || routes.some(r => r.pattern !== pattern || r.query || (r.kind !== 'single' && !r.collection))) return undefined\n const seen = new Set<string>()\n for (const route of routes) for (const post of collectionItems(content, route.collection ?? DEFAULT_COLLECTION)) {\n let key: string\n try { key = decodeURIComponent(post.slug).replace(/\\/+$/, '') }\n catch { return undefined }\n if (seen.has(key)) return undefined\n seen.add(key)\n }\n const up = '../'.repeat(patternToPagePath(pattern)!.split('/').length)\n return `---\n// Shared collection routes: ${routes.map(r => r.id).join(', ')}\n${routes.map((r, i) => `import Layout${i} from '${up}layouts/${pascalCase(r.family)}.astro'\\nimport data${i} from '${up}data/${r.collection ?? DEFAULT_COLLECTION}.json'`).join('\\n')}\nimport { postMarks${seo ? ', postSeo' : ''}, type EmittedPost } from '${up}lib/fill'\nexport function getStaticPaths() {\n const sources = [${routes.map((_, i) => `data${i}`).join(', ')}] as EmittedPost[][]\n const seen = new Set<string>()\n return sources.flatMap((posts, routeIndex) => posts.map(post => {\n const key = decodeURIComponent(post.slug).replace(/\\\\/+$/, '')\n if (seen.has(key)) throw new Error('Route collision in shared collections: ' + post.slug)\n seen.add(key)\n return { params: { ...(post.params ?? {}), slug: post.slug }, props: { post, routeIndex } }\n }))\n}\nconst { post, routeIndex } = Astro.props as { post: EmittedPost, routeIndex: number }\nconst layouts = [${routes.map((_, i) => `Layout${i}`).join(', ')}]\nconst locales = ${JSON.stringify(routes.map(r => r.locale ?? siteLocale))}\nconst kinds = ${JSON.stringify(routes.map(r => (r.kind === 'page' ? 'page' : 'post')))} as Array<'post' | 'page'>\nconst Layout = layouts[routeIndex]!\n---\n<Layout title={post.title} marks={postMarks(post)} body={post.body}\n css={post.css ?? []} lang={post.locale ?? locales[routeIndex]} entry={post.entry}${seo ? ' seo={postSeo(post, kinds[routeIndex])}' : ''} />\n`\n}\n","// hreflang: which pages are translations of each other, and where each one is.\n//\n// Both halves come from what the emitter already holds, so neither can drift:\n//\n// - A translation group is the content-store entry. The importer folds the\n// translations of one post into one entry id across locales, and a post\n// carries that address as `EmitPost.entry` — the same key the comments\n// component threads by.\n// - An address is computed from the route pattern and the post's own\n// parameters, the very values `getStaticPaths` hands Astro. The path printed\n// in a `<link rel=\"alternate\">` is therefore the path Astro builds, not a\n// producer's recomputation of it.\n//\n// A post whose address is ambiguous gets no alternates. A wrong hreflang tells\n// a search engine to serve the wrong page to a language's readers; a missing\n// one only leaves it to work that out.\n\nimport type { RouteModel } from '@contentrain/types'\nimport type { EmitContent, EmitPost } from './types.js'\nimport { DEFAULT_COLLECTION } from './types.js'\nimport { collectionItems } from './pages.js'\nimport { patternToPagePath } from './util.js'\n\nexport interface Alternate {\n /** A language tag, or `x-default`. */\n lang: string\n /** Site-root-relative path, as the build serves it. */\n path: string\n}\n\n/**\n * The path Astro builds for a route pattern and a set of parameters, in the\n * directory format the scaffold configures (`/en/hello/`). Null when a\n * parameter has no value — that page has no address to name.\n */\nexport function entryPath(pattern: string, params: Record<string, string | undefined>): string | null {\n const segments = pattern.replace(/^\\/+|\\/+$/g, '').split('/').filter(Boolean)\n if (!segments.length) return '/'\n const out: string[] = []\n for (const segment of segments) {\n if (!segment.startsWith(':')) {\n out.push(segment)\n continue\n }\n const value = params[segment.slice(1).replace(/\\*$/, '')]?.replace(/^\\/+|\\/+$/g, '')\n if (!value) return null\n out.push(value)\n }\n return `/${out.join('/')}/`\n}\n\ninterface Placement {\n lang: string\n path: string | null\n key?: string\n collection: string\n}\n\nexport interface AlternatesResult {\n /** The content with `alternates` on every post that has translations. */\n content: EmitContent\n warnings: string[]\n}\n\nconst byLangTag = (a: Alternate, b: Alternate) => (a.lang < b.lang ? -1 : a.lang > b.lang ? 1 : 0)\n\n/**\n * Attach `alternates` to every entry that has translations on the site.\n *\n * The language of a page is the one its `<html lang>` gets: the post's own\n * locale, else its route's, else the site's. A site with one language has no\n * alternates and nothing is attached.\n */\nexport function withAlternates(routes: RouteModel[], content: EmitContent, siteLocale: string): AlternatesResult {\n const warnings: string[] = []\n const placements = new Map<EmitPost, Placement[]>()\n\n for (const route of routes) {\n if (route.kind !== 'single' && route.collection === undefined) continue\n const pagePath = patternToPagePath(route.pattern)\n if (!pagePath) continue\n const collection = route.collection ?? DEFAULT_COLLECTION\n const posts = collectionItems(content, collection)\n // A static route renders its one entry at the pattern itself; with more\n // than one entry the page throws at build, and there is no address.\n if (!pagePath.includes('[') && posts.length !== 1) continue\n for (const post of posts) {\n const list = placements.get(post) ?? []\n list.push({\n lang: post.locale ?? route.locale ?? siteLocale,\n path: entryPath(route.pattern, { ...post.params, slug: post.slug }),\n key: post.entry ? JSON.stringify([post.entry.model_id, post.entry.entry_id]) : undefined,\n collection,\n })\n placements.set(post, list)\n }\n }\n\n const langs = new Set([...placements.values()].flat().map((p) => p.lang))\n if (langs.size < 2) return { content, warnings }\n\n // One address per post: a post that two routes generate has two, and naming\n // either would be a guess.\n const groups = new Map<string, Array<{ post: EmitPost; lang: string; path: string }>>()\n const unkeyed = new Map<string, number>()\n const ambiguous = new Map<string, number>()\n for (const [post, list] of placements) {\n const first = list[0]!\n if (list.length > 1 || first.path === null) {\n ambiguous.set(first.collection, (ambiguous.get(first.collection) ?? 0) + 1)\n continue\n }\n if (!first.key) {\n unkeyed.set(first.collection, (unkeyed.get(first.collection) ?? 0) + 1)\n continue\n }\n const group = groups.get(first.key) ?? []\n group.push({ post, lang: first.lang, path: first.path })\n groups.set(first.key, group)\n }\n for (const [collection, count] of unkeyed) {\n warnings.push(`collection ${collection}: ${count} posts carry no entry address — hreflang cannot link them to their translations`)\n }\n for (const [collection, count] of ambiguous) {\n warnings.push(`collection ${collection}: ${count} posts have no single address (served by more than one route, or a route parameter without a value) — no hreflang on those pages`)\n }\n\n const alternatesOf = new Map<EmitPost, Alternate[]>()\n for (const group of groups.values()) {\n if (group.length < 2) continue\n const byLang = new Map<string, string>()\n let conflict = false\n for (const member of group) {\n const seen = byLang.get(member.lang)\n if (seen !== undefined && seen !== member.path) conflict = true\n byLang.set(member.lang, member.path)\n }\n const entry = group[0]!.post.entry!\n if (conflict) {\n warnings.push(`entry ${entry.model_id}/${entry.entry_id}: two pages claim the same language — no hreflang for this entry`)\n continue\n }\n if (byLang.size < 2) continue\n const alternates: Alternate[] = [...byLang].map(([lang, path]) => ({ lang, path })).toSorted(byLangTag)\n const fallback = byLang.get(siteLocale)\n if (fallback !== undefined) alternates.push({ lang: 'x-default', path: fallback })\n for (const member of group) alternatesOf.set(member.post, alternates)\n }\n if (!alternatesOf.size) return { content, warnings }\n\n const mark = (posts: EmitPost[]) => posts.map((post) => {\n const alternates = alternatesOf.get(post)\n return alternates ? { ...post, alternates } : post\n })\n return {\n content: {\n ...content,\n ...(content.posts ? { posts: mark(content.posts) } : {}),\n ...(content.collections\n ? { collections: Object.fromEntries(Object.entries(content.collections).map(([name, posts]) => [name, mark(posts)])) }\n : {}),\n },\n warnings,\n }\n}\n","// Pages the source site kept out of search (`EmitPost.noindex`,\n// `QueryPage.noindex`) stay out of the migrated site's sitemap.\n//\n// `@astrojs/sitemap` lists every page the build writes; its `filter` is the\n// only way to leave one out. The addresses are computed here from the route\n// pattern and the entry's parameters — the values `getStaticPaths` builds the\n// page from — so the filter names exactly the pages that carry the robots meta.\n\nimport type { RouteModel } from '@contentrain/types'\nimport type { EmitContent } from './types.js'\nimport { DEFAULT_COLLECTION } from './types.js'\nimport { entryPath } from './alternates.js'\nimport { collectionItems } from './pages.js'\nimport { patternToPagePath } from './util.js'\n\n/** Site-root paths (directory format, `/a/b/`) of every noindex page, sorted. */\nexport function noindexPaths(routes: RouteModel[], content: EmitContent): string[] {\n const out = new Set<string>()\n for (const route of routes) {\n const pagePath = patternToPagePath(route.pattern)\n if (!pagePath) continue\n const dynamic = pagePath.includes('[')\n if (route.kind === 'single' || route.collection !== undefined) {\n const posts = collectionItems(content, route.collection ?? DEFAULT_COLLECTION)\n // A parameterless collection route renders its one entry at the pattern.\n if (!dynamic && posts.length !== 1) continue\n for (const post of posts) {\n if (!post.noindex) continue\n const path = entryPath(route.pattern, { ...post.params, slug: post.slug })\n if (path) out.add(path)\n }\n }\n if (route.query) {\n for (const page of content.queries?.[route.query] ?? []) {\n if (!page.noindex) continue\n const path = entryPath(route.pattern, page.params)\n if (path) out.add(path)\n }\n }\n }\n return [...out].toSorted()\n}\n\n/**\n * The `sitemap()` call for astro.config.mjs. The sitemap hands `filter` an\n * absolute, percent-encoded URL; it is compared by decoded path with the\n * trailing slash normalised, the form `noindexPaths` returns.\n */\nexport function sitemapIntegration(paths: string[]): string {\n if (!paths.length) return 'sitemap()'\n return `sitemap({ filter: (page) => !NOINDEX.has(sitemapPath(page)) })`\n}\n\n/** The declarations `sitemapIntegration` refers to, placed above `defineConfig`. */\nexport function sitemapFilterDeclarations(paths: string[]): string[] {\n if (!paths.length) return []\n return [\n `// Pages the source site kept out of search (noindex) stay out of the sitemap.`,\n `const NOINDEX = new Set(${JSON.stringify(paths)})`,\n `const sitemapPath = (page) => {`,\n ` let path = new URL(page).pathname`,\n ` try { path = decodeURI(path) } catch { /* keep the encoded path */ }`,\n ` return path.endsWith('/') ? path : \\`\\${path}/\\``,\n `}`,\n ``,\n ]\n}\n","// Project scaffolding: manifest, config, styles, and the shared fill helper\n// the generated pages use to pour content into `@@mark@@` placeholders.\n\nimport type { DesignTokens, ProjectIR } from '@contentrain/types'\nimport type { EmitOptions, RuntimeBinding } from './types.js'\nimport { DEFAULT_IMAGE_SIZES, DEFAULT_IMAGE_WIDTHS, IMAGES_TS, astroImageConfig, imagePatterns, imagesEnabled, optimizeImagesTs } from './images.js'\nimport { stableJson } from './util.js'\nimport { sitemapFilterDeclarations, sitemapIntegration } from './noindex.js'\n\n/**\n * `noindex` holds the site-root paths the sitemap leaves out (see noindex.ts);\n * `redirectsConfig` is the `redirects` block for astro.config.mjs (see redirects.ts);\n * `runtime` adds its host to the image allow-list (see images.ts).\n */\nexport function scaffoldFiles(ir: ProjectIR, options: EmitOptions, noindex: string[] = [], redirectsConfig: string | null = null, runtime?: RuntimeBinding): Record<string, string> {\n const tailwind = options.tailwind !== false\n const split = ir.viewport_strategy === 'split'\n // The sitemap integration needs an absolute site to build URLs from; without\n // one it has nothing to write, so it is only wired when there is a site.\n const sitemap = options.sitemap !== false && Boolean(ir.site.url)\n const images = imagesEnabled(options)\n const imageConfig = images ? astroImageConfig(imagePatterns(options, runtime)) : null\n const files: Record<string, string> = {}\n\n const pkg: Record<string, unknown> = {\n name: options.projectName ?? 'migrated-site',\n private: true,\n type: 'module',\n scripts: {\n dev: 'astro dev',\n // `astro check` first: type errors in generated pages should fail the\n // build, not surface later in a browser.\n build: 'astro check && astro build',\n preview: 'astro preview',\n // Split viewport production (skeleton): one build per device class, served by device.\n ...(split ? { 'build:desktop': 'VIEWPORT=desktop astro build --outDir dist/desktop', 'build:mobile': 'VIEWPORT=mobile astro build --outDir dist/mobile' } : {}),\n },\n dependencies: {\n astro: '^5.0.0',\n ...(tailwind ? { tailwindcss: '^4.0.0', '@tailwindcss/vite': '^4.0.0' } : {}),\n ...(sitemap ? { '@astrojs/sitemap': '^3.7.0' } : {}),\n // Astro's default image service; getImage() needs it at build time.\n ...(images ? { sharp: '^0.34.0' } : {}),\n // `build` runs `astro check`, so the checker is a build dependency, not a\n // dev one — as create-astro writes it. As devDependencies they were left\n // out by every install with NODE_ENV=production (a worker image, a host's\n // production build), and Astro then asks to install them: with no\n // terminal the build fails, with CI set `astro check` exits 0 unchecked.\n '@astrojs/check': '^0.9.0',\n typescript: '^5.7.0',\n },\n }\n files['package.json'] = stableJson(pkg)\n\n files['astro.config.mjs'] = [\n `import { defineConfig } from 'astro/config'`,\n ...(tailwind ? [`import tailwindcss from '@tailwindcss/vite'`] : []),\n ...(sitemap ? [`import sitemap from '@astrojs/sitemap'`] : []),\n ``,\n ...(sitemap ? sitemapFilterDeclarations(noindex) : []),\n `export default defineConfig({`,\n // Canonical URLs and sitemaps hang off \\`site\\` — for a migration, SEO\n // continuity is the point, so the source site's URL always lands here.\n // Astro rejects an empty string as an invalid URL and refuses to build, so\n // with no site the key is left out; the emitter warns about that instead.\n ...(ir.site.url ? [` site: ${JSON.stringify(ir.site.url)},`] : []),\n // A site whose addresses end in a slash is built as directories\n // (/hello/index.html); one whose addresses do not, as files (/hello.html),\n // which hosts serve at /hello — either way at the source's own address.\n ...(options.trailingSlash === false\n ? [` build: { format: 'file' },`, ` trailingSlash: 'never',`]\n : [` build: { format: 'directory' },`]),\n ...(redirectsConfig ? [redirectsConfig] : []),\n ...(sitemap ? [` integrations: [${sitemapIntegration(noindex)}],`] : []),\n // Hosts content images may be optimized from — the same list the emitted\n // src/lib/optimize-images.ts checks before calling getImage().\n ...(imageConfig ? [imageConfig] : []),\n ...(tailwind ? [` vite: { plugins: [tailwindcss()] },`] : []),\n `})`,\n ``,\n ].join('\\n')\n\n // Astro wants a tsconfig in every project — editors and the compiler read\n // it even in JS-only projects, and the emitted src/lib/fill.ts is TS.\n files['tsconfig.json'] = `${JSON.stringify({ extends: 'astro/tsconfigs/base', include: ['.astro/types.d.ts', '**/*'], exclude: ['dist', 'public', 'node_modules'] }, null, 2)}\\n`\n\n if (tailwind) files['src/styles/modern.css'] = modernCss(ir.tokens)\n\n if (options.sitemap !== false) files['public/robots.txt'] = robotsTxt(ir.site.url)\n\n files['src/lib/fill.ts'] = FILL_TS\n if (images) {\n files['src/lib/images.ts'] = IMAGES_TS\n files['src/lib/optimize-images.ts'] = optimizeImagesTs(\n imagePatterns(options, runtime),\n options.images?.widths ?? DEFAULT_IMAGE_WIDTHS,\n options.images?.sizes ?? DEFAULT_IMAGE_SIZES,\n )\n }\n return files\n}\n\n/**\n * `robots.txt` allowing crawlers and naming the sitemap.\n *\n * The `Sitemap:` line must be an absolute URL — a relative one is invalid and\n * crawlers ignore it. Without a site to make it absolute the line is left out\n * rather than pointed at a build host; the emitter warns about the missing site\n * separately.\n *\n * Nothing is disallowed. A WordPress robots.txt typically keeps crawlers out of\n * `/wp-admin/`; the migrated site has no such path, so carrying the rule over\n * would only restrict a site that no longer needs restricting.\n *\n * The sitemap is resolved under the site's own path, not the host root: a site\n * at `https://example.com/blog` serves its build output, sitemap included, under\n * `/blog/`.\n */\nexport function robotsTxt(siteUrl: string | undefined): string {\n const lines = ['User-agent: *', 'Allow: /']\n if (siteUrl) {\n const base = siteUrl.endsWith('/') ? siteUrl : `${siteUrl}/`\n lines.push('', `Sitemap: ${new URL('sitemap-index.xml', base).href}`)\n }\n return `${lines.join('\\n')}\\n`\n}\n\n/** Tailwind 4 evolution layer: CSS-first config with the site's extracted tokens. */\nfunction modernCss(tokens: DesignTokens | undefined): string {\n const lines: string[] = [`@import 'tailwindcss';`, ``]\n const theme: string[] = []\n const push = (prefix: string, map: Record<string, string> | undefined) => {\n for (const [key, value] of Object.entries(map ?? {})) theme.push(` --${prefix}-${key}: ${value};`)\n }\n push('color', tokens?.colors)\n push('font', tokens?.font_families)\n push('text', tokens?.font_sizes)\n push('spacing', tokens?.spacing)\n push('breakpoint', tokens?.breakpoints)\n if (theme.length) lines.push(`@theme {`, ...theme, `}`)\n return `${lines.join('\\n')}\\n`\n}\n\nconst FILL_TS = `// Emitted by @contentrain/emitter-astro — the template runtime.\n//\n// Chrome, item templates and attribute values carry three marker forms:\n// @@mark@@ — a value, escaped\n// @@mark_html@@ — a value, raw HTML\n// <!--@@repeat:list@@-->…<!--@@/repeat@@--> — once per list item\n// <!--@@if:name@@-->…<!--@@/if@@--> — only when name has a value\n// Rendering order is repeats → conditionals → marks, so a repeat body may hold\n// conditionals and marks that only make sense per item.\n\n/** Where page content splices into the body chrome — must match @contentrain/types CHROME_BODY_SLOT. */\nexport const BODY_SLOT = '<!--@@body@@-->'\n\nconst REPEAT_RE = /<!--@@repeat:([a-z0-9_]+)(?:\\\\|([\\\\s\\\\S]*?))?@@-->([\\\\s\\\\S]*?)<!--@@\\\\/repeat@@-->/gi\nconst IF_RE = /<!--@@if:(!?)([a-z0-9_]+)@@-->([\\\\s\\\\S]*?)<!--@@\\\\/if@@-->/gi\nconst MARK_RE = /@@([a-z0-9_]+)@@/gi\n\nexport type Values = Record<string, unknown>\n\nexport const esc = (value: unknown): string =>\n String(value ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;')\n\nconst isFilled = (value: unknown): boolean =>\n Array.isArray(value) ? value.length > 0 : value !== undefined && value !== null && value !== ''\n\n/**\n * Replace @@marks@@. Values are escaped unless the mark name ends in _html —\n * escaping is the default so content-derived text can never break the page,\n * and the _html opt-in exists for themes that print a post's own markup\n * (full content in a list card, a link-bearing excerpt).\n */\nexport function fillMarks(html: string, values: Values): string {\n return html.replace(MARK_RE, (_all, key: string) => {\n const value = values[key]\n if (Array.isArray(value)) return value.map((v) => esc(v && typeof v === 'object' && 'name' in v ? (v as { name: unknown }).name : v)).join(', ')\n return key.toLowerCase().endsWith('_html') ? String(value ?? '') : esc(value ?? '')\n })\n}\n\n/** Expand repeat blocks; each item renders the inner fragment with its own values. */\nexport function expandRepeats(html: string, values: Values): string {\n return html.replace(REPEAT_RE, (_all, name: string, sep: string | undefined, inner: string) => {\n const list = values[name]\n if (!Array.isArray(list) || list.length === 0) return ''\n return list\n .map((item, index) => {\n // An object item prints its name at @@item@@ and exposes item_<key> for\n // the rest, so a template written for string terms keeps working when\n // the producer starts sending { name, link }.\n const record = item && typeof item === 'object' ? (item as Record<string, unknown>) : undefined\n const itemValues: Values = { ...values, item: record && 'name' in record ? record.name : item, item_index: String(index) }\n if (record) {\n for (const [k, v] of Object.entries(record)) itemValues['item_' + k] = v\n }\n return renderTemplate(inner, itemValues)\n })\n .join(sep ?? '')\n })\n}\n\n/** Drop conditional blocks whose value is empty (or present, when negated). */\nexport function applyConditions(html: string, values: Values): string {\n return html.replace(IF_RE, (_all, negate: string, name: string, inner: string) =>\n isFilled(values[name]) !== (negate === '!') ? inner : '',\n )\n}\n\n/** Full render: repeats → conditionals → marks. */\nexport function renderTemplate(html: string, values: Values): string {\n return fillMarks(applyConditions(expandRepeats(html, values), values), values)\n}\n\n/** Fill marks inside attribute values (per-page classes like postid-123). */\nexport function fillAttrs(attrs: Record<string, string> | undefined, values: Values): Record<string, string> {\n const out: Record<string, string> = {}\n for (const [name, value] of Object.entries(attrs ?? {})) out[name] = fillMarks(value, values)\n return out\n}\n\n/**\n * Compose the body chrome with the page content. The split happens BEFORE any\n * rendering: the @@…@@ pattern would otherwise consume the @@body@@ inside the\n * marker comment, leaving <!----> behind and silently dropping the content\n * (measured cost on a real page: 49.8 vs 97.8).\n */\nexport function composeBody(chromeBody: string, values: Values, content: string): string {\n return chromeBody\n .split(BODY_SLOT)\n .map((part) => renderTemplate(part, values))\n .join(content)\n}\n\n/** Component mount marker — must match @contentrain/types CHROME_COMPONENT_OPEN / CHROME_COMPONENT_CLOSE. */\nexport const COMPONENT_OPEN = '<!--@@component:'\nexport const COMPONENT_CLOSE = '@@-->'\nconst COMPONENT_RE = /<!--@@component:([^@\\\\s]+)@@-->/g\n\nexport interface BodyPart {\n html: string\n /** A mount point: the component id the marker named. */\n component?: string\n}\n\n/**\n * Split rendered chrome at component markers into html parts interleaved with\n * mount points, so the layout can render a real component where the theme's\n * comments form (or contact form) stood. The parts are output in order, so the\n * page's HTML is exactly the chrome with the component's markup at the marker —\n * no parser sees the pieces separately.\n */\nexport function splitComponents(html: string): BodyPart[] {\n const parts: BodyPart[] = []\n let last = 0\n for (const match of html.matchAll(COMPONENT_RE)) {\n parts.push({ html: html.slice(last, match.index) })\n parts.push({ html: '', component: match[1] ?? '' })\n last = match.index + match[0].length\n }\n parts.push({ html: html.slice(last) })\n return parts\n}\n\n/** Where a list section's wrapper takes its items — must match @contentrain/types LIST_ITEMS_SLOT. */\nexport const ITEMS_SLOT = '<!--@@items@@-->'\n\nexport interface ListSection {\n template: string\n wrapper?: string\n count?: number\n}\n\n/**\n * Render a list in sections. Themes often render the newest post as a big card\n * and the rest as a grid — two templates in two containers — so a list is a\n * sequence of sections, each taking a count of items (or the remainder).\n */\nexport function renderSections<T>(\n sections: ListSection[],\n items: T[],\n values: (item: T) => Values,\n): string {\n let index = 0\n const out: string[] = []\n for (const section of sections) {\n const take = section.count ?? items.length - index\n const slice = items.slice(index, index + take)\n index += slice.length\n if (slice.length === 0) continue\n const rendered = slice.map((item) => renderTemplate(section.template, values(item))).join('')\n out.push(section.wrapper ? section.wrapper.split(ITEMS_SLOT).join(rendered) : rendered)\n }\n return out.join('')\n}\n\n/**\n * Render a query's results for a region inside a page — a \"recent posts\" block\n * in an article's chrome, not a list page.\n *\n * A list PAGE maps each result set to a route, so several are expected. A\n * region has no route to distinguish them, so exactly one is expected and\n * anything else is a build error rather than a guess: picking the first would\n * silently render one category's posts under every category.\n *\n * Without item markup the region gets the same plain list a list page does,\n * and the emitter has warned. An empty string here was a region that vanished\n * from every page with nothing anywhere saying so.\n */\nexport function renderQuery(pages: EmittedQueryPage[], id: string): string {\n if (pages.length !== 1) {\n throw new Error(\n 'Query \"' + id + '\" fills a page region, so it must have exactly one result set; got '\n + pages.length + '. A region has no route parameter to choose between them.',\n )\n }\n return renderQueryPage(pages[0]!)\n}\n\n/**\n * One result set as markup: its sections, its single item template, or — when\n * the producer supplied neither — a plain list of links. The plain list is not\n * fidelity and the emitter warns wherever it will be used; it exists so the\n * items are still there, because a list that renders nothing looks like a list\n * with no posts.\n */\nexport function renderQueryPage(page: EmittedQueryPage): string {\n const sections = page.sections?.length ? page.sections : page.item_template ? [{ template: page.item_template }] : []\n if (sections.length) return renderSections(sections, page.items, postMarks)\n return '<ul class=\"cr-post-list\">'\n + page.items.map((item) => '<li><a href=\"/' + item.slug + '/\">' + esc(item.title) + '</a></li>').join('')\n + '</ul>'\n}\n\n// ─── SEO helpers ───\n\n/**\n * A meta description from free text: markup stripped, whitespace collapsed,\n * cut at a word boundary. The cap is a rendering limit, not an editorial\n * choice — an excerpt is a paragraph and a description tag is a line.\n */\nexport function seoDescription(text: string | undefined, max = 300): string | undefined {\n if (!text) return undefined\n const flat = text.replace(/<[^>]*>/g, ' ').replace(/\\\\s+/g, ' ').trim()\n if (!flat) return undefined\n if (flat.length <= max) return flat\n const cut = flat.slice(0, max)\n const space = cut.lastIndexOf(' ')\n // Concatenation, not a template literal: this file is emitted from one, and\n // an unescaped interpolation would be evaluated at emit time.\n return (space > max * 0.6 ? cut.slice(0, space) : cut).trimEnd() + '…'\n}\n\n/**\n * Absolute URL for a social tag or a canonical link. Crawlers do not resolve\n * relative og:image or og:url, so a value that cannot be made absolute is\n * dropped rather than emitted half-formed. Needs \\`site\\` in astro.config.mjs.\n */\nexport function absoluteUrl(value: string | undefined, site: URL | undefined): string | undefined {\n if (!value) return undefined\n if (/^https?:\\\\/\\\\//i.test(value)) return value\n if (!site) return undefined\n try {\n return new URL(value, site).toString()\n } catch {\n return undefined\n }\n}\n\nexport interface ImageMeta {\n width?: number\n height?: number\n type?: string\n}\n\n/**\n * The \\`og:image:*\\` values worth printing. A width that is not a positive\n * integer, or a type that is not an image MIME type, would be a wrong tag\n * rather than a missing one, so it is dropped.\n */\nexport function imageMetaTags(meta: ImageMeta | undefined): { width?: string; height?: string; type?: string } {\n const size = (n: unknown) => (typeof n === 'number' && Number.isInteger(n) && n > 0 ? String(n) : undefined)\n const type = typeof meta?.type === 'string' && /^image\\\\/[a-z0-9.+-]+$/i.test(meta.type) ? meta.type.toLowerCase() : undefined\n return { width: size(meta?.width), height: size(meta?.height), type }\n}\n\n/**\n * JSON-LD payload for a \\`<script type=\"application/ld+json\">\\`. \\`<\\` is escaped\n * so a value containing \\`</script>\\` cannot close the block and inject markup.\n */\nexport function jsonLd(value: unknown): string {\n return JSON.stringify(value).replace(/</g, '\\\\\\\\u003c')\n}\n\nexport interface Breadcrumb {\n name: string\n path: string\n}\n\n/**\n * A trail every crumb of which can be printed, or undefined. A crumb needs a\n * name and a site-root-relative path; one bad crumb drops the trail, because a\n * breadcrumb with a hole in it states a hierarchy the site does not have.\n */\nexport function validTrail(trail: unknown): Breadcrumb[] | undefined {\n if (!Array.isArray(trail) || !trail.length) return undefined\n const ok = trail.every((c) => c && typeof c === 'object'\n && typeof (c as Breadcrumb).name === 'string' && (c as Breadcrumb).name.trim() !== ''\n && typeof (c as Breadcrumb).path === 'string'\n && !(c as Breadcrumb).path.includes(String.fromCharCode(92))\n && ![...(c as Breadcrumb).path].some((char) => char.charCodeAt(0) <= 32) && /^\\\\/(?!\\\\/)/.test((c as Breadcrumb).path))\n return ok ? (trail as Breadcrumb[]) : undefined\n}\n\nexport interface PageStructuredDataInput {\n url?: string\n site?: URL\n /** The document title: the page node's name. */\n title: string\n /** The entry's own title, when \\`title\\` is one an SEO plugin composed: Article headline, last crumb. */\n headline?: string\n description?: string\n image?: string\n locale?: string\n article: boolean\n pageType?: 'WebPage' | 'CollectionPage'\n publishedAt?: string\n modifiedAt?: string\n author?: string\n /** The author's page (their archive), absolute: the Article author's \\`url\\`. */\n authorUrl?: string\n /** On an author's archive: who the page is about — it becomes a ProfilePage whose main entity is this Person. */\n profile?: AuthorProfile\n /** false: the site's addresses end without a slash — an author's page is named in that form. */\n trailingSlash?: boolean\n siteName?: string\n /** \\`@id\\` of the site's own WebSite node, when its head declares one. */\n websiteId?: string\n /** \\`@id\\` of the node the kept head says publishes the site (Organization or Person). */\n publisherId?: string\n /** What \\`image\\` is — an Article image with its size is an ImageObject. */\n imageMeta?: ImageMeta\n breadcrumbs?: Breadcrumb[]\n}\n\n/**\n * Open Graph's locale form, \\`ll_RR\\` (\\`tr_TR\\`), from a BCP 47 tag\n * (\\`tr-TR\\`). A tag without a region has no OG form — guessing one would\n * name the wrong country — so it gives undefined and the tag is left out.\n */\nexport function ogLocale(tag: string | undefined): string | undefined {\n const match = /^([a-z]{2,3})[-_]([a-z]{2})$/i.exec(tag ?? '')\n return match ? match[1]!.toLowerCase() + '_' + match[2]!.toUpperCase() : undefined\n}\n\n/** An author as their archive page presents them: the Person a ProfilePage is about. */\nexport interface AuthorProfile {\n name: string\n /** The author's bio. */\n description?: string\n /** Avatar or photo, absolute or site-root-relative. */\n image?: string\n /** The author's profiles elsewhere (social accounts, a personal site) — schema.org \\`sameAs\\`. */\n same_as?: string[]\n}\n\n/**\n * An address on this site in the form its pages are named in — absolute, the\n * page path in the site's trailing-slash form, no query or fragment — so a\n * reference to a page and the page itself name one URL. Another host's\n * address is kept as given.\n */\nfunction siteAddress(value: string | undefined, site: URL | undefined, trailingSlash = true): string | undefined {\n const absolute = absoluteUrl(value, site)\n if (!absolute || !site) return absolute\n const url = new URL(absolute)\n if (url.host.toLowerCase() !== site.host.toLowerCase()) return absolute\n return absoluteUrl(pagePath(url.pathname, trailingSlash), site)\n}\n\n/** An Article image: an ImageObject when its size is known, the bare URL otherwise. */\nfunction articleImage(url: string, meta: ImageMeta | undefined): string | Record<string, unknown> {\n const tags = imageMetaTags(meta)\n if (!tags.width || !tags.height) return url\n return { '@type': 'ImageObject', url, width: Number(tags.width), height: Number(tags.height) }\n}\n\n/**\n * Who publishes the article: the node the site's own head declares, by\n * \\`@id\\`, so the logo and profile it carries count; else the site by name\n * and address. Nothing is invented beyond the name the site gives itself.\n */\nfunction publisher(input: PageStructuredDataInput): Record<string, unknown> | undefined {\n if (input.publisherId) return { '@id': input.publisherId }\n if (!input.siteName) return undefined\n // The site's own address, path included: a site under /blog/ is not its host.\n const home = input.site ? new URL(input.site.pathname.endsWith('/') ? input.site.href : input.site.href + '/').toString() : undefined\n return { '@type': 'Organization', name: input.siteName, ...(home ? { url: home } : {}) }\n}\n\n/**\n * The page's structured data as one graph: the page itself (WebPage, or\n * CollectionPage for a list), its breadcrumb trail, and on an entry the\n * Article whose main entity it is — linked by \\`@id\\` rather than repeated.\n *\n * The page node needs an absolute address to be anything; without \\`site\\`\n * only an entry's Article remains, as before. \\`isPartOf\\` points at the site's\n * own WebSite node when the kept head declares one — never at a WebSite this\n * function invents.\n */\nexport function pageStructuredData(input: PageStructuredDataInput): Record<string, unknown> | undefined {\n const graph: Array<Record<string, unknown>> = []\n const { url } = input\n const trail = url ? validTrail(input.breadcrumbs) : undefined\n const crumbs = trail\n ? trail.map((c) => ({ name: c.name, item: absoluteUrl(c.path, input.site) }))\n : []\n const breadcrumbId = url && trail && crumbs.every((c) => c.item) ? url + '#breadcrumb' : undefined\n const authorUrl = siteAddress(input.authorUrl, input.site, input.trailingSlash)\n // An author's archive is about a person: the page is a ProfilePage and the\n // Person its main entity, at the page's own address — the one the author's\n // articles name as their author's url.\n const sameAs = (input.profile?.same_as ?? []).filter((u) => /^https?:\\\\/\\\\//i.test(u))\n const profile = url && input.profile?.name ? {\n '@type': 'Person',\n '@id': url + '#person',\n name: input.profile.name,\n url,\n ...(input.profile.description ? { description: input.profile.description } : {}),\n ...(absoluteUrl(input.profile.image, input.site) ? { image: absoluteUrl(input.profile.image, input.site) } : {}),\n ...(sameAs.length ? { sameAs } : {}),\n } : undefined\n if (url) {\n graph.push({\n '@type': profile ? 'ProfilePage' : input.pageType ?? 'WebPage',\n '@id': url,\n url,\n ...(input.title ? { name: input.title } : {}),\n ...(input.description ? { description: input.description } : {}),\n ...(input.locale ? { inLanguage: input.locale } : {}),\n ...(input.websiteId ? { isPartOf: { '@id': input.websiteId } } : {}),\n ...(breadcrumbId ? { breadcrumb: { '@id': breadcrumbId } } : {}),\n ...(profile ? { mainEntity: { '@id': profile['@id'] } } : {}),\n // A page is dated as its entry is — WordPress pages have dates too.\n ...(input.publishedAt ? { datePublished: input.publishedAt } : {}),\n ...(input.modifiedAt ? { dateModified: input.modifiedAt } : {}),\n })\n }\n if (profile) graph.push(profile)\n if (breadcrumbId) {\n graph.push({\n '@type': 'BreadcrumbList',\n '@id': breadcrumbId,\n itemListElement: [...crumbs, { name: input.headline ?? input.title, item: url }].map((c, i) => ({\n '@type': 'ListItem',\n position: i + 1,\n name: c.name,\n item: c.item,\n })),\n })\n }\n if (input.article) {\n graph.push({\n '@type': 'Article',\n headline: input.headline ?? input.title,\n ...(input.description ? { description: input.description } : {}),\n ...(input.image ? { image: [articleImage(input.image, input.imageMeta)] } : {}),\n ...(input.publishedAt ? { datePublished: input.publishedAt } : {}),\n ...(input.modifiedAt ? { dateModified: input.modifiedAt } : {}),\n // The author by the same @id their archive's ProfilePage gives its Person,\n // so the two are one entity; name and url stay for a reader of this page alone.\n ...(input.author ? { author: { '@type': 'Person', ...(authorUrl ? { '@id': authorUrl + '#person' } : {}), name: input.author, ...(authorUrl ? { url: authorUrl } : {}) } } : {}),\n ...(url ? { mainEntityOfPage: { '@id': url } } : {}),\n ...(publisher(input) ? { publisher: publisher(input) } : {}),\n })\n }\n return graph.length ? { '@context': 'https://schema.org', '@graph': graph } : undefined\n}\n\nconst SITE_SEARCH = 'SearchAction'\n\n/**\n * An \\`@id\\` compared as an address: the scheme, the host's case and a slash\n * before the fragment are not part of it, so \\`https://a.com/#website\\` and\n * \\`http://A.com#website\\` name one node — the head and a page's graph can\n * each have been rewritten to the new origin in their own way.\n */\nconst ldIdKey = (id: string): string => id.trim()\n .replace(/^(?:https?:)?\\\\/\\\\/[^/?#]*/i, (origin) => origin.toLowerCase().replace(/^https?:/, ''))\n .replace(/\\\\/+(?=#|$)/, '')\n\n/**\n * The page's structured data as the source's SEO plugin rendered it, ready to\n * print — or undefined, and the generated graph is used. What the plugin\n * described is kept as it was (FAQ, HowTo, Product, its WebPage and Article)\n * except a WebSite's SearchAction, which would be false on the migrated site\n * (a static site has no ?s= search), and the nodes the kept head already\n * carries (\\`headIds\\` — WebSite, Organization, its logo), which every page\n * would otherwise print twice.\n */\nexport function sourceStructuredData(schema: unknown, headIds: string[] = []): Record<string, unknown> | undefined {\n if (!schema || typeof schema !== 'object') return undefined\n const root = schema as Record<string, unknown>\n const graph = root['@graph']\n const nodes: unknown[] = Array.isArray(schema) ? schema : Array.isArray(graph) ? graph : [schema]\n const typesOf = (node: Record<string, unknown>): string[] =>\n (Array.isArray(node['@type']) ? node['@type'] : [node['@type']]).filter((t): t is string => typeof t === 'string')\n const inHead = new Set(headIds.map(ldIdKey))\n const kept: Array<Record<string, unknown>> = []\n for (const value of nodes) {\n if (!value || typeof value !== 'object' || Array.isArray(value)) continue\n let node = value as Record<string, unknown>\n if (!typesOf(node).length) continue\n const website = typesOf(node).some((t) => t.toLowerCase() === 'website')\n if (typeof node['@id'] === 'string' && inHead.has(ldIdKey(node['@id']))) continue\n if (website && node['potentialAction'] !== undefined) {\n const action = node['potentialAction']\n const actions = (Array.isArray(action) ? action : [action]).filter((a) =>\n !(a && typeof a === 'object' && typesOf(a as Record<string, unknown>).includes(SITE_SEARCH)))\n const { potentialAction: _, ...rest } = node\n node = actions.length ? { ...rest, potentialAction: Array.isArray(action) ? actions : actions[0] } : rest\n }\n kept.push(node)\n }\n if (!kept.length) return undefined\n const context = Array.isArray(schema) ? undefined : root['@context']\n return { '@context': context ?? 'https://schema.org', '@graph': kept }\n}\n\n// ─── Feed and llms.txt ───\n\n/**\n * Site-root address of an entry: its route pattern filled with the entry's\n * parameters — the values getStaticPaths builds the page from — in the\n * directory format the build writes. Undefined when a parameter is missing.\n */\nexport function entryAddress(pattern: string, params: Record<string, string | undefined>, trailingSlash = true): string | undefined {\n const segments = pattern.replace(/^\\\\/+|\\\\/+$/g, '').split('/').filter(Boolean)\n if (!segments.length) return '/'\n const out: string[] = []\n for (const segment of segments) {\n if (!segment.startsWith(':')) {\n out.push(segment)\n continue\n }\n const value = params[segment.slice(1).replace(/\\\\*$/, '')]?.replace(/^\\\\/+|\\\\/+$/g, '')\n if (!value) return undefined\n out.push(value)\n }\n return '/' + out.join('/') + (trailingSlash ? '/' : '')\n}\n\n/**\n * The address a built page is served at, from the path Astro gives it. The\n * file build names a page \\`/hello.html\\` and the host serves it at\n * \\`/hello\\`; the source site's own form — with or without the trailing\n * slash — is what its canonical, og:url and hreflang must name.\n */\nexport function pagePath(pathname: string, trailingSlash = true): string {\n const path = pathname.replace(/\\\\/index\\\\.html$/, '/').replace(/\\\\.html$/, '')\n if (path === '/' || path === '') return '/'\n const bare = path.replace(/\\\\/+$/, '')\n return trailingSlash ? bare + '/' : bare\n}\n\n/** One page as a feed item or an llms.txt link. */\nexport interface SiteLink {\n title: string\n url: string\n description?: string\n /** ISO 8601. */\n date?: string\n author?: string\n}\n\n/**\n * A collection's entries as absolute links, newest first; undated entries\n * keep their order after the dated ones. Only the entries in \\`locale\\` —\n * an entry without one is in the site's default, \\`siteLocale\\` — and with\n * \\`indexedOnly\\`, none the source kept out of search.\n */\nexport function entryLinks(posts: EmittedPost[], pattern: string, site: URL | undefined, locale: string, siteLocale: string, indexedOnly = false, trailingSlash = true): SiteLink[] {\n const links: SiteLink[] = []\n for (const post of posts) {\n if ((post.locale ?? siteLocale) !== locale) continue\n if (indexedOnly && post.noindex) continue\n const url = absoluteUrl(entryAddress(pattern, { ...(post.params ?? {}), slug: post.slug }, trailingSlash), site)\n if (!url) continue\n links.push({\n title: post.title,\n url,\n description: seoDescription(post.description ?? post.excerpt),\n date: post.published_at,\n author: post.author,\n })\n }\n const time = (link: SiteLink) => (link.date ? Date.parse(link.date) || 0 : 0)\n return links\n .map((link, index) => ({ link, index }))\n .sort((a, b) => time(b.link) - time(a.link) || a.index - b.index)\n .map((entry) => entry.link)\n}\n\n/** Text for XML: escaped, and without the control characters XML 1.0 forbids. */\nconst xmlText = (value: string): string => value\n .replace(/[\\\\u0000-\\\\u0008\\\\u000B\\\\u000C\\\\u000E-\\\\u001F]/g, '')\n .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;')\n\nexport interface RssChannel {\n title: string\n /** The site's home page, absolute. */\n link: string\n /** The feed's own address, absolute. */\n self: string\n description?: string\n language?: string\n items: SiteLink[]\n}\n\n/**\n * RSS 2.0, the format WordPress serves at /feed/. The channel's\n * \\`lastBuildDate\\` is its newest item's date, so rebuilding unchanged\n * content writes the same bytes.\n */\nexport function rssFeed(channel: RssChannel): string {\n const rfc822 = (iso: string | undefined): string | undefined => {\n const time = iso ? Date.parse(iso) : NaN\n return Number.isNaN(time) ? undefined : new Date(time).toUTCString()\n }\n const tag = (name: string, value: string | undefined) => (value ? '<' + name + '>' + xmlText(value) + '</' + name + '>' : '')\n const newest = channel.items.map((item) => rfc822(item.date)).find(Boolean)\n const items = channel.items.map((item) => [\n '<item>',\n tag('title', item.title),\n tag('link', item.url),\n '<guid isPermaLink=\"true\">' + xmlText(item.url) + '</guid>',\n tag('pubDate', rfc822(item.date)),\n tag('dc:creator', item.author),\n tag('description', item.description),\n '</item>',\n ].filter(Boolean).join(''))\n return [\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n '<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\">',\n '<channel>',\n tag('title', channel.title),\n tag('link', channel.link),\n '<atom:link href=\"' + xmlText(channel.self) + '\" rel=\"self\" type=\"application/rss+xml\"/>',\n // Required by RSS 2.0; a site without a tagline gets its title.\n tag('description', channel.description || channel.title),\n tag('language', channel.language),\n tag('lastBuildDate', newest),\n ...items,\n '</channel>',\n '</rss>',\n ].filter(Boolean).join('\\\\n') + '\\\\n'\n}\n\nexport interface LlmsSection {\n name: string\n links: SiteLink[]\n}\n\n/**\n * /llms.txt (llmstxt.org): the site's name, its tagline, and a section of\n * links per kind of page — what a language model reads to find its way\n * around the site, as Yoast's generator writes it.\n */\nexport function llmsTxt(input: { title: string; description?: string; sections: LlmsSection[] }): string {\n const line = (value: string) => value.replace(/\\\\s+/g, ' ').trim()\n const label = (value: string) => line(value).replace(/[\\\\[\\\\]\\\\\\\\]/g, '\\\\\\\\$&')\n const href = (url: string) => url.replace(/\\\\(/g, '%28').replace(/\\\\)/g, '%29').replace(/\\\\s/g, '%20')\n const out = ['# ' + line(input.title)]\n if (input.description && line(input.description)) out.push('', '> ' + line(input.description))\n for (const section of input.sections) {\n if (!section.links.length) continue\n out.push('', '## ' + line(section.name), '')\n for (const link of section.links) {\n const description = link.description ? line(link.description) : ''\n out.push('- [' + label(link.title) + '](' + href(link.url) + ')' + (description ? ': ' + description : ''))\n }\n }\n return out.join('\\\\n') + '\\\\n'\n}\n\n/** Emitted stylesheets live under /styles/legacy/ — pages reference them by file name. */\nexport const cssHref = (file: string): string => '/styles/legacy/' + (file.split('/').pop() ?? file)\n\n/** A term as a list card prints it: name, and the archive it links to. */\nexport interface TermRef {\n name: string\n link?: string\n}\n\n/** The display name of a term given as a string or a TermRef. */\nexport const termName = (term: string | TermRef): string => (typeof term === 'string' ? term : term.name)\n\nexport interface MarkablePost {\n slug: string\n title: string\n body?: string\n dates?: string[]\n author?: string\n author_first?: string\n author_last?: string\n authors?: string[]\n /**\n * Strings, or objects for themes whose term lists link each term\n * (\\`<!--@@repeat:terms@@-->\\` with \\`item_name\\` / \\`item_link\\`). The\n * \\`term{n}\\` and \\`terms\\` marks print the name either way.\n */\n terms?: Array<string | TermRef>\n featured?: string[]\n excerpt?: string\n excerpt_html?: string\n marks?: Record<string, unknown>\n}\n\n/**\n * The mark vocabulary: title, author (+ first/last), date{n}, term{n}, feat{n},\n * excerpt, slug — plus \\`terms\\`/\\`authors\\` as LISTS for repeat blocks, the\n * _html variants for raw insertion, and any producer-supplied extras.\n */\nexport function postMarks(post: MarkablePost): Values {\n const values: Values = {\n title: post.title,\n author: post.author ?? '',\n author_first: post.author_first ?? '',\n author_last: post.author_last ?? '',\n excerpt: post.excerpt ?? '',\n // The plain excerpt is text: escaped here, or a decoded \\`<script>\\` in it would print as markup.\n excerpt_html: post.excerpt_html ?? esc(post.excerpt ?? ''),\n body_html: post.body ?? '',\n slug: post.slug,\n feat: post.featured?.[0] ?? '',\n // Objects stay objects for repeat blocks (item_name / item_link); the joined\n // \\`terms\\` mark prints names, so fillMarks never sees [object Object].\n terms: (post.terms ?? []).map((t) => (typeof t === 'string' ? { name: t, link: '' } : { name: t.name, link: t.link ?? '' })),\n terms_names: (post.terms ?? []).map(termName),\n authors: post.authors ?? (post.author ? [post.author] : []),\n }\n for (const [i, d] of (post.dates ?? []).entries()) values['date' + i] = d\n for (const [i, t] of (post.terms ?? []).entries()) values['term' + i] = termName(t)\n for (const [i, f] of (post.featured ?? []).entries()) values['feat' + i] = f\n return { ...values, ...(post.marks ?? {}) }\n}\n\n/**\n * The shapes of the emitted data files. Pages import JSON and assert these\n * rather than letting TypeScript infer the shape from the file's contents:\n * inference reads only the fields the data HAPPENS to carry, so a site whose\n * posts need no extra route parameters produced a type without \\`params\\` and\n * \\`astro check\\` — which the build runs first — failed on the page that reads\n * it. The contract is what the emitter may write, not what one site wrote.\n */\n/** Content-store address of an entry — what a mounted comments component keys its thread by. */\nexport interface EntryRef {\n model_id: string\n entry_id: string\n locale?: string\n}\n\n/** Share-card values a page's source set by hand; each one left out is derived. */\nexport interface SocialOverride {\n title?: string\n description?: string\n image?: string\n}\n\nexport interface TwitterOverride extends SocialOverride {\n card?: string\n}\n\nexport interface SeoInput {\n title?: string\n headline?: string\n description?: string\n canonical?: string\n image?: string\n imageMeta?: ImageMeta\n alternates?: Array<{ lang: string; path: string }>\n breadcrumbs?: Breadcrumb[]\n pageType?: 'WebPage' | 'CollectionPage'\n type?: 'article' | 'website'\n publishedAt?: string\n modifiedAt?: string\n author?: string\n authorUrl?: string\n profile?: AuthorProfile\n /** An archive page's own feed, site-root-relative — the Seo component links it. */\n feed?: { path: string; title?: string }\n noindex?: boolean\n nofollow?: boolean\n openGraph?: SocialOverride\n twitter?: TwitterOverride\n schema?: unknown\n}\n\n/**\n * The SEO of one entry page. \\`featured\\` holds bare file names — only the\n * producer knows where media is served from — so an image is taken from it\n * only when it already reads as a path; otherwise \\`image\\` supplies it and\n * the tag is omitted rather than pointing at a URL that does not resolve.\n */\nexport function postSeo(post: EmittedPost, kind: 'post' | 'page' = 'post'): SeoInput {\n const featured = (post.featured ?? []).find((f) => f.startsWith('/') || /^https?:/i.test(f))\n return {\n // The document title an SEO plugin composed; the entry's own name stays the headline.\n title: post.seo_title || post.title,\n headline: post.title,\n description: post.description ?? post.excerpt,\n canonical: post.canonical,\n image: post.image ?? featured,\n // The measurements describe \\`image\\`; a \\`featured\\` fallback is another file.\n imageMeta: post.image ? post.image_meta : undefined,\n alternates: post.alternates,\n breadcrumbs: post.breadcrumbs,\n // A WordPress page is a web page, not an article: og:type website, no\n // Article node, no article:* tags — as the source's SEO plugin printed it.\n type: kind === 'page' ? 'website' : 'article',\n publishedAt: post.published_at,\n modifiedAt: post.modified_at,\n author: post.author,\n authorUrl: post.author_url,\n noindex: post.noindex,\n nofollow: post.nofollow,\n openGraph: post.open_graph,\n twitter: post.twitter,\n schema: post.schema,\n }\n}\n\nexport interface EmittedPost extends MarkablePost {\n body: string\n /** Meta description; falls back to the excerpt. */\n description?: string\n /** Social image, absolute or site-root-relative. */\n image?: string\n image_meta?: ImageMeta\n /** Translations of this entry, itself included, and x-default. Computed by the emitter. */\n alternates?: Array<{ lang: string; path: string }>\n /** Trail to this page, itself excluded. */\n breadcrumbs?: Breadcrumb[]\n /** Canonical override; default is the page's own address. */\n canonical?: string\n /** The author's page (their archive), absolute or site-root-relative. */\n author_url?: string\n /** Document title, when the source composed one apart from \\`title\\`. */\n seo_title?: string\n open_graph?: SocialOverride\n twitter?: TwitterOverride\n /** The source SEO plugin's JSON-LD for this page. */\n schema?: unknown\n /** ISO 8601, for Article structured data. */\n published_at?: string\n modified_at?: string\n /** Route parameters beyond slug — the date parts of a dated permalink, a post id. */\n params?: Record<string, string>\n /** Stylesheets only this page loads. */\n css?: string[]\n locale?: string\n /** Present when the producer bound this post to the content store. */\n entry?: EntryRef\n /** Kept out of search: robots meta, and no sitemap entry. */\n noindex?: boolean\n nofollow?: boolean\n}\n\n/** One static path of a list route, as it appears in the emitted query data. */\nexport interface EmittedQueryPage {\n params: Record<string, string>\n items: EmittedPost[]\n marks?: Record<string, unknown>\n css?: string[]\n item_template?: string\n sections?: ListSection[]\n title?: string\n description?: string\n image?: string\n image_meta?: ImageMeta\n canonical?: string\n breadcrumbs?: Breadcrumb[]\n noindex?: boolean\n nofollow?: boolean\n open_graph?: SocialOverride\n twitter?: TwitterOverride\n schema?: unknown\n /** On an author's archive: who the page is about. */\n profile?: AuthorProfile\n}\n`\n","// Fragment balance check.\n//\n// Chrome is injected as raw HTML, so an unbalanced fragment does not fail the\n// build — the browser silently repairs it and the page loses its layout. That\n// is the failure that cost a page 36 against 100 when body chrome was split\n// into before/after halves. Lifting a header or footer out of the body blob is\n// only safe when each fragment closes what it opens, so the emitter checks and\n// says which tags are dangling rather than trusting the producer's word.\n//\n// This is deliberately not an HTML parser: it skips comments, doctypes and raw\n// text elements, tolerates the tags HTML lets you leave open, and reports what\n// it finds. It answers \"was this split mid-element?\", not \"is this valid HTML?\".\n\nconst VOID_TAGS = new Set([\n 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',\n 'link', 'meta', 'param', 'source', 'track', 'wbr',\n])\n\n/** Tags a browser closes for you. Theme markup is full of them, so counting\n * them would report every real page as broken. */\nconst OPTIONAL_END_TAGS = new Set([\n 'p', 'li', 'dt', 'dd', 'option', 'optgroup', 'thead', 'tbody',\n 'tfoot', 'tr', 'td', 'th', 'rt', 'rp',\n])\n\n/** Elements whose content is text, not markup — `<script>if (a < b)</script>`. */\nconst RAW_TEXT_TAGS = new Set(['script', 'style', 'textarea', 'title'])\n\ninterface Tag {\n name: string\n closing: boolean\n selfClosing: boolean\n}\n\nfunction scanTags(html: string): Tag[] {\n const tags: Tag[] = []\n let i = 0\n while (i < html.length) {\n const lt = html.indexOf('<', i)\n if (lt === -1) break\n if (html.startsWith('<!--', lt)) {\n const end = html.indexOf('-->', lt + 4)\n i = end === -1 ? html.length : end + 3\n continue\n }\n if (html.startsWith('<!', lt) || html.startsWith('<?', lt)) {\n const end = html.indexOf('>', lt)\n i = end === -1 ? html.length : end + 1\n continue\n }\n const head = /^<(\\/?)([a-zA-Z][a-zA-Z0-9-]*)/.exec(html.slice(lt, lt + 64))\n const [matched = '', slash = '', rawName = ''] = head ?? []\n if (!rawName) {\n i = lt + 1\n continue\n }\n // Walk to the tag's `>`, ignoring one inside a quoted attribute value.\n let j = lt + matched.length\n let quote = ''\n while (j < html.length) {\n const ch = html[j]\n if (quote) {\n if (ch === quote) quote = ''\n } else if (ch === '\"' || ch === \"'\") {\n quote = ch\n } else if (ch === '>') {\n break\n }\n j++\n }\n const name = rawName.toLowerCase()\n const closing = slash === '/'\n tags.push({ name, closing, selfClosing: html.slice(lt, j + 1).endsWith('/>') })\n i = j + 1\n if (!closing && RAW_TEXT_TAGS.has(name)) {\n // Searched in `html` itself, not in `html.toLowerCase()`: lowercasing\n // changes the length of some text ('İ' becomes two code units), so an\n // index found in the lowered copy lands past the real `</script` once\n // such a character comes before it — and every raw-text element after a\n // Turkish capital read as never closed. HTML matches tag names by ASCII\n // case only, which is what `i` without `u` does.\n const end = new RegExp(`</${name}`, 'gi')\n end.lastIndex = i\n const close = end.exec(html)?.index ?? -1\n i = close === -1 ? html.length : close\n }\n }\n return tags\n}\n\nexport interface BalanceReport {\n /** Elements opened and never closed — the fragment ends mid-element. */\n unclosed: string[]\n /** Elements closed but never opened — the fragment starts mid-element. */\n unopened: string[]\n}\n\nexport function checkBalance(html: string): BalanceReport {\n const stack: string[] = []\n const unopened: string[] = []\n for (const tag of scanTags(html)) {\n if (VOID_TAGS.has(tag.name) || tag.selfClosing || OPTIONAL_END_TAGS.has(tag.name)) continue\n if (!tag.closing) {\n stack.push(tag.name)\n continue\n }\n const at = stack.lastIndexOf(tag.name)\n if (at === -1) unopened.push(tag.name)\n else stack.length = at\n }\n return { unclosed: stack.toReversed(), unopened }\n}\n\n/** Human-readable balance complaint, or null when the fragment is sound. */\nexport function balanceWarning(html: string): string | null {\n const { unclosed, unopened } = checkBalance(html)\n const parts: string[] = []\n if (unclosed.length) parts.push(`never closed: ${unclosed.join(', ')}`)\n if (unopened.length) parts.push(`closed but never opened: ${unopened.join(', ')}`)\n return parts.length ? parts.join('; ') : null\n}\n","// Per-page SEO: the one head region the emitter owns.\n//\n// A migrated page used to inherit the TEMPLATE page's head verbatim, so every\n// post carried the template's `<link rel=\"canonical\">`, its `og:title` and its\n// Article JSON-LD. That is worse than having none: a whole site canonicalised\n// onto one URL de-indexes itself, and every share card shows the wrong story.\n// SEO continuity is the reason a migration keeps the source addresses at all,\n// so the emitter derives these tags per page from the entry and removes the\n// template's copies rather than letting two of each fight.\n//\n// What stays: everything else the theme put in `<head>` — charset, preloads,\n// feeds, icons, verification tokens, and the structured data that describes\n// the site rather than the page (WebSite, Organization).\n\n/**\n * Tags the emitter owns; a source copy of any of these is replaced, not\n * duplicated. `twitter:site` is the site's handle, the same on every page and\n * not derivable from an entry, so it stays in the head.\n */\nconst TITLE_RE = /<title\\b[^>]*>[\\s\\S]*?<\\/title>\\s*/gi\nconst META_RE = /<meta\\b[^>]*\\b(?:name|property)\\s*=\\s*[\"'](?:description|robots|googlebot|og:[^\"']*|twitter:(?!site[\"'])[^\"']*)[\"'][^>]*>\\s*/gi\nconst CANONICAL_RE = /<link\\b[^>]*\\brel\\s*=\\s*[\"'][^\"']*\\bcanonical\\b[^\"']*[\"'][^>]*>\\s*/gi\n/**\n * A translation link names the template page's translations. An RSS\n * `rel=\"alternate\"` carries no hreflang and stays.\n */\nconst HREFLANG_RE = /<link\\b(?=[^>]*\\brel\\s*=\\s*[\"']?[^\"'>]*\\balternate\\b)(?=[^>]*\\bhreflang\\s*=)[^>]*>\\s*/gi\nconst JSONLD_RE = /<script\\b[^>]*\\btype\\s*=\\s*[\"']application\\/ld\\+json[\"'][^>]*>([\\s\\S]*?)<\\/script>\\s*/gi\n\n/**\n * Structured data that describes THIS page. An Article names one post, a\n * WebPage (and every subtype — CollectionPage for an archive, ProfilePage for an\n * author) one address, a BreadcrumbList one page's trail: copied from the\n * template, each is a lie on every other page. Matched by suffix because\n * schema.org names its page and article subtypes that way.\n */\nfunction isPageType(type: string): boolean {\n const t = type.toLowerCase()\n return t.endsWith('page') || t.endsWith('article') || PAGE_POSTINGS.has(t)\n}\nconst PAGE_POSTINGS = new Set(['blogposting', 'liveblogposting', 'socialmediaposting', 'discussionforumposting', 'breadcrumblist'])\n\n/**\n * Structured data that describes the SITE and is true on every page. An\n * SEO plugin writes it into the same `@graph` as the page's own nodes — Yoast\n * and Rank Math put WebSite, Organization, WebPage and BreadcrumbList in one\n * block — so removing page-scoped blocks whole took the site's identity with\n * them.\n */\nconst SITE_TYPES = new Set([\n 'website', 'organization', 'corporation', 'localbusiness', 'newsmediaorganization',\n 'educationalorganization', 'ngo', 'onlinebusiness', 'onlinestore', 'governmentorganization',\n])\n\ntype LdNode = Record<string, unknown>\n\nconst typesOf = (node: unknown): string[] => {\n const type = (node as LdNode | null)?.['@type']\n return (Array.isArray(type) ? type : [type]).filter((t): t is string => typeof t === 'string')\n}\nconst isPageNode = (node: unknown) => typesOf(node).some(isPageType)\nconst isSiteNode = (node: unknown) => typesOf(node).some((t) => SITE_TYPES.has(t.toLowerCase()))\n\n/** Every `{\"@id\": …}` reference anywhere inside a node. */\nfunction references(value: unknown, out = new Set<string>()): Set<string> {\n if (Array.isArray(value)) {\n for (const v of value) references(v, out)\n } else if (value && typeof value === 'object') {\n const node = value as LdNode\n const keys = Object.keys(node)\n if (keys.length === 1 && typeof node['@id'] === 'string') out.add(node['@id'])\n for (const v of Object.values(node)) references(v, out)\n }\n return out\n}\n\n/**\n * A WebSite's SearchAction points at WordPress search (`/?s=`). A static site\n * has no search endpoint, so the claim would be false on the new site.\n */\nfunction withoutSearchAction(node: LdNode): { node: LdNode; dropped: boolean } {\n const action = node['potentialAction']\n const actions = Array.isArray(action) ? action : action === undefined ? [] : [action]\n const kept = actions.filter((a) => !typesOf(a).includes('SearchAction'))\n if (kept.length === actions.length) return { node, dropped: false }\n const { potentialAction: _, ...rest } = node\n return { node: kept.length ? { ...rest, potentialAction: Array.isArray(action) ? kept : kept[0] } : rest, dropped: true }\n}\n\ninterface LdDecision {\n /** The block to keep, re-serialized; undefined to keep the original bytes; null to remove it. */\n json: string | null | undefined\n kept: string[]\n droppedSearch: boolean\n}\n\n/**\n * What to do with one JSON-LD block. A block with no page-scoped node stays as\n * it was. A block with one loses its page-scoped nodes and keeps the site's —\n * WebSite, Organization — together with the nodes those refer to by `@id` (a\n * logo, the person a personal site is published by), never pulling a page node\n * back in. Nothing site-scoped left means the block goes.\n */\nfunction decideLd(json: string): LdDecision {\n let parsed: unknown\n try {\n parsed = JSON.parse(json)\n } catch {\n // Unparseable JSON-LD is left alone: removing markup we cannot read would\n // be guessing, and a broken block is the theme's problem, not ours.\n return { json: undefined, kept: [], droppedSearch: false }\n }\n const graph = (parsed as { '@graph'?: unknown } | null)?.['@graph']\n const nodes: unknown[] = Array.isArray(parsed) ? parsed : Array.isArray(graph) ? graph : [parsed]\n if (!nodes.some(isPageNode)) return { json: undefined, kept: [], droppedSearch: false }\n\n const byId = new Map<string, unknown>()\n for (const node of nodes) {\n const id = (node as LdNode | null)?.['@id']\n if (typeof id === 'string') byId.set(id, node)\n }\n const keep = new Set<unknown>(nodes.filter((n) => isSiteNode(n) && !isPageNode(n)))\n const queue = [...keep]\n while (queue.length) {\n for (const id of references(queue.shift())) {\n const target = byId.get(id)\n if (target && !keep.has(target) && !isPageNode(target)) {\n keep.add(target)\n queue.push(target)\n }\n }\n }\n if (!keep.size) return { json: null, kept: [], droppedSearch: false }\n\n let droppedSearch = false\n const kept = nodes.filter((n) => keep.has(n)).map((n) => {\n if (!typesOf(n).some((t) => t.toLowerCase() === 'website')) return n\n const result = withoutSearchAction(n as LdNode)\n droppedSearch ||= result.dropped\n return result.node\n })\n const context = (parsed as LdNode | null)?.['@context']\n const rebuilt = Array.isArray(parsed)\n ? kept\n : Array.isArray(graph)\n ? { ...(context === undefined ? {} : { '@context': context }), '@graph': kept }\n : kept[0]\n return {\n // `<` escaped so a string value can never close the script element.\n json: JSON.stringify(rebuilt).replace(/</g, '\\\\u003c'),\n kept: [...new Set(kept.flatMap(typesOf))],\n droppedSearch,\n }\n}\n\n/**\n * \\`@id\\` of the WebSite node in a head's structured data, if it declares one\n * — the node the page's WebPage says it is part of. Only what the head says:\n * no id, no link.\n */\nexport function websiteIdOf(html: string): string | undefined {\n for (const match of html.matchAll(JSONLD_RE)) {\n let parsed: unknown\n try {\n parsed = JSON.parse(match[1] ?? '')\n } catch {\n continue\n }\n const graph = (parsed as { '@graph'?: unknown } | null)?.['@graph']\n const nodes: unknown[] = Array.isArray(parsed) ? parsed : Array.isArray(graph) ? graph : [parsed]\n for (const node of nodes) {\n const id = (node as LdNode | null)?.['@id']\n if (typeof id === 'string' && id && typesOf(node).some((t) => t.toLowerCase() === 'website')) return id\n }\n }\n return undefined\n}\n\n/**\n * \\`@id\\` of every node in a head's structured data — the site's own nodes\n * (WebSite, Organization, its logo) that every page already carries. A page's\n * plugin graph repeats them; printing them twice is valid but is noise.\n */\nexport function headLdIdsOf(html: string): string[] {\n const ids: string[] = []\n for (const match of html.matchAll(JSONLD_RE)) {\n let parsed: unknown\n try {\n parsed = JSON.parse(match[1] ?? '')\n } catch {\n continue\n }\n const graph = (parsed as { '@graph'?: unknown } | null)?.['@graph']\n const nodes: unknown[] = Array.isArray(parsed) ? parsed : Array.isArray(graph) ? graph : [parsed]\n for (const node of nodes) {\n const id = (node as LdNode | null)?.['@id']\n if (typeof id === 'string' && id && !ids.includes(id)) ids.push(id)\n }\n }\n return ids\n}\n\n/**\n * `@id` of the node that publishes the site, as the head's structured data\n * says: the WebSite's own `publisher` reference (Yoast and Rank Math write\n * one — an Organization, or the Person a personal site is published by),\n * else the head's first Organization. The Article names it by `@id`, so the\n * logo and profile the site declared count for every post.\n */\nexport function publisherIdOf(html: string): string | undefined {\n // A reference only counts when the node it names is on the page: an Article\n // pointing at an @id nothing declares names no one.\n const declared = new Set(headLdIdsOf(html))\n let organization: string | undefined\n for (const match of html.matchAll(JSONLD_RE)) {\n let parsed: unknown\n try {\n parsed = JSON.parse(match[1] ?? '')\n } catch {\n continue\n }\n const graph = (parsed as { '@graph'?: unknown } | null)?.['@graph']\n const nodes: unknown[] = Array.isArray(parsed) ? parsed : Array.isArray(graph) ? graph : [parsed]\n for (const node of nodes) {\n const types = typesOf(node).map((t) => t.toLowerCase())\n if (types.includes('website')) {\n const ref = (node as LdNode)['publisher']\n const id = (Array.isArray(ref) ? ref[0] : ref) as LdNode | undefined\n if (typeof id?.['@id'] === 'string' && declared.has(id['@id'])) return id['@id']\n }\n const id = (node as LdNode | null)?.['@id']\n if (!organization && typeof id === 'string' && id && isSiteNode(node) && !types.includes('website')) organization = id\n }\n }\n return organization\n}\n\nexport interface StripResult {\n html: string\n /** What was taken out, for the emit warning — never a silent removal. */\n removed: string[]\n /** Site-wide structured-data types kept out of a block that was otherwise page-scoped. */\n kept: string[]\n /**\n * The robots directives of the template's `robots` / `googlebot` tags that\n * are site settings rather than this page's indexing — Yoast's\n * `max-image-preview:large, max-snippet:-1, max-video-preview:-1`. The Seo\n * component prints them on every page, after the page's own noindex/nofollow.\n */\n robots: string[]\n}\n\n/** Directives that say whether THIS page is indexed or followed; everything else is a site setting. */\nconst PAGE_ROBOTS = new Set(['index', 'noindex', 'follow', 'nofollow', 'all', 'none'])\n\nfunction siteRobots(tag: string, into: string[]): void {\n const content = /\\bcontent\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)')/i.exec(tag)\n for (const raw of (content?.[1] ?? content?.[2] ?? '').split(',')) {\n const directive = raw.trim().toLowerCase()\n if (directive && !PAGE_ROBOTS.has(directive) && !into.includes(directive)) into.push(directive)\n }\n}\n\n/** Remove the source head's per-page SEO tags so the emitter's own are the only ones. */\nexport function stripSeoTags(html: string): StripResult {\n const removed: string[] = []\n const kept: string[] = []\n const robots: string[] = []\n const note = (label: string) => {\n if (!removed.includes(label)) removed.push(label)\n }\n let out = html.replace(TITLE_RE, () => {\n note('<title>')\n return ''\n })\n out = out.replace(META_RE, (tag) => {\n const name = /\\b(?:name|property)\\s*=\\s*[\"']([^\"']+)[\"']/i.exec(tag)?.[1] ?? 'meta'\n if (/^(?:robots|googlebot)$/i.test(name)) siteRobots(tag, robots)\n note(name.toLowerCase().startsWith('og:') ? 'og:*' : name.toLowerCase().startsWith('twitter:') ? 'twitter:*' : name)\n return ''\n })\n out = out.replace(CANONICAL_RE, () => {\n note('canonical')\n return ''\n })\n out = out.replace(HREFLANG_RE, () => {\n note('hreflang')\n return ''\n })\n out = out.replace(JSONLD_RE, (tag: string, json: string) => {\n const decision = decideLd(json)\n if (decision.json === undefined) return tag\n note('page-scoped JSON-LD')\n if (decision.droppedSearch) note('WebSite SearchAction')\n for (const type of decision.kept) if (!kept.includes(type)) kept.push(type)\n if (decision.json === null) return ''\n const open = tag.slice(0, tag.indexOf('>') + 1)\n return `${open}${decision.json}</script>\\n`\n })\n return { html: out, removed, kept, robots }\n}\n\n/**\n * Head-only tags found in BODY chrome. A browser that closed `<head>` early\n * leaves the template's canonical and og tags in the body, and a faithful clone\n * carries them there — so the page ends up with the emitter's correct tags AND\n * the template's stale ones, which no fidelity score can see.\n *\n * They are reported, not removed: the body is page content, and `<title>` is\n * legal inside `<svg>`, so cutting tags out of it would break real markup to\n * fix an invisible one. Only unambiguous head tags are looked for.\n */\nexport function bodySeoLeaks(html: string): string[] {\n const found: string[] = []\n if (/<link\\b[^>]*\\brel\\s*=\\s*[\"'][^\"']*\\bcanonical\\b/i.test(html)) found.push('canonical')\n if (/<meta\\b[^>]*\\bproperty\\s*=\\s*[\"']og:/i.test(html)) found.push('og:*')\n if (/<meta\\b[^>]*\\bname\\s*=\\s*[\"']description[\"']/i.test(html)) found.push('description')\n return found\n}\n\n/**\n * `src/components/Seo.astro`. The canonical address comes from `Astro.url` and\n * `Astro.site` rather than from data: it is then, by construction, the address\n * Astro actually generated, and no producer has to recompute a permalink.\n */\nexport const SEO_COMPONENT = `---\n/**\n * Per-page SEO — emitted by @contentrain/emitter-astro.\n *\n * Title, description, canonical, Open Graph, Twitter card and (on entry pages)\n * Article structured data, derived from the entry. The source theme's copies of\n * these tags are removed from the head chrome at emit time, so each appears once.\n *\n * Canonical and absolute URLs need \\`site\\` in astro.config.mjs; without it the\n * canonical link and og:url are omitted rather than pointing at a build host.\n */\nimport { absoluteUrl, imageMetaTags, jsonLd, ogLocale, pagePath, pageStructuredData, seoDescription, sourceStructuredData, type AuthorProfile, type Breadcrumb, type ImageMeta, type SocialOverride, type TwitterOverride } from '../lib/fill'\n\ninterface Props {\n /** The document title — an SEO plugin's composed one when the source had it. */\n title?: string\n /** The entry's own title, when it differs from \\`title\\`: Article headline, last breadcrumb. */\n headline?: string\n /** Meta description; falls back to the entry's excerpt, tags stripped. */\n description?: string\n /** Overrides the generated address — for a page that canonicalises elsewhere. */\n canonical?: string\n /** Social image: absolute, or site-root-relative. */\n image?: string\n /** Size and type of \\`image\\`, as the producer measured it. */\n imageMeta?: ImageMeta\n /** This page's translations, itself included, plus x-default. */\n alternates?: Array<{ lang: string; path: string }>\n /** The trail to this page, itself excluded — the page is appended with its own address. */\n breadcrumbs?: Breadcrumb[]\n /** \\`CollectionPage\\` on a list; \\`WebPage\\` otherwise. */\n pageType?: 'WebPage' | 'CollectionPage'\n /** \\`@id\\` of the site's WebSite node kept in the head chrome, when there is one. */\n websiteId?: string\n /** \\`@id\\` of the node the kept head says publishes the site — the Article's publisher. */\n publisherId?: string\n /** false: the site's addresses end without a slash (\\`/hello\\`), as the source's did. */\n trailingSlash?: boolean\n /** \\`@id\\` of every structured-data node the head chrome keeps — not repeated from \\`schema\\`. */\n headLdIds?: string[]\n /** \\`article\\` on entry pages, \\`website\\` on lists and static pages. */\n type?: 'article' | 'website'\n /** ISO 8601 — structured data only; the displayed date stays a mark. */\n publishedAt?: string\n modifiedAt?: string\n author?: string\n /** The author's page, for the Article author's \\`url\\`. */\n authorUrl?: string\n /** On an author's archive: who the page is about — ProfilePage + Person. */\n profile?: AuthorProfile\n /** This page's own feed (an archive's), site-root-relative: \\`<link rel=\"alternate\" type=\"application/rss+xml\">\\`. */\n feed?: { path: string; title?: string }\n siteName?: string\n locale?: string\n /** The page is kept out of search: \\`<meta name=\"robots\" content=\"noindex\">\\`. */\n noindex?: boolean\n nofollow?: boolean\n /** Site-wide robots directives from the template head (\\`max-image-preview:large\\`, …), printed on every page. */\n robotsDefault?: string[]\n /** Share-card values the source set by hand, over the derived ones. */\n openGraph?: SocialOverride\n /** Unset values follow Open Graph. */\n twitter?: TwitterOverride\n /** The source SEO plugin's JSON-LD for this page — printed instead of the generated graph. */\n schema?: unknown\n}\n\nconst {\n title = '',\n headline,\n description,\n canonical,\n image,\n imageMeta,\n alternates = [],\n breadcrumbs,\n pageType,\n websiteId,\n publisherId,\n trailingSlash = true,\n headLdIds = [],\n type = 'website',\n publishedAt,\n modifiedAt,\n author,\n authorUrl,\n profile,\n feed,\n siteName,\n locale,\n noindex = false,\n nofollow = false,\n robotsDefault = [],\n openGraph,\n twitter,\n schema,\n} = Astro.props\n\nconst site = Astro.site\nconst url = canonical ? absoluteUrl(canonical, site) : absoluteUrl(pagePath(Astro.url.pathname, trailingSlash), site)\nconst imageUrl = absoluteUrl(image, site)\nconst imageTags = imageUrl ? imageMetaTags(imageMeta) : {}\n// hreflang needs absolute URLs; without \\`site\\` there are none to give.\nconst hreflang = alternates.flatMap((a) => {\n const href = absoluteUrl(pagePath(a.path, trailingSlash), site)\n return href ? [{ lang: a.lang, href }] : []\n})\n// og:locale takes ll_RR; a language without a region has no such form and is left out.\nconst ogLang = ogLocale(locale)\nconst localeAlternates = [...new Set(hreflang.map((a) => ogLocale(a.lang)))].filter((l): l is string => Boolean(l) && l !== ogLang)\nconst desc = seoDescription(description)\nconst robots = [noindex && 'noindex', nofollow && 'nofollow', ...robotsDefault].filter(Boolean).join(', ')\n// Share cards: what the source set by hand, else what the page says.\nconst ogTitle = openGraph?.title || title\nconst ogDesc = seoDescription(openGraph?.description) ?? desc\nconst ogImage = absoluteUrl(openGraph?.image, site) ?? imageUrl\n// The measurements describe \\`image\\`; an override is another file.\nconst ogImageTags = ogImage === imageUrl ? imageTags : {}\nconst twTitle = twitter?.title || ogTitle\nconst twDesc = seoDescription(twitter?.description) ?? ogDesc\nconst twImage = absoluteUrl(twitter?.image, site) ?? ogImage\nconst twCard = twitter?.card === 'summary' || twitter?.card === 'summary_large_image' ? twitter.card : twImage ? 'summary_large_image' : 'summary'\nconst article = type === 'article'\nconst structured = sourceStructuredData(schema, headLdIds) ?? pageStructuredData({\n url,\n site,\n title,\n headline,\n description: desc,\n image: imageUrl,\n imageMeta: imageUrl ? imageMeta : undefined,\n locale,\n article,\n pageType,\n publishedAt,\n modifiedAt,\n author,\n authorUrl,\n profile,\n trailingSlash,\n siteName,\n websiteId,\n publisherId,\n breadcrumbs,\n})\n---\n<title>{title}</title>\n{robots && <meta name=\"robots\" content={robots} />}\n{desc && <meta name=\"description\" content={desc} />}\n{url && <link rel=\"canonical\" href={url} />}\n{hreflang.map((a) => <link rel=\"alternate\" hreflang={a.lang} href={a.href} />)}\n{feed && <link rel=\"alternate\" type=\"application/rss+xml\" title={feed.title} href={feed.path} />}\n<meta property=\"og:type\" content={type} />\n{ogTitle && <meta property=\"og:title\" content={ogTitle} />}\n{ogDesc && <meta property=\"og:description\" content={ogDesc} />}\n{url && <meta property=\"og:url\" content={url} />}\n{ogImage && <meta property=\"og:image\" content={ogImage} />}\n{ogImageTags.width && <meta property=\"og:image:width\" content={ogImageTags.width} />}\n{ogImageTags.height && <meta property=\"og:image:height\" content={ogImageTags.height} />}\n{ogImageTags.type && <meta property=\"og:image:type\" content={ogImageTags.type} />}\n{siteName && <meta property=\"og:site_name\" content={siteName} />}\n{ogLang && <meta property=\"og:locale\" content={ogLang} />}\n{localeAlternates.map((l) => <meta property=\"og:locale:alternate\" content={l} />)}\n{article && publishedAt && <meta property=\"article:published_time\" content={publishedAt} />}\n{article && modifiedAt && <meta property=\"article:modified_time\" content={modifiedAt} />}\n{article && author && <meta property=\"article:author\" content={author} />}\n<meta name=\"twitter:card\" content={twCard} />\n{twTitle && <meta name=\"twitter:title\" content={twTitle} />}\n{twDesc && <meta name=\"twitter:description\" content={twDesc} />}\n{twImage && <meta name=\"twitter:image\" content={twImage} />}\n{structured && <script is:inline type=\"application/ld+json\" set:html={jsonLd(structured)} />}\n`\n","// The site's RSS feed and its /llms.txt (MG-15 §3–4).\n//\n// WordPress serves every site's newest posts as RSS at /feed/, and readers,\n// aggregators and newsletter tools subscribe to it; Yoast writes an llms.txt\n// that tells a language model what the site holds. A migration that drops\n// the first breaks every subscription, and one that never writes the second\n// leaves the site harder for AI search to read than it was.\n//\n// Both are Astro endpoints over the data files the entry pages are built\n// from, and address each entry the way its page is addressed — the route\n// pattern filled with the entry's parameters — so neither can list a page\n// the site does not build, or miss one it does.\n//\n// The feed is built at /feed.xml, not /feed/: a static build writes an\n// endpoint at /feed/ as a file named `feed`, which no host serves at /feed/.\n// The theme's head link to the feed is pointed at the new address, and /feed/\n// gets a 301 to it in the redirect config and the host's redirect file.\n\nimport type { ProjectIR, RouteModel } from '@contentrain/types'\nimport { DEFAULT_COLLECTION } from './types.js'\nimport { patternToPagePath } from './util.js'\n\n/** Where the feed is built. */\nexport const FEED_PATH = '/feed.xml'\n/** WordPress's feed address, redirected (301) to FEED_PATH. */\nexport const FEED_REDIRECT_FROM = '/feed/'\n/** Items in the feed — WordPress's default `posts_per_rss`. */\nexport const FEED_ITEMS = 10\n/** Links per llms.txt section. A long archive is summarised by its newest pages. */\nexport const LLMS_LINKS = 100\n\n/** A collection some route builds one page per entry of, and the pattern it builds them at. */\nexport interface LinkSource {\n collection: string\n pattern: string\n}\n\n/**\n * The collections the site builds entry pages for in its default language,\n * each with the first route that does: posts first, pages next, the rest by\n * name. A route in another language is left out — the feed and llms.txt\n * describe the site as it is in its default one.\n */\nexport function linkSources(routes: RouteModel[], siteLocale: string): LinkSource[] {\n const byCollection = new Map<string, string>()\n for (const route of routes) {\n if (route.kind !== 'single' && route.collection === undefined) continue\n if ((route.locale ?? siteLocale) !== siteLocale) continue\n const pagePath = patternToPagePath(route.pattern)\n if (!pagePath || !pagePath.includes('[')) continue\n const collection = route.collection ?? DEFAULT_COLLECTION\n if (!byCollection.has(collection)) byCollection.set(collection, route.pattern)\n }\n const rank = (c: string) => (c === DEFAULT_COLLECTION ? 0 : c === 'pages' ? 1 : 2)\n return [...byCollection]\n .map(([collection, pattern]) => ({ collection, pattern }))\n .toSorted((a, b) => rank(a.collection) - rank(b.collection) || (a.collection < b.collection ? -1 : a.collection > b.collection ? 1 : 0))\n}\n\n/** An archive route (a category, a tag, an author) whose pages get a feed each, as WordPress's do. */\nexport interface ArchiveFeedSource {\n routeId: string\n pattern: string\n query: string\n}\n\n/**\n * Archive routes in the site's default language whose every page is an\n * address with a feed: a query route with parameters and no page number —\n * `/category/:term*`, `/tag/:term`, `/author/:author`. The paginated\n * continuation (`/category/:term/page/:page`) has no feed of its own.\n */\nexport function archiveFeedSources(routes: RouteModel[], siteLocale: string): ArchiveFeedSource[] {\n const out: ArchiveFeedSource[] = []\n for (const route of routes) {\n if (!route.query || (route.locale ?? siteLocale) !== siteLocale) continue\n const pagePath = patternToPagePath(route.pattern)\n if (!pagePath || !pagePath.includes('[')) continue\n if (route.pattern.split('/').some((seg) => /^:page\\*?$/.test(seg))) continue\n out.push({ routeId: route.id, pattern: route.pattern, query: route.query })\n }\n return out\n}\n\n/** Where an archive route's feed endpoint lives: its page path with `/feed.xml` appended. */\nexport function archiveFeedFile(pattern: string): string | null {\n const pagePath = patternToPagePath(pattern)\n return pagePath ? `src/pages/${pagePath.replace(/\\.astro$/, '')}/feed.xml.ts` : null\n}\n\n/**\n * The feed of every page an archive route builds — the newest posts it lists,\n * at their own addresses — at `<archive>/feed.xml`. Its entries are the\n * route's own first result set, so the feed lists what the archive page does.\n */\nexport function archiveFeedEndpoint(source: ArchiveFeedSource, postPattern: string, input: FeedInput): string {\n const locale = JSON.stringify(input.siteLocale)\n const file = archiveFeedFile(source.pattern)!\n const up = '../'.repeat(file.split('/').length - 2)\n const slash = input.trailingSlash === false ? ', false, false' : ''\n return `// Archive feed for route ${source.routeId} (${source.pattern}) — emitted by @contentrain/emitter-astro.\n// One feed per page the archive builds, at <archive>/feed.xml, as WordPress serves\n// one at <archive>/feed/: the ${FEED_ITEMS} newest posts the archive lists.\nimport type { APIRoute, GetStaticPaths } from 'astro'\nimport data from '${up}data/queries/${source.query}.json'\nimport { absoluteUrl, entryAddress, entryLinks, rssFeed, type EmittedQueryPage } from '${up}lib/fill'\n\nexport const getStaticPaths = (() => (data as EmittedQueryPage[]).map((page) => ({ params: page.params, props: { page } }))) satisfies GetStaticPaths\n\nexport const GET: APIRoute = ({ props, site }) => {\n const page = (props as { page: EmittedQueryPage }).page\n const address = entryAddress(${JSON.stringify(source.pattern)}, page.params) ?? '/'\n const items = entryLinks(page.items, ${JSON.stringify(postPattern)}, site, ${locale}, ${locale}${slash}).slice(0, ${FEED_ITEMS})\n const body = rssFeed({\n title: page.title || ${JSON.stringify(siteTitle(input.ir))},\n link: absoluteUrl(address, site) ?? '',\n self: absoluteUrl(address + 'feed.xml', site) ?? '',\n description: page.description ?? ${JSON.stringify(input.description ?? '')},\n language: ${locale},\n items,\n })\n return new Response(body, { headers: { 'Content-Type': 'application/rss+xml; charset=utf-8' } })\n}\n`\n}\n\nconst sectionName = (collection: string) => collection.charAt(0).toUpperCase() + collection.slice(1).replace(/[-_]+/g, ' ')\n\nfunction siteTitle(ir: ProjectIR): string {\n if (ir.site.title?.trim()) return ir.site.title.trim()\n try {\n return new URL(ir.site.url).host\n } catch {\n return ir.site.url\n }\n}\n\nexport interface FeedInput {\n ir: ProjectIR\n siteLocale: string\n /** The site's tagline, for the channel description and the llms.txt summary. */\n description?: string\n /** false: entries are addressed without a trailing slash. */\n trailingSlash?: boolean\n}\n\n/** `src/pages/feed.xml.ts`: the newest posts as RSS 2.0. */\nexport function feedEndpoint(source: LinkSource, input: FeedInput): string {\n const locale = JSON.stringify(input.siteLocale)\n return `// RSS feed at ${FEED_PATH} — emitted by @contentrain/emitter-astro.\n// Built from the data the post pages are built from: the ${FEED_ITEMS} newest posts,\n// each at the address its own page has.\nimport type { APIRoute } from 'astro'\nimport data from '../data/${source.collection}.json'\nimport { entryLinks, rssFeed, type EmittedPost } from '../lib/fill'\n\nexport const GET: APIRoute = ({ site }) => {\n const items = entryLinks(data as EmittedPost[], ${JSON.stringify(source.pattern)}, site, ${locale}, ${locale}${input.trailingSlash === false ? ', false, false' : ''}).slice(0, ${FEED_ITEMS})\n const body = rssFeed({\n title: ${JSON.stringify(siteTitle(input.ir))},\n link: new URL('/', site).toString(),\n self: new URL(${JSON.stringify(FEED_PATH)}, site).toString(),\n description: ${JSON.stringify(input.description ?? '')},\n language: ${locale},\n items,\n })\n return new Response(body, { headers: { 'Content-Type': 'application/rss+xml; charset=utf-8' } })\n}\n`\n}\n\n/** `src/pages/llms.txt.ts`: a section of links per collection. */\nexport function llmsEndpoint(sources: LinkSource[], input: FeedInput): string {\n const locale = JSON.stringify(input.siteLocale)\n const imports = sources.map((s, i) => `import c${i} from '../data/${s.collection}.json'`)\n const sections = sources.map((s, i) =>\n ` { name: ${JSON.stringify(sectionName(s.collection))}, links: entryLinks(c${i} as EmittedPost[], ${JSON.stringify(s.pattern)}, site, ${locale}, ${locale}, true${input.trailingSlash === false ? ', false' : ''}).slice(0, ${LLMS_LINKS}) },`)\n return `// /llms.txt (llmstxt.org) — emitted by @contentrain/emitter-astro.\n// What a language model reads to find its way around the site: its name, its\n// tagline, and the newest ${LLMS_LINKS} pages of each kind, at their own addresses —\n// none the source kept out of search (noindex).\nimport type { APIRoute } from 'astro'\n${imports.join('\\n')}\nimport { entryLinks, llmsTxt, type EmittedPost } from '../lib/fill'\n\nexport const GET: APIRoute = ({ site }) => {\n const body = llmsTxt({\n title: ${JSON.stringify(siteTitle(input.ir))},\n description: ${JSON.stringify(input.description ?? '')},\n sections: [\n${sections.join('\\n')}\n ],\n })\n return new Response(body, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } })\n}\n`\n}\n\nconst FEED_LINK_RE = /<link\\b(?=[^>]*\\brel\\s*=\\s*[\"']?[^\"'>]*\\balternate\\b)(?=[^>]*\\btype\\s*=\\s*[\"']?application\\/(?:rss|atom)\\+xml)[^>]*>/gi\nconst HREF_RE = /(\\bhref\\s*=\\s*)([\"'])([^\"']*)\\2/i\n\n/** www. and the bare host are one site: a WordPress head often names the other. */\nconst siteHost = (url: URL) => url.host.toLowerCase().replace(/^www\\./, '')\n\n/**\n * The feed a head link names, when it is one of the site's own: `main` for\n * WordPress's main RSS feed (/feed/, /feed/rss2/, ?feed=rss2), `other` for\n * the rest it serves (comments, a category, Atom). A feed on another host —\n * FeedBurner, a newsletter tool — is not the site's to build or to break.\n */\nfunction ownFeed(href: string, type: string, site: URL): 'main' | 'other' | undefined {\n let url: URL\n try {\n url = new URL(href, site)\n } catch {\n return undefined\n }\n if (siteHost(url) !== siteHost(site)) return undefined\n const path = url.pathname.replace(/\\/+$/, '')\n const query = url.searchParams.get('feed')\n const main = path === '/feed' || path === '/feed/rss2' || path === '/feed/rss' || (path === '' && (query === 'rss2' || query === 'rss'))\n return main && /rss\\+xml/i.test(type) ? 'main' : 'other'\n}\n\nexport interface FeedLinkResult {\n html: string\n /** Links now pointing at FEED_PATH. */\n rewritten: number\n /**\n * The site's other feed links, removed: a category's, a tag's or an\n * author's is the template page's own (each archive page links its feed\n * itself), and a comments or Atom feed is not built.\n */\n other: number\n}\n\n/** Point the head's link to the site's main feed at the one the build writes, and remove the template page's others. */\nexport function rewriteFeedLinks(html: string, siteUrl: string): FeedLinkResult {\n let site: URL\n try {\n site = new URL(siteUrl)\n } catch {\n return { html, rewritten: 0, other: 0 }\n }\n let rewritten = 0\n let other = 0\n const out = html.replace(FEED_LINK_RE, (tag) => {\n const href = HREF_RE.exec(tag)\n const type = /\\btype\\s*=\\s*[\"']?([^\"'\\s>]+)/i.exec(tag)?.[1] ?? ''\n const kind = href ? ownFeed(href[3] ?? '', type, site) : undefined\n if (kind === 'other') {\n other++\n return ''\n }\n if (kind !== 'main') return tag\n rewritten++\n return tag.replace(HREF_RE, `$1$2${FEED_PATH}$2`)\n })\n return { html: out, rewritten, other }\n}\n","// Layout families → Astro layouts.\n//\n// Chrome travels as DATA, not as Astro template source: the compiler must\n// never parse browser-tolerated theme markup. And the body chrome is injected\n// as ONE fragment: real themes nest the content container deep inside the\n// chrome, so splitting it into before/after halves produces unbalanced\n// fragments that the parser silently \"repairs\" (measured cost: 36 vs 100).\n// Content splices in at CHROME_BODY_SLOT before the single injection.\n//\n// Root attributes travel with the chrome for the same reason: themes key their\n// container rules off `<body class=\"wp-singular single …\">` and `<html class=\"js\n// wf-…\">`, so a page with perfect content and empty root attributes loses its\n// whole layout (measured: 36.4 vs 100).\n\nimport type { ComponentDef, LayoutFamily } from '@contentrain/types'\nimport { CHROME_BODY_SLOT, CHROME_COMPONENT_CLOSE, CHROME_COMPONENT_OPEN } from '@contentrain/types'\nimport type { ChromeComponentRef } from './chrome.js'\nimport { balanceWarning } from './balance.js'\nimport { bodySeoLeaks, headLdIdsOf, publisherIdOf, stripSeoTags, websiteIdOf } from './seo.js'\nimport { rewriteFeedLinks } from './feed.js'\nimport { pascalCase, stableJson } from './util.js'\n\nexport interface FamilyGenResult {\n files: Record<string, string>\n warnings: string[]\n}\n\n/** A component the family's chrome mounts at a `<!--@@component:ID@@-->` marker. */\n/** A safe identifier for a query id used as a module-local variable name. */\nfunction queryVar(id: string): string {\n return `q_${id.replace(/[^a-zA-Z0-9]/g, '_')}`\n}\n\nexport interface MountRef {\n id: string\n name: string\n variant: string\n /** QueryBinding id when the placement binds this region to a query. */\n query?: string\n}\n\nconst escapeRe = (s: string) => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\nconst MARKER_RE = new RegExp(`${escapeRe(CHROME_COMPONENT_OPEN)}([^@\\\\s]+)${escapeRe(CHROME_COMPONENT_CLOSE)}`, 'g')\n\n/** Component ids the chrome names, in order of first appearance. */\nexport function componentMarkers(html: string): string[] {\n return [...new Set([...html.matchAll(MARKER_RE)].map((m) => m[1] ?? ''))]\n}\n\nexport interface FamilyOptions {\n /** Emit `<Seo>` and take the template page's SEO tags out of the head chrome. */\n seo?: boolean\n /** `og:site_name`, from `ProjectIR.site.title`. */\n siteName?: string\n /**\n * Query ids a placement may bind to — the ones the caller verified exist in\n * the content. A binding outside this set is ignored rather than emitting an\n * import of a data file that was never supplied.\n */\n boundQueries?: ReadonlySet<string>\n /** Run the page's content through the build-time image pass (src/lib/optimize-images.ts). */\n images?: boolean\n /** false: the site's addresses end without a slash — the Seo component names pages that way. */\n trailingSlash?: boolean\n /** The site URL, when the build writes the site feed: the head's link to the main feed is pointed at it. */\n feedSite?: string\n}\n\nexport function familyFiles(\n family: LayoutFamily,\n lang: string,\n components: ChromeComponentRef[] = [],\n definitions: Map<string, ComponentDef> = new Map(),\n /** Component ids found in the CONTENT bodies rendered through this family (a form inside a page's post_content). */\n bodyMarkers: Iterable<string> = [],\n options: FamilyOptions = {},\n): FamilyGenResult {\n const name = pascalCase(family.id)\n // Header/footer regions are emitted as shared components (see chrome.ts) and\n // rendered as siblings of the body fragment; the layout injects the rest.\n const chunks = (family.chrome ?? []).filter((c) => c.position !== 'header' && c.position !== 'footer')\n const warnings: string[] = []\n // The template page's title, description, canonical, og/twitter tags and\n // per-page JSON-LD are the emitter's to render per entry; two of each is\n // worse than none.\n const seoOn = options.seo !== false\n for (const chunk of family.chrome ?? []) {\n if ((chunk.position === 'header' || chunk.position === 'footer') && componentMarkers(chunk.html).length) {\n warnings.push(`family ${family.id}: ${chunk.position} chrome carries a component marker — components mount in body chrome only; the marker renders as a comment`)\n }\n }\n const joined = (position: string) =>\n chunks.filter((c) => c.position === position).map((c) => c.html).join('\\n')\n\n const bodyChunks = chunks.filter((c) => c.position === 'body')\n let body: string\n if (bodyChunks.length) {\n body = bodyChunks.map((c) => c.html).join('\\n')\n if (!body.includes(CHROME_BODY_SLOT)) {\n warnings.push(`family ${family.id}: body chrome has no ${CHROME_BODY_SLOT} marker — content appended at the end`)\n body += CHROME_BODY_SLOT\n }\n } else {\n // Legacy pair: compose into one string with the slot between the halves.\n body = `${joined('before_body')}${CHROME_BODY_SLOT}${joined('after_body')}`\n }\n if (seoOn) {\n const leaked = bodySeoLeaks(body)\n if (leaked.length) {\n warnings.push(`family ${family.id}: body chrome carries head-only ${leaked.join(', ')} — a page then serves the template's copy alongside the emitter's; lift them into the head chunk or drop them at capture`)\n }\n }\n\n const unbalanced = balanceWarning(body)\n if (unbalanced) {\n warnings.push(`family ${family.id}: body chrome is not balanced (${unbalanced}) — the browser will repair it and the page loses its layout`)\n }\n\n // Component mount points. A marker names a ComponentDef; the layout imports\n // that component and renders it there. A marker nobody defined is dropped\n // with a warning rather than left as a mystery comment in every page.\n // Markers live in the chrome (the theme's comments region) or in the content\n // body itself (a contact form inside a page's post_content); the layout splits\n // the COMPOSED html, so both mount the same way. The mount table just has to\n // know every id that can appear.\n const placements = new Map((family.components ?? []).map((p) => [p.component, p]))\n const mounts: MountRef[] = []\n const mount = (id: string, def: ComponentDef) => {\n if (mounts.some((m) => m.id === id)) return\n const placement = placements.get(id)\n const variant = placement?.variant ?? def.variants?.[0]?.key ?? 'default'\n const query = placement?.query && (options.boundQueries?.has(placement.query) ?? false) ? placement.query : undefined\n mounts.push({ id, name: pascalCase(id), variant, ...(query ? { query } : {}) })\n }\n for (const id of componentMarkers(body)) {\n const def = definitions.get(id)\n if (!def) {\n warnings.push(`family ${family.id}: component marker \"${id}\" has no component definition — marker dropped`)\n body = body.split(`${CHROME_COMPONENT_OPEN}${id}${CHROME_COMPONENT_CLOSE}`).join('')\n continue\n }\n mount(id, def)\n }\n for (const id of new Set(bodyMarkers)) {\n const def = definitions.get(id)\n if (!def) {\n warnings.push(`family ${family.id}: content bodies carry component marker \"${id}\" with no component definition — rendered as a comment`)\n continue\n }\n mount(id, def)\n }\n for (const placement of family.components ?? []) {\n if (!mounts.some((m) => m.id === placement.component)) {\n warnings.push(`family ${family.id}: component placement \"${placement.component}\" has no marker in the body chrome or in any content body — not mounted`)\n }\n }\n\n let head = joined('head')\n let robotsDefault: string[] = []\n if (seoOn) {\n const stripped = stripSeoTags(head)\n head = stripped.html\n robotsDefault = stripped.robots\n if (stripped.removed.length) {\n const kept = stripped.kept.length ? `; kept the site-wide ${stripped.kept.join(', ')} from its structured data` : ''\n warnings.push(`family ${family.id}: removed the template page's ${stripped.removed.join(', ')} from head chrome — these describe one page, and the emitter renders title, description, canonical, social tags and Article data per page${kept}`)\n }\n }\n\n if (options.feedSite) {\n const feed = rewriteFeedLinks(head, options.feedSite)\n head = feed.html\n if (feed.other) {\n warnings.push(`family ${family.id}: removed ${feed.other} feed links from the head chrome — the template page's own archive feed, or a comments or Atom feed the site does not build; each archive page links its own feed`)\n }\n }\n\n const files: Record<string, string> = {}\n files[`src/data/chrome/${family.id}.json`] = stableJson({\n head,\n body,\n html_attrs: family.root_attrs?.html ?? {},\n body_attrs: family.root_attrs?.body ?? {},\n })\n\n const cssLinks = (family.css.files ?? [])\n .map((f) => `<link rel=\"stylesheet\" href=\"/styles/legacy/${f.split('/').pop()}\" />`)\n .join('\\n')\n\n const siteNameProp = options.siteName ? ` siteName={${JSON.stringify(options.siteName)}}` : ''\n const trailingSlashProp = options.trailingSlash === false ? ' trailingSlash={false}' : ''\n const publisherId = seoOn ? publisherIdOf(head) : undefined\n const publisherIdProp = publisherId ? ` publisherId={${JSON.stringify(publisherId)}}` : ''\n const websiteId = seoOn ? websiteIdOf(head) : undefined\n const websiteIdProp = websiteId ? ` websiteId={${JSON.stringify(websiteId)}}` : ''\n const robotsProp = robotsDefault.length ? ` robotsDefault={${JSON.stringify(robotsDefault)}}` : ''\n const headLdIds = seoOn ? headLdIdsOf(head) : []\n const headLdIdsProp = headLdIds.length ? ` headLdIds={${JSON.stringify(headLdIds)}}` : ''\n const imported = [...new Set([...components.map((c) => c.name), ...mounts.map((m) => m.name)])]\n const componentImports = [\n ...(seoOn ? [`import Seo from '../components/Seo.astro'`] : []),\n ...imported.map((n) => `import ${n} from '../components/${n}.astro'`),\n ].join('\\n')\n const mountType = [...new Set(mounts.map((m) => `typeof ${m.name}`))].join(' | ')\n // A placement may bind its region to a query instead of to cloned markup.\n // The LAYOUT resolves that data, not the component: a component file is\n // shared by id across families, while a placement belongs to one family, so\n // two families binding the same component to different queries cannot both\n // be served by one file. Binding here is what lets them.\n const queryMounts = mounts.filter((m) => m.query)\n const queryImports = [...new Set(queryMounts.map((m) => m.query!))]\n .map((q) => `import ${queryVar(q)} from '../data/queries/${q}.json'`)\n .join('\\n')\n const queryRender = queryMounts.length\n ? `${[...new Set(queryMounts.map((m) => m.query!))].map(q => `const ${queryVar(q)}Html = renderQuery(${queryVar(q)} as EmittedQueryPage[], ${JSON.stringify(q)})`).join('\\n')}\\n`\n : ''\n const mountTable = mounts.length\n ? `// Components the chrome mounts at <!--@@component:ID@@--> markers; the page's\n// entry address travels to each so a comments thread knows which entry it is.\n// A mount with \\`html\\` renders a query's results rather than cloned markup.\nconst mounts: Record<string, { Mount: ${mountType}; variant: string; html?: string } | undefined> = {\n${mounts.map((m) => ` ${JSON.stringify(m.id)}: { Mount: ${m.name}, variant: ${JSON.stringify(m.variant)}${m.query ? `, html: ${queryVar(m.query)}Html` : ''} },`).join('\\n')}\n}\nconst parts = splitComponents(html).map((part) => ({\n html: part.html,\n Mount: part.component ? mounts[part.component]?.Mount : undefined,\n variant: part.component ? (mounts[part.component]?.variant ?? 'default') : 'default',\n listHtml: part.component ? mounts[part.component]?.html : undefined,\n}))\n`\n : ''\n const bodyRender = mounts.length\n ? ` {parts.map(({ html: part, Mount, variant, listHtml }) => (Mount ? <Mount entry={entry} variant={variant} html={listHtml} lang={htmlAttrs.lang} /> : <Fragment set:html={part} />))}`\n : ` <Fragment set:html={html} />`\n const renderRefs = (position: 'header' | 'footer') =>\n components\n .filter((c) => c.position === position)\n .map((c) => ` <${c.name} marks={marks} />`)\n .join('\\n')\n\n files[`src/layouts/${name}.astro`] = `---\n// Family: ${family.id}${family.name ? ` (${family.name})` : ''} — emitted by @contentrain/emitter-astro\nimport chrome from '../data/chrome/${family.id}.json'\nimport { cssHref, fillAttrs, renderTemplate, composeBody${mounts.length ? ', splitComponents' : ''}${queryMounts.length ? ', renderQuery, type EmittedQueryPage' : ''}${seoOn ? ', type SeoInput' : ''} } from '../lib/fill'\n${options.images ? `import { optimizeHtmlImages } from '../lib/optimize-images'\\n` : ''}${queryImports}${queryImports ? '\\n' : ''}\n${componentImports ? `${componentImports}\\n` : ''}\ninterface Props {\n title?: string\n marks?: Record<string, unknown>\n /** Page content as an HTML string; slot children are used when absent. */\n body?: string\n /** Stylesheets only this page loads (page-builder sites emit CSS per page). */\n css?: string[]\n /** Document language — a multilingual site's routes each pass their own. */\n lang?: string\n /** Content-store address of the page's entry — mounted components (comments) key on it. */\n entry?: { model_id: string; entry_id: string; locale?: string }${seoOn ? `\n /** Per-page SEO — see src/components/Seo.astro. */\n seo?: SeoInput` : ''}\n}\nconst { title = '', marks = {}, body, css = [], lang = ${JSON.stringify(lang)}${mounts.length ? ', entry' : ''}${seoOn ? ', seo' : ''} } = Astro.props\nconst head = renderTemplate(chrome.head, marks)\n// Root attributes carry the theme's layout hooks; values may hold @@marks@@.\n// An explicit lang from the source wins over the project default.\nconst htmlAttrs = { lang, ...fillAttrs(chrome.html_attrs, marks) }\nconst bodyAttrs = fillAttrs(chrome.body_attrs, marks)\n${options.images\n ? `// Content images get srcset, WebP, size and lazy loading at build time; the\n// theme chrome around them is left as the source had it.\nconst content = await optimizeHtmlImages(body ?? (Astro.slots.has('default') ? await Astro.slots.render('default') : ''))`\n : `const content = body ?? (Astro.slots.has('default') ? await Astro.slots.render('default') : '')`}\n// Split at the marker FIRST, then fill marks per side — filling first would\n// eat the @@body@@ inside the marker and silently drop the content.\nconst html = composeBody(chrome.body, marks, content)\n${queryRender}${mountTable}---\n<!doctype html>\n<html {...htmlAttrs}>\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n${cssLinks\n .split('\\n')\n .filter(Boolean)\n .map((l) => ` ${l}`)\n .join('\\n')}\n {css.map((file) => (\n <link rel=\"stylesheet\" href={cssHref(file)} />\n ))}\n <Fragment set:html={head} />\n${seoOn\n ? ` <Seo title={title} locale={lang}${siteNameProp}${websiteIdProp}${publisherIdProp}${trailingSlashProp}${robotsProp}${headLdIdsProp} {...(seo ?? {})} />`\n : ` <title>{title}</title>`}\n </head>\n <body {...bodyAttrs}>\n${renderRefs('header') ? `${renderRefs('header')}\\n` : ''}${bodyRender}\n${renderRefs('footer') ? `${renderRefs('footer')}\\n` : ''} </body>\n</html>\n`\n return { files, warnings }\n}\n","// The runtime-component client, emitted as `src/lib/embed.ts`.\n//\n// Kept as source text (like the fill runtime in scaffold.ts) so the generated\n// site depends on nothing but Astro. The text is type-checked and executed by\n// this package's tests — `embed.test.ts` writes it to disk and imports it, and\n// `typecheck.test.ts` runs tsc over it with Astro's strict settings — so the\n// code that ships is the code that was tested, escaping included.\n\nexport const EMBED_TS = `// Emitted by @contentrain/emitter-astro — the runtime-component client.\n//\n// Comments and forms are the two regions of a migrated site that need a live\n// service. This module is the browser side of the provider's PUBLIC API\n// (Studio: /api/forms/v1 and /api/comments/v1) — the same contract that\n// @contentrain/query/cdn implements, inlined so the generated site depends on\n// nothing but Astro. No credential travels with a request: the endpoints are\n// unauthenticated by design, and a page cannot keep a secret.\n//\n// Rendering helpers are pure (strings in, HTML out, escaped by default) so the\n// mount functions at the bottom are the only code that touches the DOM.\n\nexport interface Runtime {\n base_url: string\n project_id: string\n}\n\nexport interface EntryRef {\n model_id: string\n entry_id: string\n locale?: string\n}\n\nexport class EmbedError extends Error {\n status: number\n /** The API's machine code (\\`data.code\\`), when the body carries one — e.g. \\`payment_required\\`. */\n code?: string\n constructor(status: number, message: string, code?: string) {\n super(message)\n this.name = 'EmbedError'\n this.status = status\n if (code) this.code = code\n }\n}\n\n/**\n * The workspace's subscription is inactive (Studio answers 402\n * \\`payment_required\\` on the public forms and comments endpoints). Not the\n * visitor's problem and not transient: no retry helps until the owner updates\n * billing.\n */\nexport function isPaymentRequired(error: unknown): boolean {\n return error instanceof EmbedError && (error.status === 402 || error.code === 'payment_required')\n}\n\n/**\n * Take a widget off the page without a word to the visitor — the message is\n * written for the workspace owner, who finds it in the console. The rest of\n * the page is untouched.\n */\nexport function hideUnavailable(host: HTMLElement, what: 'form' | 'comments'): void {\n host.innerHTML = ''\n host.hidden = true\n // A site stylesheet that sets display on the element would override hidden.\n host.style.display = 'none'\n console.debug('[contentrain] ' + what + ' hidden: the workspace subscription is inactive (402 payment_required). The workspace owner needs to update billing in Contentrain Studio.')\n}\n\n/**\n * Visitor-facing text. The component passes the page's text from the site's\n * ui-strings dictionary (\\`data-strings\\`); these are the defaults it falls\n * back to, key for key.\n */\nexport const strings = {\n loading: 'Loading…',\n send: 'Send',\n sending: 'Sending…',\n commentsTitle: 'Comments',\n noComments: 'No comments yet.',\n closed: 'Comments are closed.',\n postComment: 'Post comment',\n reply: 'Reply',\n replyingTo: 'Replying to',\n cancel: 'Cancel',\n loadMore: 'Load more comments',\n moderator: 'Moderator',\n pending: 'Thank you — your comment is awaiting moderation.',\n posted: 'Your comment has been posted.',\n name: 'Name',\n email: 'Email',\n emailNote: '(never shown)',\n website: 'Website',\n comment: 'Comment',\n failed: 'Something went wrong. Please try again.',\n honeypot: 'Leave this field empty',\n}\n\nexport const esc = (value: unknown): string =>\n String(value ?? '')\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n\n// ─── Transport ───\n\n/** \\`base\\` without a trailing slash, segments URL-encoded, empty query values dropped. */\nexport function publicUrl(base: string, segments: string[], query?: Record<string, string | number | undefined>): string {\n const path = segments.map((s) => encodeURIComponent(s)).join('/')\n const params = new URLSearchParams()\n for (const [key, value] of Object.entries(query ?? {})) {\n if (value !== undefined && value !== '') params.set(key, String(value))\n }\n const qs = params.toString()\n return base.replace(/\\\\/+$/, '') + '/' + path + (qs ? '?' + qs : '')\n}\n\nasync function failure(res: Response): Promise<EmbedError> {\n const text = await res.text().catch(() => '')\n let message = text || 'Request failed'\n let code: string | undefined\n try {\n const parsed = JSON.parse(text) as { message?: unknown; statusMessage?: unknown; data?: { code?: unknown } }\n const m = parsed.message ?? parsed.statusMessage\n if (typeof m === 'string' && m) message = m\n if (typeof parsed.data?.code === 'string') code = parsed.data.code\n } catch {\n // not JSON — the raw text is the message\n }\n return new EmbedError(res.status, message, code)\n}\n\nexport async function getJson<T>(url: string): Promise<T> {\n const res = await fetch(url)\n if (!res.ok) throw await failure(res)\n return (await res.json()) as T\n}\n\nexport async function postJson<T>(url: string, body: unknown): Promise<T> {\n const res = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n })\n if (!res.ok) throw await failure(res)\n return (await res.json()) as T\n}\n\nexport const formsRoot = (rt: Runtime): string => rt.base_url.replace(/\\\\/+$/, '') + '/api/forms/v1'\nexport const commentsRoot = (rt: Runtime): string => rt.base_url.replace(/\\\\/+$/, '') + '/api/comments/v1'\n\n/** The hidden input Cloudflare Turnstile adds to the enclosing form. */\nexport const CAPTCHA_FIELD = 'cf-turnstile-response'\n\n// ─── Forms ───\n\nexport interface FieldDef {\n type: string\n required?: boolean\n label?: string\n options?: string[]\n min?: number\n max?: number\n pattern?: string\n default?: unknown\n}\n\nexport interface FormConfig {\n modelId: string\n locale: string\n /** Exposed fields only, keyed by field id. */\n fields: Record<string, FieldDef>\n captcha: 'turnstile' | null\n captchaSiteKey: string | null\n successMessage?: string\n honeypotField: string | null\n}\n\nexport interface FieldError {\n field: string\n message: string\n}\n\nexport interface FormSubmitResult {\n success: boolean\n message?: string\n errors?: FieldError[]\n}\n\n/** The documented request body: values under \\`data\\`, control fields beside it. */\nexport interface FormPayload {\n data: Record<string, unknown>\n captchaToken?: string\n _hp?: string\n}\n\nexport function fetchFormConfig(rt: Runtime, model: string): Promise<FormConfig> {\n return getJson<FormConfig>(publicUrl(formsRoot(rt), [rt.project_id, model, 'config']))\n}\n\nexport function submitForm(rt: Runtime, model: string, payload: FormPayload): Promise<FormSubmitResult> {\n return postJson<FormSubmitResult>(publicUrl(formsRoot(rt), [rt.project_id, model, 'submit']), payload)\n}\n\nfunction coerce(def: FieldDef, value: string): unknown {\n switch (def.type) {\n case 'number':\n case 'integer':\n case 'decimal':\n case 'percent':\n case 'rating':\n return value === '' ? '' : Number(value)\n case 'boolean':\n return value === 'on' || value === 'true'\n default:\n return value\n }\n}\n\n/**\n * Form entries (\\`new FormData(form)\\`) → request body. Only exposed fields\n * reach \\`data\\`; the captcha token and the honeypot value travel beside it, so\n * a control field can never collide with a model field. Files are skipped —\n * uploads are not part of the public contract.\n */\nexport function formPayload(\n entries: Iterable<[string, unknown]>,\n config: { fields: Record<string, FieldDef>; honeypotField: string | null },\n): FormPayload {\n const payload: FormPayload = { data: {} }\n for (const [name, raw] of entries) {\n if (typeof raw !== 'string') continue\n if (name === CAPTCHA_FIELD) {\n if (raw) payload.captchaToken = raw\n continue\n }\n if (config.honeypotField && name === config.honeypotField) {\n payload._hp = raw\n continue\n }\n const def = config.fields[name]\n if (!def) continue\n payload.data[name] = coerce(def, raw)\n }\n return payload\n}\n\nexport function labelFor(id: string, def: FieldDef): string {\n if (def.label) return def.label\n const words = id.replace(/[_-]+/g, ' ').trim()\n return words.charAt(0).toUpperCase() + words.slice(1)\n}\n\n/** One input for one exposed field, chosen by field type. */\nexport function fieldControl(id: string, def: FieldDef, prefix = 'cr-field'): string {\n const common =\n ' id=\"' + esc(prefix + '-' + id) + '\" name=\"' + esc(id) + '\"' +\n (def.required ? ' required' : '') +\n (def.pattern ? ' pattern=\"' + esc(def.pattern) + '\"' : '')\n const range =\n (def.min !== undefined ? ' min=\"' + esc(def.min) + '\"' : '') +\n (def.max !== undefined ? ' max=\"' + esc(def.max) + '\"' : '')\n const length = def.max !== undefined ? ' maxlength=\"' + esc(def.max) + '\"' : ''\n switch (def.type) {\n case 'text':\n case 'markdown':\n case 'richtext':\n case 'code':\n return '<textarea' + common + length + ' rows=\"5\"></textarea>'\n case 'select': {\n const blank = def.required ? '' : '<option value=\"\"></option>'\n const options = (def.options ?? []).map((o) => '<option value=\"' + esc(o) + '\">' + esc(o) + '</option>').join('')\n return '<select' + common + '>' + blank + options + '</select>'\n }\n case 'boolean':\n return '<input type=\"checkbox\"' + common + ' />'\n case 'number':\n case 'integer':\n case 'rating':\n return '<input type=\"number\"' + common + range + (def.type === 'integer' ? ' step=\"1\"' : '') + ' />'\n case 'decimal':\n case 'percent':\n return '<input type=\"number\" step=\"any\"' + common + range + ' />'\n case 'email':\n return '<input type=\"email\"' + common + length + ' />'\n case 'url':\n return '<input type=\"url\"' + common + length + ' />'\n case 'phone':\n return '<input type=\"tel\"' + common + length + ' />'\n case 'date':\n return '<input type=\"date\"' + common + ' />'\n case 'datetime':\n return '<input type=\"datetime-local\"' + common + ' />'\n case 'color':\n return '<input type=\"color\"' + common + ' />'\n default:\n return '<input type=\"text\"' + common + length + ' />'\n }\n}\n\nexport function honeypotHtml(field: string | null): string {\n if (!field) return ''\n return (\n '<p class=\"cr-hp\" aria-hidden=\"true\" style=\"position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden\">' +\n '<label>' + esc(strings.honeypot) + ' <input type=\"text\" name=\"' + esc(field) + '\" tabindex=\"-1\" autocomplete=\"off\" /></label></p>'\n )\n}\n\nexport function captchaHtml(captcha: 'turnstile' | null, siteKey: string | null): string {\n if (captcha !== 'turnstile' || !siteKey) return ''\n return '<div class=\"cf-turnstile\" data-sitekey=\"' + esc(siteKey) + '\"></div>'\n}\n\nexport function errorsHtml(errors: FieldError[]): string {\n return (\n '<ul class=\"cr-errors\" role=\"alert\">' +\n errors.map((e) => '<li data-field=\"' + esc(e.field) + '\">' + esc(e.field) + ': ' + esc(e.message) + '</li>').join('') +\n '</ul>'\n )\n}\n\n/** The whole form for a public form config: one control per exposed field, honeypot, captcha, submit. */\nexport function formHtml(config: FormConfig, prefix = 'cr-field'): string {\n const fields = Object.entries(config.fields)\n .map(([id, def]) => {\n const control = fieldControl(id, def, prefix)\n const label = '<label for=\"' + esc(prefix + '-' + id) + '\">' + esc(labelFor(id, def)) + (def.required ? ' <span aria-hidden=\"true\">*</span>' : '') + '</label>'\n return '<p class=\"cr-field cr-field--' + esc(def.type) + '\">' + (def.type === 'boolean' ? control + ' ' + label : label + control) + '</p>'\n })\n .join('')\n return (\n '<form class=\"cr-form\" method=\"post\" data-model=\"' + esc(config.modelId) + '\">' +\n fields +\n honeypotHtml(config.honeypotField) +\n captchaHtml(config.captcha, config.captchaSiteKey) +\n '<p class=\"cr-actions\"><button type=\"submit\">' + esc(strings.send) + '</button></p>' +\n '<div class=\"cr-status\" aria-live=\"polite\"></div>' +\n '</form>'\n )\n}\n\n// ─── Comments ───\n\nexport interface CommentAuthor {\n name: string\n url: string | null\n isModerator: boolean\n}\n\nexport interface PublicComment {\n id: string\n parentId: string | null\n depth: number\n author: CommentAuthor\n /** Plain text. */\n body: string\n type: 'comment' | 'pingback' | 'trackback'\n createdAt: string\n replies: PublicComment[]\n}\n\nexport interface ThreadConfig {\n closed: boolean\n requireApproval: boolean\n requireEmail: boolean\n maxDepth: number\n maxBodyLength: number\n captcha: 'turnstile' | null\n captchaSiteKey: string | null\n honeypotField: string | null\n}\n\nexport interface CommentThread {\n entry: { modelId: string; entryId: string; locale: string }\n config: ThreadConfig\n comments: PublicComment[]\n total: number\n page: number\n limit: number\n}\n\nexport interface CommentSubmitBody {\n author: { name: string; email?: string; url?: string }\n body: string\n /** null for a root comment — sent explicitly, as the provider's own fixture does. */\n parentId: string | null\n captchaToken?: string\n _hp?: string\n}\n\nexport interface CommentSubmitResult {\n success: boolean\n status?: 'pending' | 'approved'\n comment?: PublicComment\n errors?: FieldError[]\n}\n\nexport interface ThreadQuery {\n page?: number\n limit?: number\n sort?: 'oldest' | 'newest'\n}\n\nexport function fetchThread(rt: Runtime, entry: EntryRef, query?: ThreadQuery): Promise<CommentThread> {\n const url = publicUrl(commentsRoot(rt), [rt.project_id, entry.model_id, entry.entry_id], {\n locale: entry.locale,\n page: query?.page,\n limit: query?.limit,\n sort: query?.sort,\n })\n return getJson<CommentThread>(url)\n}\n\nexport function submitComment(rt: Runtime, entry: EntryRef, body: CommentSubmitBody): Promise<CommentSubmitResult> {\n const url = publicUrl(commentsRoot(rt), [rt.project_id, entry.model_id, entry.entry_id], { locale: entry.locale })\n return postJson<CommentSubmitResult>(url, body)\n}\n\n/** Comment-form entries → the documented request body (inputs: author_name, author_email, author_url, body, parent_id). */\nexport function commentPayload(entries: Iterable<[string, unknown]>, honeypotField: string | null): CommentSubmitBody {\n const values: Record<string, string> = {}\n const payload: CommentSubmitBody = { author: { name: '' }, body: '', parentId: null }\n for (const [name, raw] of entries) {\n if (typeof raw !== 'string') continue\n if (name === CAPTCHA_FIELD) {\n if (raw) payload.captchaToken = raw\n continue\n }\n if (honeypotField && name === honeypotField) {\n payload._hp = raw\n continue\n }\n values[name] = raw\n }\n payload.author.name = (values.author_name ?? '').trim()\n if (values.author_email?.trim()) payload.author.email = values.author_email.trim()\n if (values.author_url?.trim()) payload.author.url = values.author_url.trim()\n payload.body = (values.body ?? '').trim()\n payload.parentId = values.parent_id?.trim() || null\n return payload\n}\n\n/** Plain text → paragraphs; blank lines separate paragraphs, single newlines break lines. */\nexport function bodyHtml(text: string): string {\n return text\n .split(/\\\\n{2,}/)\n .map((p) => p.trim())\n .filter(Boolean)\n .map((p) => '<p>' + esc(p).replace(/\\\\n/g, '<br />') + '</p>')\n .join('')\n}\n\nexport function hasMore(thread: CommentThread): boolean {\n return thread.page * thread.limit < thread.total\n}\n\n/** A word's first character (a whole code point, so an emoji or an accented letter stays intact). */\nfunction firstChar(word: string | undefined): string {\n return word ? Array.from(word)[0] ?? '' : ''\n}\n\n/**\n * Up to two initials of a name, for the comment's monogram. The provider sends\n * no avatar (no email hash leaves it), so a letter mark stands in for one.\n */\nexport function initials(name: string): string {\n const words = name.trim().split(/\\\\s+/).filter(Boolean)\n return (firstChar(words[0]) + (words.length > 1 ? firstChar(words[words.length - 1]) : '')).toLocaleUpperCase()\n}\n\n/**\n * A commenter's website, as an address that is safe to link: the browser's own parse of it (which drops tabs and newlines\n * and lower-cases the scheme, so \\`java\\\\tscript:\\` and \\` JaVaScRiPt:\\` are what they are), and only http: or https:.\n * Anything else (javascript:, data:, vbscript:, a bare word, an address that does not parse) is no link: HTML escaping\n * does not stop a scheme, and a row may not have come through the public form that checks it.\n */\nexport function safeHref(raw: string): string | null {\n try {\n const url = new URL(raw.trim())\n return url.protocol === 'http:' || url.protocol === 'https:' ? url.href : null\n } catch {\n return null\n }\n}\n\n/** One comment with its replies. A reply button appears only while the thread is open and depth allows it. */\nexport function commentHtml(c: PublicComment, config: { closed: boolean; maxDepth: number }): string {\n const href = c.author.url ? safeHref(c.author.url) : null\n const author = href\n ? '<a href=\"' + esc(href) + '\" rel=\"nofollow ugc noopener\" target=\"_blank\">' + esc(c.author.name) + '</a>'\n : esc(c.author.name)\n const badge = c.author.isModerator ? ' <span class=\"cr-moderator\">' + esc(strings.moderator) + '</span>' : ''\n const date = '<time datetime=\"' + esc(c.createdAt) + '\">' + esc(c.createdAt.slice(0, 10)) + '</time>'\n const canReply = !config.closed && c.depth < config.maxDepth\n const reply = canReply\n ? '<button type=\"button\" class=\"cr-reply\" data-parent=\"' + esc(c.id) + '\" data-author=\"' + esc(c.author.name) + '\">' + esc(strings.reply) + '</button>'\n : ''\n const replies = c.replies.length\n ? '<ol class=\"cr-replies\">' + c.replies.map((r) => commentHtml(r, config)).join('') + '</ol>'\n : ''\n return (\n '<li class=\"cr-comment cr-comment--' + esc(c.type) + '\" id=\"cr-comment-' + esc(c.id) + '\" data-depth=\"' + esc(c.depth) + '\">' +\n '<div class=\"cr-comment-meta\"><span class=\"cr-avatar\" aria-hidden=\"true\">' + esc(initials(c.author.name)) + '</span>' +\n '<span class=\"cr-comment-author\">' + author + '</span>' + badge + ' ' + date + '</div>' +\n '<div class=\"cr-comment-body\">' + bodyHtml(c.body) + '</div>' +\n (reply ? '<div class=\"cr-comment-actions\">' + reply + '</div>' : '') +\n replies +\n '</li>'\n )\n}\n\nexport function threadHtml(thread: CommentThread): string {\n if (!thread.comments.length) return '<p class=\"cr-empty\">' + esc(strings.noComments) + '</p>'\n return '<ol class=\"cr-comment-list\">' + thread.comments.map((c) => commentHtml(c, thread.config)).join('') + '</ol>'\n}\n\n/** The comment form; a reply carries its parent in a hidden input. */\nexport function commentFormHtml(config: ThreadConfig, prefix = 'cr-c'): string {\n const emailLabel = esc(strings.email) + (config.requireEmail ? ' <span aria-hidden=\"true\">*</span>' : '') + ' <small>' + esc(strings.emailNote) + '</small>'\n return (\n '<form class=\"cr-comment-form\" method=\"post\">' +\n '<input type=\"hidden\" name=\"parent_id\" value=\"\" />' +\n '<p class=\"cr-replying\" hidden><span class=\"cr-replying-to\"></span> <button type=\"button\" class=\"cr-cancel-reply\">' + esc(strings.cancel) + '</button></p>' +\n '<p class=\"cr-field\"><label for=\"' + prefix + '-body\">' + esc(strings.comment) + ' <span aria-hidden=\"true\">*</span></label>' +\n '<textarea id=\"' + prefix + '-body\" name=\"body\" required maxlength=\"' + esc(config.maxBodyLength) + '\" rows=\"5\"></textarea></p>' +\n '<p class=\"cr-field\"><label for=\"' + prefix + '-name\">' + esc(strings.name) + ' <span aria-hidden=\"true\">*</span></label>' +\n '<input type=\"text\" id=\"' + prefix + '-name\" name=\"author_name\" required maxlength=\"120\" autocomplete=\"name\" /></p>' +\n '<p class=\"cr-field\"><label for=\"' + prefix + '-email\">' + emailLabel + '</label>' +\n '<input type=\"email\" id=\"' + prefix + '-email\" name=\"author_email\"' + (config.requireEmail ? ' required' : '') + ' maxlength=\"254\" autocomplete=\"email\" /></p>' +\n '<p class=\"cr-field\"><label for=\"' + prefix + '-url\">' + esc(strings.website) + '</label>' +\n '<input type=\"url\" id=\"' + prefix + '-url\" name=\"author_url\" maxlength=\"2048\" autocomplete=\"url\" /></p>' +\n honeypotHtml(config.honeypotField) +\n captchaHtml(config.captcha, config.captchaSiteKey) +\n '<p class=\"cr-actions\"><button type=\"submit\">' + esc(strings.postComment) + '</button></p>' +\n '<div class=\"cr-status\" aria-live=\"polite\"></div>' +\n '</form>'\n )\n}\n\n// ─── Turnstile (explicit render, so widgets added after load still render) ───\n\ninterface TurnstileApi {\n render: (el: Element, options: { sitekey: string }) => string\n reset: (id?: string) => void\n}\n\nconst TURNSTILE_SRC = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=__crTurnstileReady'\nconst widgets = new WeakMap<Element, string>()\nconst waiting: Element[] = []\n\nfunction turnstileApi(): TurnstileApi | undefined {\n return (globalThis as { turnstile?: TurnstileApi }).turnstile\n}\n\nfunction renderWidget(api: TurnstileApi, el: Element): void {\n if (widgets.has(el)) return\n widgets.set(el, api.render(el, { sitekey: el.getAttribute('data-sitekey') ?? '' }))\n}\n\n/** Render every \\`.cf-turnstile\\` under \\`root\\`, loading the Turnstile script once on first use. */\nexport function renderCaptcha(root: ParentNode): void {\n const els = Array.from(root.querySelectorAll('.cf-turnstile'))\n if (!els.length) return\n const api = turnstileApi()\n if (api) {\n for (const el of els) renderWidget(api, el)\n return\n }\n waiting.push(...els)\n if (document.querySelector('script[data-cr-turnstile]')) return\n ;(globalThis as Record<string, unknown>).__crTurnstileReady = () => {\n const ready = turnstileApi()\n if (!ready) return\n for (const el of waiting.splice(0)) renderWidget(ready, el)\n }\n const script = document.createElement('script')\n script.src = TURNSTILE_SRC\n script.async = true\n script.defer = true\n script.setAttribute('data-cr-turnstile', '1')\n document.head.appendChild(script)\n}\n\nexport function resetCaptcha(root: ParentNode): void {\n const api = turnstileApi()\n if (!api) return\n for (const el of Array.from(root.querySelectorAll('.cf-turnstile'))) api.reset(widgets.get(el))\n}\n\n// ─── Mounting (the only DOM code) ───\n\n/** ui-strings dictionary key → the \\`strings\\` entry it sets. */\nexport const STRING_KEYS: ReadonlyArray<readonly [string, keyof typeof strings]> = [\n ['common.loading', 'loading'],\n ['common.failed', 'failed'],\n ['form.send', 'send'],\n ['form.sending', 'sending'],\n ['form.honeypot', 'honeypot'],\n ['comments.title', 'commentsTitle'],\n ['comments.empty', 'noComments'],\n ['comments.closed', 'closed'],\n ['comments.post', 'postComment'],\n ['comments.reply', 'reply'],\n ['comments.replying_to', 'replyingTo'],\n ['comments.cancel', 'cancel'],\n ['comments.load_more', 'loadMore'],\n ['comments.moderator', 'moderator'],\n ['comments.pending', 'pending'],\n ['comments.posted', 'posted'],\n ['comments.field.name', 'name'],\n ['comments.field.email', 'email'],\n ['comments.field.email_note', 'emailNote'],\n ['comments.field.website', 'website'],\n ['comments.field.comment', 'comment'],\n]\n\n/**\n * Take the page's interface text from the host element. A value that is not a\n * non-empty string leaves the default; text that cannot be parsed changes\n * nothing, because a mount that fails over a label is worse than an English one.\n */\nexport function applyStrings(host: HTMLElement): void {\n const raw = host.dataset.strings\n if (!raw) return\n let text: unknown\n try {\n text = JSON.parse(raw)\n } catch {\n return\n }\n if (!text || typeof text !== 'object') return\n for (const [key, name] of STRING_KEYS) {\n const value = (text as Record<string, unknown>)[key]\n if (typeof value === 'string' && value !== '') strings[name] = value\n }\n}\n\nexport function runtimeOf(host: HTMLElement): Runtime {\n return { base_url: host.dataset.baseUrl ?? '', project_id: host.dataset.project ?? '' }\n}\n\nfunction entryOf(host: HTMLElement): EntryRef {\n const locale = host.dataset.locale\n return { model_id: host.dataset.model ?? '', entry_id: host.dataset.entry ?? '', ...(locale ? { locale } : {}) }\n}\n\nfunction setStatus(form: HTMLFormElement, html: string): void {\n const status = form.querySelector('.cr-status')\n if (status) status.innerHTML = html\n}\n\nfunction busy(form: HTMLFormElement, on: boolean): void {\n const button = form.querySelector<HTMLButtonElement>('button[type=\"submit\"]')\n if (!button) return\n button.disabled = on\n if (on) {\n button.dataset.label = button.textContent ?? ''\n button.textContent = strings.sending\n } else {\n button.textContent = button.dataset.label ?? button.textContent\n }\n}\n\nfunction entriesOf(form: HTMLFormElement): Array<[string, unknown]> {\n const out: Array<[string, unknown]> = []\n new FormData(form).forEach((value, key) => out.push([key, value]))\n return out\n}\n\n/** \\`<cr-form data-base-url data-project data-model>\\` → fetch the config, render, submit. */\nexport async function mountForm(host: HTMLElement): Promise<void> {\n applyStrings(host)\n const rt = runtimeOf(host)\n const model = host.dataset.model ?? ''\n // The page's own copy of the form (its fields as HTML) stays in view while Studio's form loads; without one, a\n // loading line holds the place.\n const drawn = host.querySelector('form')\n if (!drawn) host.innerHTML = '<p class=\"cr-loading\">' + esc(strings.loading) + '</p>'\n let config: FormConfig\n try {\n config = await fetchFormConfig(rt, model)\n } catch (error) {\n if (isPaymentRequired(error)) return hideUnavailable(host, 'form')\n const message = '<p class=\"cr-error\" role=\"alert\">' + esc(error instanceof Error ? error.message : strings.failed) + '</p>'\n if (drawn) drawn.insertAdjacentHTML('beforeend', message)\n else host.innerHTML = message\n return\n }\n host.innerHTML = formHtml(config)\n renderCaptcha(host)\n const form = host.querySelector('form')\n if (!form) return\n form.addEventListener('submit', async (event) => {\n event.preventDefault()\n busy(form, true)\n setStatus(form, '')\n try {\n const result = await submitForm(rt, model, formPayload(entriesOf(form), config))\n if (result.success) {\n host.innerHTML = '<p class=\"cr-success\" role=\"status\">' + esc(result.message ?? config.successMessage ?? '') + '</p>'\n return\n }\n setStatus(form, errorsHtml(result.errors ?? []))\n resetCaptcha(form)\n } catch (error) {\n if (isPaymentRequired(error)) return hideUnavailable(host, 'form')\n setStatus(form, '<p class=\"cr-error\" role=\"alert\">' + esc(error instanceof Error ? error.message : strings.failed) + '</p>')\n resetCaptcha(form)\n } finally {\n busy(form, false)\n }\n })\n}\n\n/** \\`<cr-comments data-base-url data-project data-model data-entry data-locale>\\` → thread + form. */\nexport async function mountComments(host: HTMLElement): Promise<void> {\n applyStrings(host)\n const rt = runtimeOf(host)\n const entry = entryOf(host)\n host.innerHTML = '<p class=\"cr-loading\">' + esc(strings.loading) + '</p>'\n let thread: CommentThread\n try {\n thread = await fetchThread(rt, entry)\n } catch (error) {\n if (isPaymentRequired(error)) return hideUnavailable(host, 'comments')\n host.innerHTML = '<p class=\"cr-error\" role=\"alert\">' + esc(error instanceof Error ? error.message : strings.failed) + '</p>'\n return\n }\n\n host.innerHTML =\n '<section class=\"cr-comments-section\">' +\n '<h2 class=\"cr-comments-title\">' + esc(strings.commentsTitle) + ' <span class=\"cr-count\">(' + esc(thread.total) + ')</span></h2>' +\n '<div class=\"cr-thread\">' + threadHtml(thread) + '</div>' +\n (hasMore(thread) ? '<p class=\"cr-more\"><button type=\"button\" class=\"cr-load-more\">' + esc(strings.loadMore) + '</button></p>' : '') +\n (thread.config.closed ? '<p class=\"cr-closed\">' + esc(strings.closed) + '</p>' : commentFormHtml(thread.config)) +\n '</section>'\n renderCaptcha(host)\n\n const threadEl = host.querySelector('.cr-thread')\n const form = host.querySelector<HTMLFormElement>('form.cr-comment-form')\n let page = thread.page\n\n host.addEventListener('click', async (event) => {\n const target = event.target as HTMLElement | null\n if (!target) return\n const replyButton = target.closest<HTMLElement>('.cr-reply')\n if (replyButton && form) {\n const parent = form.querySelector<HTMLInputElement>('input[name=\"parent_id\"]')\n if (parent) parent.value = replyButton.dataset.parent ?? ''\n const replying = form.querySelector<HTMLElement>('.cr-replying')\n const to = form.querySelector('.cr-replying-to')\n if (to) to.textContent = strings.replyingTo + ' ' + (replyButton.dataset.author ?? '')\n if (replying) replying.hidden = false\n replyButton.closest('.cr-comment')?.appendChild(form)\n form.querySelector<HTMLTextAreaElement>('textarea[name=\"body\"]')?.focus()\n return\n }\n if (target.closest('.cr-cancel-reply') && form) {\n const parent = form.querySelector<HTMLInputElement>('input[name=\"parent_id\"]')\n if (parent) parent.value = ''\n const replying = form.querySelector<HTMLElement>('.cr-replying')\n if (replying) replying.hidden = true\n host.querySelector('.cr-comments-section')?.appendChild(form)\n return\n }\n const more = target.closest<HTMLButtonElement>('.cr-load-more')\n if (more && threadEl) {\n more.disabled = true\n try {\n const next = await fetchThread(rt, entry, { page: page + 1 })\n page = next.page\n const list = threadEl.querySelector('.cr-comment-list')\n if (list) list.insertAdjacentHTML('beforeend', next.comments.map((c) => commentHtml(c, next.config)).join(''))\n if (!hasMore(next)) more.closest('.cr-more')?.remove()\n } catch (error) {\n if (isPaymentRequired(error)) return hideUnavailable(host, 'comments')\n // Anything else: the button comes back for another try.\n } finally {\n more.disabled = false\n }\n }\n })\n\n form?.addEventListener('submit', async (event) => {\n event.preventDefault()\n busy(form, true)\n setStatus(form, '')\n try {\n const result = await submitComment(rt, entry, commentPayload(entriesOf(form), thread.config.honeypotField))\n if (!result.success) {\n setStatus(form, errorsHtml(result.errors ?? []))\n resetCaptcha(form)\n return\n }\n const parentId = result.comment?.parentId ?? null\n if (result.status === 'approved' && result.comment && threadEl) {\n const html = commentHtml(result.comment, thread.config)\n const parent = parentId ? threadEl.querySelector('#cr-comment-' + parentId) : null\n if (parent) {\n let replies = parent.querySelector(':scope > .cr-replies')\n if (!replies) {\n parent.insertAdjacentHTML('beforeend', '<ol class=\"cr-replies\"></ol>')\n replies = parent.querySelector(':scope > .cr-replies')\n }\n replies?.insertAdjacentHTML('beforeend', html)\n } else {\n const list = threadEl.querySelector('.cr-comment-list')\n if (list) list.insertAdjacentHTML('beforeend', html)\n else threadEl.innerHTML = '<ol class=\"cr-comment-list\">' + html + '</ol>'\n }\n }\n form.reset()\n host.querySelector('.cr-comments-section')?.appendChild(form)\n const replying = form.querySelector<HTMLElement>('.cr-replying')\n if (replying) replying.hidden = true\n resetCaptcha(form)\n setStatus(form, '<p class=\"cr-success\" role=\"status\">' + esc(result.status === 'approved' ? strings.posted : strings.pending) + '</p>')\n } catch (error) {\n if (isPaymentRequired(error)) return hideUnavailable(host, 'comments')\n setStatus(form, '<p class=\"cr-error\" role=\"alert\">' + esc(error instanceof Error ? error.message : strings.failed) + '</p>')\n resetCaptcha(form)\n } finally {\n busy(form, false)\n }\n })\n}\n`\n","// The generated site's interface text — what its comments and forms say to a\n// visitor — lives in the site's content store, not in its code.\n//\n// A migrated Turkish site whose comment form says \"Send\" is a defect the\n// customer can see and cannot fix without editing TypeScript. As a dictionary\n// in `.contentrain/`, the text is content: the migration writes it in the\n// source site's language, the customer edits it in Studio or through MCP, and a\n// rebuild picks it up with no re-emit.\n//\n// The emitter owns the key list and the English defaults, so the site still\n// works — in English — when the dictionary is absent. It never writes the\n// dictionary itself: which words a site uses is a content decision.\n\nimport type { ModelDefinition } from '@contentrain/types'\nimport { DICTIONARY_TITLE_FIELD } from '@contentrain/types'\n\n/** Where the dictionary lives in an emitted project, relative to its root. */\nexport const UI_STRINGS_DIR = '.contentrain/content/site/ui-strings'\n\n/** Every key the runtime components read, with the text they show without a dictionary. */\nexport const UI_STRING_DEFAULTS = {\n 'comments.cancel': 'Cancel',\n 'comments.closed': 'Comments are closed.',\n 'comments.empty': 'No comments yet.',\n 'comments.field.comment': 'Comment',\n 'comments.field.email': 'Email',\n 'comments.field.email_note': '(never shown)',\n 'comments.field.name': 'Name',\n 'comments.field.website': 'Website',\n 'comments.load_more': 'Load more comments',\n 'comments.moderator': 'Moderator',\n 'comments.noscript': 'Comments need JavaScript.',\n 'comments.pending': 'Thank you — your comment is awaiting moderation.',\n 'comments.post': 'Post comment',\n 'comments.posted': 'Your comment has been posted.',\n 'comments.replying_to': 'Replying to',\n 'comments.reply': 'Reply',\n 'comments.title': 'Comments',\n 'common.failed': 'Something went wrong. Please try again.',\n 'common.loading': 'Loading…',\n 'form.honeypot': 'Leave this field empty',\n 'form.noscript': 'This form needs JavaScript.',\n 'form.send': 'Send',\n 'form.sending': 'Sending…',\n} as const\n\nexport type UiStringKey = keyof typeof UI_STRING_DEFAULTS\n\n/**\n * The dictionary model a producer creates in the site's store. Exported so the\n * migration and the emitter cannot disagree about its id, kind or domain.\n */\nexport const UI_STRINGS_MODEL: ModelDefinition = {\n id: 'ui-strings',\n name: 'Site interface text',\n kind: 'dictionary',\n domain: 'site',\n i18n: true,\n title_field: DICTIONARY_TITLE_FIELD,\n description: 'What the site’s comments and forms say to visitors, per language. Missing keys fall back to English.',\n}\n\n/**\n * A project-relative directory safe to put in a glob, or undefined. The value\n * lands in source the build evaluates, so anything that could leave the\n * project or change the pattern is refused rather than escaped.\n */\nexport function uiStringsDir(dir: string | undefined): string | undefined {\n const value = (dir ?? UI_STRINGS_DIR).replace(/^\\/+|\\/+$/g, '')\n if (!value || value.split('/').some((s) => s === '..' || s === '.' || s === '') || !/^[\\w.@-]+(?:\\/[\\w.@-]+)*$/.test(value)) return undefined\n return value\n}\n\n/**\n * `src/lib/ui-strings.ts` for an emitted project: reads every locale file of\n * the dictionary at build time and resolves a page's text over the defaults.\n *\n * `import.meta.glob` returns an empty map when nothing matches, so a project\n * without the dictionary still builds. It does not stay quiet about it: a page\n * in a language other than English with no dictionary, or a dictionary missing\n * keys, is reported once in the build log.\n */\nexport function uiStringsSource(dir: string): string {\n const pattern = `/${dir}/*.json`\n return `// Emitted by @contentrain/emitter-astro — the site's interface text.\n//\n// Comments and forms read their visitor-facing text from the ui-strings\n// dictionary in the content store (${dir}/{locale}.json), so it is\n// edited as content — in Studio or through MCP — and a rebuild applies it.\n// Keys the dictionary does not have show the English default below.\n\nconst DEFAULTS: Record<string, string> = ${JSON.stringify(UI_STRING_DEFAULTS, null, 2)}\n\nconst files = import.meta.glob<Record<string, unknown>>(${JSON.stringify(pattern)}, { eager: true, import: 'default' })\nconst dictionaries = new Map(\n Object.entries(files).map(([path, dictionary]) => [path.slice(path.lastIndexOf('/') + 1, -5).toLowerCase(), dictionary]),\n)\nconst reported = new Set<string>()\n\n/** The interface text for a page language: the dictionary's values over the defaults. */\nexport function uiStrings(lang: string | undefined): Record<string, string> {\n const locale = (lang || 'en').toLowerCase()\n // tr-TR reads tr.json when there is no tr-tr.json — the store keys by primary subtag.\n const name = dictionaries.has(locale) ? locale : (locale.split('-')[0] ?? locale)\n const dictionary = dictionaries.get(name)\n const text: Record<string, string> = { ...DEFAULTS }\n const missing: string[] = []\n for (const key of Object.keys(DEFAULTS)) {\n const value = dictionary?.[key]\n if (typeof value === 'string' && value !== '') text[key] = value\n else missing.push(key)\n }\n if (!name.startsWith('en') && missing.length && !reported.has(name)) {\n reported.add(name)\n console.warn(\n dictionary\n ? '[contentrain] ui-strings: ' + name + '.json has no text for ' + missing.join(', ') + ' — those show in English'\n : '[contentrain] ui-strings: no dictionary for \"' + name + '\" at ${dir}/' + name + '.json — comments and forms show English text',\n )\n }\n return text\n}\n`\n}\n","// Components.\n//\n// `runtime`-sourced comments and forms get REAL implementations: an Astro\n// component that mounts a custom element carrying the runtime binding and the\n// page's entry address, and a client script that fetches, renders and submits\n// against the provider's public API (src/lib/embed.ts). Everything else — and\n// every runtime component when no binding was given — is a `<cr-component>`\n// placeholder that marks the spot: the emitter cannot invent an ad slot, and a\n// comments form without a provider is a promise the page cannot keep.\n//\n// A component is mounted where the chrome carries its marker\n// (`<!--@@component:ID@@-->`, see layouts.ts); emitting the file alone would\n// leave it unused, which is not integration.\n\nimport type { ComponentDef, RuntimeBinding } from '@contentrain/types'\nimport { EMBED_TS } from './embed.js'\nimport { uiStringsSource } from './ui-strings.js'\nimport { pascalCase, stableJson } from './util.js'\n\nexport interface ComponentGenResult {\n files: Record<string, string>\n warnings: string[]\n}\n\n/** Component types this emitter implements against a runtime binding. */\nexport const RUNTIME_IMPLEMENTED = new Set<ComponentDef['type']>(['comments', 'form'])\n\nexport function isRuntimeImplemented(c: ComponentDef, runtime: RuntimeBinding | undefined): boolean {\n if (!runtime || c.source !== 'runtime' || !RUNTIME_IMPLEMENTED.has(c.type)) return false\n return c.type !== 'form' || Boolean(c.model)\n}\n\nexport function componentFiles(\n components: ComponentDef[],\n runtime: RuntimeBinding | undefined,\n queryBound: ReadonlySet<string> = new Set(),\n uiStringsDir = '.contentrain/content/site/ui-strings',\n): ComponentGenResult {\n const files: Record<string, string> = {}\n const warnings: string[] = []\n let needsRuntime = false\n\n for (const c of components) {\n const name = pascalCase(c.id)\n if (isRuntimeImplemented(c, runtime)) {\n needsRuntime = true\n files[`src/components/${name}.astro`] = c.type === 'comments' ? commentsSource(c) : formSource(c)\n continue\n }\n if (c.source === 'runtime' && RUNTIME_IMPLEMENTED.has(c.type)) {\n warnings.push(\n !runtime\n ? `component ${c.id} (${c.type}): no runtime binding given — emitted as a placeholder; pass input.runtime to mount it`\n : `component ${c.id} (form): no model named — a form must say which collection it submits to; emitted as a placeholder`,\n )\n }\n files[`src/components/${name}.astro`] = placeholderSource(c, queryBound.has(c.id))\n }\n\n if (needsRuntime && runtime) {\n files['src/data/runtime.json'] = stableJson({ base_url: runtime.base_url, project_id: runtime.project_id })\n files['src/lib/embed.ts'] = EMBED_TS\n files['src/lib/ui-strings.ts'] = uiStringsSource(uiStringsDir)\n }\n return { files, warnings }\n}\n\nfunction header(c: ComponentDef, extra = ''): string {\n return `/**\n * cr-component: type=${c.type} source=${c.source}${c.name ? ` — ${c.name}` : ''}\n * Emitted by @contentrain/emitter-astro.${extra}\n */`\n}\n\nfunction placeholderSource(c: ComponentDef, queryBound: boolean): string {\n const variants = (c.variants ?? []).map((v) => v.key)\n return `---\n${header(\n c,\n `${c.source === 'runtime' ? '\\n * Placeholder — needs a live provider; see the migration handoff offers.' : ' Placeholder.'}${\n queryBound ? '\\n * A placement binds this region to a query; the layout renders it and passes the html.' : ''\n }${\n variants.length ? `\\n * Variants: ${variants.join(', ')}` : ''\n }`,\n)}\ninterface Props {\n variant?: string\n /** Content-store address of the page's entry; unused by a placeholder. */\n entry?: { model_id: string; entry_id: string; locale?: string }\n /**\n * Rendered list markup when a placement binds this region to a query. The\n * layout resolves the data — a component file is shared by id across\n * families, a placement belongs to one — so the region is editable here\n * while staying current there.\n */\n html?: string\n /** The page's language; unused by a placeholder. */\n lang?: string\n}\nconst { variant = ${JSON.stringify(variants[0] ?? 'default')}, html } = Astro.props\n---\n{html\n ? <div class=\"cr-query\" data-type=${JSON.stringify(c.type)} data-variant={variant} set:html={html} />\n : <cr-component data-type=${JSON.stringify(c.type)} data-variant={variant}></cr-component>}\n`\n}\n\nfunction commentsSource(c: ComponentDef): string {\n return `---\n${header(c, `\n * Mounted comments thread: reads the entry's approved comments from the runtime\n * provider's public API and posts new ones. Pending comments are never shown —\n * moderation happens on the provider. Renders nothing on a page without an\n * entry address (a list, a static page).`)}\nimport runtime from '../data/runtime.json'\nimport { uiStrings } from '../lib/ui-strings'\n\ninterface Props {\n /** Content-store address of the page's entry — the thread's key. */\n entry?: { model_id: string; entry_id: string; locale?: string }\n variant?: string\n /** Accepted for uniform mounting — the layout passes it to every mount; a thread renders its own markup. */\n html?: string\n /** The page's language — which ui-strings dictionary the thread's text comes from. */\n lang?: string\n}\nconst { entry, variant = 'default', lang } = Astro.props\nconst text = uiStrings(lang)\n---\n{entry && (\n <cr-comments\n class={\\`cr-comments cr-comments--\\${variant}\\`}\n data-base-url={runtime.base_url}\n data-project={runtime.project_id}\n data-model={entry.model_id}\n data-entry={entry.entry_id}\n data-locale={entry.locale}\n data-variant={variant}\n data-strings={JSON.stringify(text)}\n >\n <noscript><p class=\"cr-noscript\">{text['comments.noscript']}</p></noscript>\n </cr-comments>\n)}\n\n<script>\n import { mountComments } from '../lib/embed'\n\n class CrComments extends HTMLElement {\n connectedCallback() {\n void mountComments(this)\n }\n }\n if (!customElements.get('cr-comments')) customElements.define('cr-comments', CrComments)\n</script>\n`\n}\n\nfunction formSource(c: ComponentDef): string {\n return `---\n${header(c, `\n * Mounted public form for the ${JSON.stringify(c.model)} model: fetches the\n * exposed fields from the runtime provider, renders one control per field\n * (honeypot and captcha included when the model asks for them) and submits.\n * Submissions land on the provider for moderation; an approved one becomes a\n * content entry there.`)}\nimport runtime from '../data/runtime.json'\nimport { uiStrings } from '../lib/ui-strings'\n\ninterface Props {\n variant?: string\n /** Accepted for uniform mounting; a form is addressed by its model, not by the page's entry. */\n entry?: { model_id: string; entry_id: string; locale?: string }\n /** Accepted for uniform mounting — the layout passes it to every mount; a form renders its own markup. */\n html?: string\n /** The page's language — which ui-strings dictionary the form's text comes from. */\n lang?: string\n}\nconst { variant = 'default', lang } = Astro.props\nconst text = uiStrings(lang)\n---\n<cr-form\n class={\\`cr-form-mount cr-form-mount--\\${variant}\\`}\n data-base-url={runtime.base_url}\n data-project={runtime.project_id}\n data-model=${JSON.stringify(c.model)}\n data-variant={variant}\n data-strings={JSON.stringify(text)}\n>\n <noscript><p class=\"cr-noscript\">{text['form.noscript']}</p></noscript>\n</cr-form>\n\n<script>\n import { mountForm } from '../lib/embed'\n\n class CrForm extends HTMLElement {\n connectedCallback() {\n void mountForm(this)\n }\n }\n if (!customElements.get('cr-form')) customElements.define('cr-form', CrForm)\n</script>\n`\n}\n","// Header and footer chrome → shared Astro components.\n//\n// The body chrome stays one fragment because the content container is nested\n// deep inside it. The masthead and the colophon are the exception: they sit\n// outside the content path and repeat across families, so lifting them out\n// gives the project one address for the nav (where a jQuery-free menu replaces\n// the theme's) and one copy of the markup instead of N.\n//\n// Identity is by CONTENT, not by declared name: two families whose headers are\n// byte-identical share a component even if the producer named them differently,\n// and two different headers that claim the same name get separate files plus a\n// warning — never a silent swap, which would put one family's nav on another's\n// pages.\n\nimport type { LayoutFamily } from '@contentrain/types'\nimport { CHROME_BODY_SLOT } from '@contentrain/types'\nimport { balanceWarning } from './balance.js'\nimport { pascalCase, stableJson } from './util.js'\n\nexport type ChromePosition = 'header' | 'footer'\n\nexport interface ChromeComponentRef {\n name: string\n position: ChromePosition\n}\n\nexport interface ChromeRegistry {\n files: Record<string, string>\n /** Family id → the components its layout renders, in chunk order. */\n byFamily: Map<string, ChromeComponentRef[]>\n warnings: string[]\n}\n\nconst DEFAULT_NAME: Record<ChromePosition, string> = {\n header: 'SiteHeader',\n footer: 'SiteFooter',\n}\n\n/** A component name Astro can import: PascalCase and starting with a letter. */\nfunction componentName(raw: string): string {\n const pascal = pascalCase(raw)\n return /^[A-Za-z]/.test(pascal) ? pascal : `Chrome${pascal}`\n}\n\nfunction componentSource(name: string): string {\n return `---\n/**\n * Chrome component: ${name} — emitted by @contentrain/emitter-astro.\n * Markup travels as data so the Astro compiler never parses theme markup, and\n * fills the same \\`@@mark@@\\` placeholders the rest of the chrome does.\n */\nimport chunk from '../data/components/${name}.json'\nimport { renderTemplate } from '../lib/fill'\n\ninterface Props {\n /** Page marks — the region's own placeholders (site name, current-page class). */\n marks?: Record<string, unknown>\n}\nconst { marks = {} } = Astro.props\n---\n<Fragment set:html={renderTemplate(chunk.html, marks)} />\n`\n}\n\nexport function chromeComponents(families: LayoutFamily[]): ChromeRegistry {\n const files: Record<string, string> = {}\n const byFamily = new Map<string, ChromeComponentRef[]>()\n const warnings: string[] = []\n /** html → emitted component name, so identical regions collapse into one. */\n const byHtml = new Map<string, string>()\n /** emitted name → its html, so a name clash can be detected and renamed. */\n const byName = new Map<string, string>()\n\n for (const family of families) {\n const refs: ChromeComponentRef[] = []\n for (const chunk of family.chrome ?? []) {\n if (chunk.position !== 'header' && chunk.position !== 'footer') continue\n const position: ChromePosition = chunk.position\n const where = `family ${family.id}: ${position} chrome`\n\n if (chunk.html.includes(CHROME_BODY_SLOT)) {\n warnings.push(\n `${where} carries ${CHROME_BODY_SLOT} — content goes in the body chunk; the marker is rendered as a comment`,\n )\n }\n const unbalanced = balanceWarning(chunk.html)\n if (unbalanced) {\n warnings.push(\n `${where} is not balanced (${unbalanced}) — a lifted region must close what it opens; keep it in the body chunk`,\n )\n }\n\n const wanted = componentName(chunk.component ?? DEFAULT_NAME[position])\n let name = byHtml.get(chunk.html)\n if (name === undefined) {\n name = wanted\n for (let n = 2; byName.has(name) && byName.get(name) !== chunk.html; n++) {\n name = `${wanted}${n}`\n }\n if (name !== wanted) {\n warnings.push(\n `${where}: component name \"${wanted}\" is already used by different markup — emitted as \"${name}\"`,\n )\n }\n byHtml.set(chunk.html, name)\n byName.set(name, chunk.html)\n files[`src/data/components/${name}.json`] = stableJson({ html: chunk.html })\n files[`src/components/${name}.astro`] = componentSource(name)\n }\n refs.push({ name, position })\n }\n if (refs.length) byFamily.set(family.id, refs)\n }\n\n return { files, byFamily, warnings }\n}\n","// Legacy CSS quarantine (the fidelity layer).\n//\n// Migrated pages must render pixel-identically, and a utility framework's\n// preflight/reset would fight the theme's CSS. The truce is a cascade layer:\n// every legacy stylesheet is wrapped in `@layer legacy { … }`, and modern CSS\n// lives in its own layer — the two never meet in specificity.\n//\n// One CSS rule constrains the wrapping: `@import` (and `@charset`) must\n// precede all other statements, so a file cannot simply be wrapped whole.\n// Leading imports are hoisted above the wrapper and given `layer(legacy)`;\n// an import *after* other rules is already invalid CSS that browsers ignore,\n// so the file is left unwrapped and a warning recorded instead of silently\n// producing different invalidity.\n\nimport { LEGACY_CSS_LAYER } from '@contentrain/types'\n\nexport interface WrapCssResult {\n content: string\n warning?: string\n}\n\nconst LEADING = /^(\\s*(?:@charset[^;]+;|@import[^;]+;|\\/\\*[\\s\\S]*?\\*\\/)\\s*)+/\n\nexport function wrapLegacyCss(path: string, css: string): WrapCssResult {\n const rest = css.replace(LEADING, '')\n if (/@import\\s/.test(rest)) {\n return {\n content: css,\n warning: `${path}: @import after other rules — left unwrapped (fix the source stylesheet; mid-file imports are ignored by browsers)`,\n }\n }\n const leading = css.slice(0, css.length - rest.length)\n const hoisted = leading.replace(/@import\\s+([^;]+);/g, (full, spec: string) =>\n /\\blayer\\s*\\(/.test(spec) ? full : `@import ${spec.trim()} layer(${LEGACY_CSS_LAYER});`,\n )\n return { content: `${hoisted}@layer ${LEGACY_CSS_LAYER} {\\n${rest}\\n}\\n` }\n}\n","// The live site's own redirect rules → Astro's `redirects` config (AI-11).\n//\n// Only a plain one-to-one rule can be written: `match` absent or `url`, not a\n// regular expression, a site-root `from` without a query string, and a\n// status Astro serves as a redirect. Everything else is returned with its\n// reason as a rule to set up by hand at the host. Turning a pattern into a\n// literal `from` would produce a redirect that matches the wrong address, so\n// the emitter never guesses.\n//\n// A rule whose `from` is an address the migrated site builds a page at is\n// not written either: the page holds the content. Astro does not refuse the\n// pair — the redirect is written over the page and the build reports no\n// error, so the page would vanish silently.\n//\n// In a static build Astro serves a redirect as an HTML page with a\n// meta refresh, `noindex` and a canonical link to the target; the status\n// reaches crawlers only through a host adapter that writes the host's own\n// redirect file.\n\nimport type { RawRedirect, RouteModel } from '@contentrain/types'\nimport type { EmitContent } from './types.js'\nimport { DEFAULT_COLLECTION } from './types.js'\nimport { entryPath } from './alternates.js'\nimport { collectionItems } from './pages.js'\nimport { patternToPagePath } from './util.js'\n\n/** The statuses written to `astro.config`; any other is a manual rule. */\nexport const REDIRECT_STATUSES = [301, 302, 307, 308] as const\nexport type RedirectStatus = typeof REDIRECT_STATUSES[number]\n\n/** A rule the emitter did not write, with why. */\nexport interface ManualRedirect {\n redirect: RawRedirect\n reason: string\n}\n\nexport interface RedirectPlan {\n /** `from` → rule, as `astro.config` `redirects` takes it. Sorted by `from`. */\n config: Record<string, { status: RedirectStatus; destination: string }>\n /** The input rules written, in input order. */\n written: RawRedirect[]\n /** The rules to set up by hand at the host, in input order. */\n manual: ManualRedirect[]\n}\n\n/** Whitespace, a control character or DEL anywhere in the string. */\nconst hasControlOrSpace = (value: string): boolean =>\n [...value].some((char) => char.charCodeAt(0) <= 32 || char.charCodeAt(0) === 127)\n\n/** `/old/` and `/old` are one address in the directory build format. */\nfunction addressKey(path: string): string {\n return path === '/' ? '/' : `${path.replace(/\\/+$/, '')}/`\n}\n\n/**\n * Every address the emitted site builds a page at, as `addressKey`s, with the\n * route that owns it. Single and collection routes build one page per entry,\n * query routes one per result set, and a route without parameters one page.\n */\nexport function builtAddresses(routes: RouteModel[], content: EmitContent): Map<string, string> {\n const out = new Map<string, string>()\n const put = (path: string | null, routeId: string) => {\n if (path !== null && !out.has(addressKey(path))) out.set(addressKey(path), routeId)\n }\n for (const route of routes) {\n const pagePath = patternToPagePath(route.pattern)\n if (!pagePath) continue\n if (!pagePath.includes('[')) {\n put(entryPath(route.pattern, {}), route.id)\n continue\n }\n if (route.kind === 'single' || route.collection !== undefined) {\n for (const post of collectionItems(content, route.collection ?? DEFAULT_COLLECTION)) {\n put(entryPath(route.pattern, { ...post.params, slug: post.slug }), route.id)\n }\n }\n if (route.query) {\n for (const page of content.queries?.[route.query] ?? []) put(entryPath(route.pattern, page.params), route.id)\n }\n }\n return out\n}\n\n/** The decoded path for a literal Astro redirect key, or why `from` cannot be one. */\nfunction checkFrom(from: string, hostOnly = false): { path: string } | { reason: string } {\n if (!from.startsWith('/') || from.startsWith('//')) return { reason: 'from is not a site-root path' }\n if (/[?#]/.test(from)) return { reason: 'from has a query string or fragment — a static redirect matches the path only' }\n let path: string\n try {\n path = decodeURI(from)\n }\n catch {\n return { reason: 'from is not a valid percent-encoded path' }\n }\n if (hasControlOrSpace(path) || path.includes('\\\\')) return { reason: 'from contains whitespace, control characters or a backslash' }\n // Astro reads brackets in a route as parameters.\n if (/[[\\]]/.test(path)) return { reason: 'from contains [ or ], which Astro reads as a route parameter' }\n const segments = path.split('/').filter(Boolean)\n if (segments.some((s) => s === '.' || s === '..')) return { reason: 'from contains a . or .. segment' }\n // The directory build writes a redirect as `<from>/index.html`; a host\n // serving `/old.php` looks for that file, not the directory. A host file\n // matches the address itself, so a host-only rule may name a file.\n if (!hostOnly && /\\.[a-z0-9]{1,5}$/i.test(segments.at(-1) ?? '')) return { reason: 'from ends in a file name — the static build serves it as a directory, not at this address' }\n return { path }\n}\n\n/**\n * A path the build writes a file at: the emitter's own endpoints (robots,\n * llms.txt, every feed, @astrojs/sitemap's files), the 404 page, and the\n * directories of Astro's bundles and the legacy stylesheets. A host file\n * answers before the filesystem — a forced Netlify rule and every vercel.json\n * redirect — so a host-only rule here would shadow the file. `builtAddresses`\n * knows pages only. Compared without a trailing slash (a host file gets both\n * forms) and in lower case (Netlify matches rules case-insensitively).\n * WordPress's own `/sitemap.xml` and `/sitemap_index.xml` are not the build's:\n * a 301 from them is one a migration wants.\n */\nfunction buildOutputFile(path: string): boolean {\n const p = (path.replace(/\\/+$/, '') || '/').toLowerCase()\n return p === '/robots.txt' || p === '/llms.txt' || p === '/404.html' || p.endsWith('/feed.xml')\n || /^\\/sitemap-(?:index|\\d+)\\.xml$/.test(p) || /^\\/(?:_astro|styles)(?:\\/|$)/.test(p)\n}\n\nfunction checkTo(to: string): boolean {\n if (hasControlOrSpace(to)) return false\n if (to.startsWith('/')) return !to.startsWith('//')\n try {\n const url = new URL(to)\n return url.protocol === 'https:' || url.protocol === 'http:'\n }\n catch {\n return false\n }\n}\n\n/**\n * Split the rules into what `astro.config` serves and what the host has to.\n * `built` is `builtAddresses` of the emitted routes. `hostOnly`: the rules go\n * to host files alone (`EmitInput.hostRedirects`), so the checks that hold\n * only for a static redirect page (a `from` naming a file) are left out.\n */\nexport function planRedirects(redirects: RawRedirect[], built: Map<string, string>, options: { hostOnly?: boolean } = {}): RedirectPlan {\n const config: RedirectPlan['config'] = {}\n const written: RawRedirect[] = []\n const manual: ManualRedirect[] = []\n const claimed = new Set<string>()\n const builtLower = new Map([...built].map(([address, route]) => [address.toLowerCase(), [address, route] as const]))\n\n for (const redirect of redirects) {\n const skip = (reason: string) => manual.push({ redirect, reason })\n if (redirect.regex) { skip('from is a regular expression'); continue }\n if (redirect.match !== undefined && redirect.match !== 'url') { skip(`match \"${redirect.match}\" is a pattern, not one address`); continue }\n const status = redirect.status ?? 301\n if (!(REDIRECT_STATUSES as readonly number[]).includes(status)) { skip(`status ${status} is not one of ${REDIRECT_STATUSES.join('/')}`); continue }\n const from = checkFrom(redirect.from, options.hostOnly)\n if ('reason' in from) { skip(from.reason); continue }\n // Only a host-only rule can name a file (see checkFrom).\n if (options.hostOnly && buildOutputFile(from.path)) { skip('from is a file the build writes — the host file would serve the redirect over it'); continue }\n if (!checkTo(redirect.to)) { skip('to is not a site-root path or an http(s) URL'); continue }\n const key = addressKey(from.path)\n const owner = built.get(key)\n if (owner !== undefined) { skip(`the migrated site builds a page at ${key} (route ${owner}) — the page is kept`); continue }\n // Netlify matches rules case-insensitively, and macOS and Windows file\n // systems cannot hold /About/index.html beside /about/index.html: either\n // way the redirect would be served over the page, or loop into it.\n const caseTwin = builtLower.get(key.toLowerCase())\n if (caseTwin) { skip(`from differs only in letter case from the page at ${caseTwin[0]} (route ${caseTwin[1]}) — a case-insensitive host or file system would serve the redirect over it; the page is kept`); continue }\n if (claimed.has(key)) { skip(`another rule already redirects ${key}`); continue }\n if (!/^https?:/.test(redirect.to)) {\n let target: string\n try {\n target = decodeURI(redirect.to.replace(/[?#].*$/, ''))\n }\n catch {\n target = redirect.to\n }\n if (addressKey(target) === key) { skip('to is the same address as from'); continue }\n }\n claimed.add(key)\n config[from.path] = { status: status as RedirectStatus, destination: redirect.to }\n written.push(redirect)\n }\n\n const sorted = Object.fromEntries(Object.entries(config).toSorted(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))\n return { config: sorted, written, manual }\n}\n\n/** The `redirects` line for astro.config.mjs, or null when there is nothing to write. */\nexport function astroRedirectsConfig(config: RedirectPlan['config']): string | null {\n const entries = Object.entries(config)\n if (!entries.length) return null\n const lines = entries.map(([from, rule]) => ` ${JSON.stringify(from)}: { status: ${rule.status}, destination: ${JSON.stringify(rule.destination)} },`)\n return [` redirects: {`, ...lines, ` },`].join('\\n')\n}\n\n/** Hosts whose own redirect file the emitter can write. */\nexport type RedirectHost = 'netlify' | 'cloudflare' | 'vercel'\n\n/**\n * Cloudflare Pages takes 2,000 static rules, Vercel 2,048 redirects; each\n * rule is written twice (with and without its trailing slash).\n */\nexport const HOST_RULE_LIMIT = 2000\n\nexport interface HostRedirectFiles {\n /** Path → content, for the emitted project. */\n files: Record<string, string>\n /** Which host each written file is for, e.g. `public/_redirects (netlify)`. */\n written: string[]\n /** Config keys a host file cannot express, left to the meta-refresh fallback. */\n skipped: string[]\n /**\n * Config keys left out of a Cloudflare or Vercel file because it reached\n * that host's rule limit — served by the meta-refresh fallback only there.\n */\n over_limit: string[]\n}\n\n/** Netlify reads `:name` and `*` in a rule as placeholders. */\nconst HOST_PATTERN_CHAR = /(?:^|\\/):|\\*/\n\n/** path-to-regexp (vercel.json `source`): escape what it would read as syntax. */\nconst escapePathPattern = (s: string): string => s.replace(/[:()*+?{}\\\\]/g, (c) => `\\\\${c}`)\n\n/** A path as a host matches it: percent-encoded, with and without the trailing slash. */\nfunction hostSources(path: string): string[] {\n const encoded = encodeURI(path)\n if (encoded === '/') return ['/']\n const bare = encoded.replace(/\\/+$/, '')\n return [bare, `${bare}/`]\n}\n\n/**\n * Real HTTP redirects for the host the site deploys to. A static Astro build\n * serves `redirects` as meta-refresh pages; the status code only exists if the\n * host answers the request itself, from its own redirect file. The\n * meta-refresh pages stay as the fallback for a host without one.\n *\n * - `netlify` — `public/_redirects`, forced (`301!`): the build writes a page\n * at every redirected path, and Netlify serves an existing file before an\n * unforced rule.\n * - `cloudflare` — `public/_redirects`, unforced: Cloudflare Pages has no `!`\n * and applies its rules before static assets.\n * - `vercel` — `vercel.json` `redirects`, which Vercel applies before the\n * filesystem.\n *\n * Without a host both the Netlify file and `vercel.json` are written; a site\n * on Cloudflare Pages should name its host.\n */\nexport function hostRedirectFiles(config: RedirectPlan['config'], host?: RedirectHost): HostRedirectFiles {\n const entries = Object.entries(config)\n const skipped = entries.filter(([from]) => HOST_PATTERN_CHAR.test(from)).map(([from]) => from)\n const rules = entries.filter(([from]) => !skipped.includes(from))\n if (!rules.length) return { files: {}, written: [], skipped, over_limit: [] }\n // Netlify has no rule limit; Cloudflare's and Vercel's files stop at theirs.\n const limited = rules.slice(0, Math.floor(HOST_RULE_LIMIT / 2))\n const overLimit = host === 'netlify' ? [] : rules.slice(limited.length).map(([from]) => from)\n\n const files: Record<string, string> = {}\n const written: string[] = []\n const redirectsFile = (force: boolean, list: typeof rules) => [\n '# Emitted by @contentrain/emitter-astro — the source site\\'s redirects, as real HTTP redirects.',\n ...list.flatMap(([from, rule]) => hostSources(from).map((source) => `${source} ${rule.destination} ${rule.status}${force ? '!' : ''}`)),\n '',\n ].join('\\n')\n if (host === undefined || host === 'netlify') {\n files['public/_redirects'] = redirectsFile(true, rules)\n written.push('public/_redirects (netlify)')\n }\n if (host === 'cloudflare') {\n files['public/_redirects'] = redirectsFile(false, limited)\n written.push('public/_redirects (cloudflare)')\n }\n if (host === undefined || host === 'vercel') {\n const redirects = limited.flatMap(([from, rule]) => hostSources(from).map((source) => ({\n source: escapePathPattern(source),\n destination: rule.destination,\n statusCode: rule.status,\n })))\n files['vercel.json'] = `${JSON.stringify({ redirects }, null, 2)}\\n`\n written.push('vercel.json (vercel)')\n }\n return { files, written, skipped, over_limit: overLimit }\n}\n","// Only published content is built.\n//\n// The content store keeps every entry the source had — drafts, posts waiting\n// for review, posts scheduled for later (WordPress `future`, imported as\n// `published` with a `publish_at` ahead), password-protected and private posts.\n// Only a public entry whose status is `published` and whose `publish_at`, if it\n// has one, has come gets a page, a\n// sitemap line, a feed item, an llms.txt link or a card in a list. The rest\n// stay in the store for the editor and are held back here, before anything is\n// emitted — so every later step (pages, data files, sitemap, feed, redirects,\n// hreflang) sees the published site and nothing else.\n//\n// `publish_at` is compared with the emit time (`EmitOptions.now`). The data\n// files carry no unpublished entry, so a scheduled post goes live with the\n// next emit after its time, not by rebuilding the same files.\n//\n// A published page can link to an entry that is held back. The link would\n// 404; it is kept as its text instead, and each one is reported.\n\nimport type { RouteModel } from '@contentrain/types'\nimport type { EmitContent, EmitPost, QueryPage, WithheldEntry, WithheldLink } from './types.js'\nimport { DEFAULT_COLLECTION } from './types.js'\nimport { entryPath } from './alternates.js'\nimport { collectionItems } from './pages.js'\nimport { patternToPagePath } from './util.js'\nimport { builtAddresses } from './redirects.js'\n\n/**\n * Why an entry is not built, or null when it is. `now` is epoch ms. With\n * `requireStatus` an entry that carries no status is held back: the producer\n * said it passes every entry's status, so a missing one is a gap, not a\n * published entry.\n */\nexport function holdReason(post: Pick<EmitPost, 'status' | 'publish_at' | 'visibility'>, now: number, requireStatus = false): WithheldEntry['reason'] | null {\n // Password-protected and private content is never built, whatever its status\n // says — and a visibility this does not know is not taken for public.\n if (post.visibility !== undefined && post.visibility !== 'public') return 'visibility'\n if (post.status === undefined && requireStatus) return 'status_missing'\n if (post.status !== undefined && post.status !== 'published') return 'status'\n if (post.publish_at === undefined) return null\n const at = Date.parse(post.publish_at)\n // A schedule that cannot be read is not a schedule that has come.\n if (Number.isNaN(at)) return 'publish_at_invalid'\n return at > now ? 'scheduled' : null\n}\n\nconst bareHost = (host: string) => host.replace(/^www\\./, '').toLowerCase()\n\n/** `/a/b` and `/a/b/` are one address. */\nconst addressKey = (path: string) => (path === '/' ? '/' : `${path.replace(/\\/+$/, '')}/`)\n\nexport interface PublishedContent {\n content: EmitContent\n withheld: WithheldEntry[]\n links: WithheldLink[]\n /** The addresses held-back entries would have had and no published entry builds (`/a/b/`). */\n gone: Set<string>\n}\n\n/**\n * The held-back address an href points at, or null: a site-root path, or an\n * absolute URL on the site's own host (`www.` either way).\n */\nexport function withheldTarget(href: string, gone: Set<string>, siteUrl?: string): string | null {\n if (!gone.size) return null\n let site: URL | undefined\n try {\n site = siteUrl ? new URL(siteUrl) : undefined\n } catch {\n site = undefined\n }\n let url: URL\n try {\n url = new URL(href, site ?? 'http://site.invalid')\n } catch {\n return null\n }\n const own = site ? bareHost(url.host) === bareHost(site.host) : url.host === 'site.invalid'\n if (!own || !/^https?:$/.test(url.protocol)) return null\n let path = url.pathname\n try {\n path = decodeURI(path)\n } catch { /* keep the encoded path */ }\n return gone.has(addressKey(path)) ? path : null\n}\n\n/**\n * The content with every unpublished entry removed — from its collection and\n * from the list pages that carry it as an item — and links to them unwrapped.\n * Content with nothing held back is returned as given.\n */\nexport function publishedContent(routes: RouteModel[], content: EmitContent, now: number, siteUrl?: string, requireStatus = false): PublishedContent {\n const withheld: WithheldEntry[] = []\n const keep = (collection: string) => (post: EmitPost) => {\n const reason = holdReason(post, now, requireStatus)\n if (!reason) return true\n withheld.push({ collection, slug: post.slug, ...(post.status ? { status: post.status } : {}), ...(post.visibility ? { visibility: post.visibility } : {}), ...(post.publish_at ? { publish_at: post.publish_at } : {}), reason })\n return false\n }\n const collections = content.collections\n ? Object.fromEntries(Object.entries(content.collections).map(([name, posts]) => [name, posts.filter(keep(name))]))\n : undefined\n const isLive = (post: EmitPost) => holdReason(post, now, requireStatus) === null\n // `posts` is read only when `collections.posts` is absent; filtered either way, reported once.\n const posts = content.posts?.filter(content.collections?.[DEFAULT_COLLECTION] ? isLive : keep(DEFAULT_COLLECTION))\n const queries = content.queries\n ? Object.fromEntries(Object.entries(content.queries).map(([id, pages]) => [id, pages.map((page) => (page.items.every(isLive) ? page : { ...page, items: page.items.filter(isLive) }))]))\n : undefined\n const listedOnly = Object.values(content.queries ?? {}).flat().flatMap((p) => p.items).some((item) => !isLive(item))\n if (!withheld.length && !listedOnly) return { content, withheld, links: [], gone: new Set() }\n\n const filtered: EmitContent = {\n ...content,\n ...(posts ? { posts } : {}),\n ...(collections ? { collections } : {}),\n ...(queries ? { queries } : {}),\n }\n\n // The addresses the held-back entries would have had, less any a published\n // entry still builds (a published translation, a page on the same path).\n const built = builtAddresses(routes, filtered)\n const gone = new Set<string>()\n for (const route of routes) {\n const pagePath = patternToPagePath(route.pattern)\n if (!pagePath || !pagePath.includes('[') || (route.kind !== 'single' && route.collection === undefined)) continue\n const collection = route.collection ?? DEFAULT_COLLECTION\n for (const post of collectionItems(content, collection)) {\n const path = entryPath(route.pattern, { ...post.params, slug: post.slug })\n if (path && !isLive(post) && !built.has(addressKey(path))) gone.add(addressKey(path))\n }\n }\n\n const links: WithheldLink[] = []\n const unlink = (html: string | undefined, page: string): string | undefined => {\n if (!html || !gone.size) return html\n return html.replace(ANCHOR_RE, (anchor, double: string | undefined, single: string | undefined, inner: string) => {\n const to = withheldTarget((double ?? single ?? '').replace(/&amp;/g, '&'), gone, siteUrl)\n if (!to) return anchor\n links.push({ page, href: to })\n return inner\n })\n }\n const unlinkPost = (post: EmitPost, page: string): EmitPost => {\n const body = unlink(post.body, page) ?? post.body\n const excerpt_html = unlink(post.excerpt_html, page)\n return body === post.body && excerpt_html === post.excerpt_html ? post : { ...post, body, ...(excerpt_html !== undefined ? { excerpt_html } : {}) }\n }\n const pageOf = (collection: string, post: EmitPost) => {\n const route = routes.find((r) => (r.kind === 'single' || r.collection !== undefined) && (r.collection ?? DEFAULT_COLLECTION) === collection)\n return (route && entryPath(route.pattern, { ...post.params, slug: post.slug })) ?? `${collection}/${post.slug}`\n }\n if (filtered.collections) {\n filtered.collections = Object.fromEntries(Object.entries(filtered.collections).map(([name, list]) => [name, list.map((p) => unlinkPost(p, pageOf(name, p)))]))\n }\n if (filtered.posts) filtered.posts = filtered.posts.map((p) => unlinkPost(p, pageOf(DEFAULT_COLLECTION, p)))\n if (filtered.queries) {\n filtered.queries = Object.fromEntries(Object.entries(filtered.queries).map(([id, pages]) => [id, pages.map((page: QueryPage) => {\n const route = routes.find((r) => r.query === id)\n const at = (route && entryPath(route.pattern, page.params)) ?? `query ${id}`\n return { ...page, items: page.items.map((item) => unlinkPost(item, at)) }\n })]))\n }\n return { content: filtered, withheld, links, gone }\n}\n\n// One anchor with its href and its content. Anchors do not nest, so the first\n// closing tag is this one's.\nconst ANCHOR_RE = /<a\\b[^>]*?\\shref\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)')[^>]*>([\\s\\S]*?)<\\/a\\s*>/gi\n\n/** The report lines for what was held back. */\nexport function withheldWarnings(withheld: WithheldEntry[], links: WithheldLink[]): string[] {\n const out: string[] = []\n if (withheld.length) {\n const count = (reason: WithheldEntry['reason']) => withheld.filter((w) => w.reason === reason).length\n const parts = [\n count('visibility') && `${count('visibility')} password-protected or private`,\n count('status_missing') && `${count('status_missing')} with no status (options.requireStatus)`,\n count('status') && `${count('status')} not published (draft, in review, rejected or archived)`,\n count('scheduled') && `${count('scheduled')} scheduled for later — built by the first emit after their publish_at`,\n count('publish_at_invalid') && `${count('publish_at_invalid')} with a publish_at that is not a date`,\n ].filter(Boolean)\n out.push(`unpublished: ${withheld.length} entries not built — ${parts.join('; ')}. They stay in the content store; EmitResult.withheld lists each`)\n }\n if (links.length) {\n const sample = links.slice(0, 5).map((l) => `${l.href} (on ${l.page})`).join(', ')\n out.push(`unpublished: ${links.length} links from published pages to unpublished entries kept as plain text — ${sample}${links.length > 5 ? ' …' : ''}`)\n }\n return out\n}\n","// A page's SEO as the source's plugin held it (`RawSeo.entries[address]`,\n// what the WordPress Bridge exports) → the fields an `EmitPost` / `QueryPage`\n// carries. The emitter renders; choosing among plugins and reading their\n// output is done here once, so every producer reading a Bridge export maps it\n// the same way.\n\nimport type { RawSeoEntry, SeoProvider } from '@contentrain/types'\nimport { SEO_PROVIDERS } from '@contentrain/types'\nimport type { SocialOverride, TwitterOverride } from './types.js'\n\n/** The SEO fields of one page, ready to spread into an `EmitPost` or `QueryPage`. */\nexport interface SeoFields {\n /** For an `EmitPost`; a `QueryPage`'s `title` is already its document title. */\n seo_title?: string\n description?: string\n canonical?: string\n /** Kept out of search. */\n noindex?: boolean\n nofollow?: boolean\n open_graph?: SocialOverride\n twitter?: TwitterOverride\n schema?: unknown\n}\n\nexport interface SeoFromRawOptions {\n /** The plugin the live site's head came from (`RawSeo.serving`). Its block is read first. */\n serving?: SeoProvider | 'wordpress-core'\n /**\n * The page's own absolute address. A canonical equal to it is left out — the\n * Seo component already points a page at itself — so only a canonical that\n * sends the page somewhere else becomes an override.\n */\n url?: string\n}\n\n/**\n * A plugin's unrendered template, in its own syntax: Yoast `%%title%%`,\n * SEOPress `%%post_title%%`, Rank Math `%title%` / `%customfield(name)%`,\n * AIOSEO `#post_title`. Printed as-is it would be a literal\n * `%%title%% %%sep%%` in the search result, so such a value is dropped and\n * the page's own falls in. AIOSEO's tags are matched by name, not as any\n * `#word`: a title can hold a hashtag.\n */\nconst TEMPLATE_RE: Record<SeoProvider, RegExp> = {\n yoast: /%%[a-z0-9_-]+%%/i,\n seopress: /%%[a-z0-9_-]+%%/i,\n rank_math: /%[a-z_]+(?:\\([^)]*\\))?%/i,\n aioseo: /#(?:(?:alt|archive|attachment|author|category|current|custom_field|page|parent|post|search|separator|site|tax|taxonomy)_[a-z0-9_-]+|categories|permalink|tagline)\\b/i,\n}\n\n/**\n * Whether a string holds a template token. Percent-encoding is decoded first\n * when it decodes: `%E4%B8%AD` in a CJK URL is text, not Rank Math's `%AD%`.\n * A real token never decodes (`%ti…` is no escape), so it is tested as it is.\n */\nfunction hasToken(value: string, template: RegExp): boolean {\n let decoded = value\n try {\n decoded = decodeURI(value)\n } catch {\n // Not decodable — as a template token is not: test the string itself.\n }\n return template.test(decoded)\n}\n\nconst text = (value: string | undefined, template: RegExp | undefined): string | undefined => {\n const v = value?.trim()\n if (!v) return undefined\n return template && hasToken(v, template) ? undefined : v\n}\n\nconst addressKey = (url: string): string => url.trim().replace(/^https?:\\/\\//i, '//').replace(/\\/+(?=[?#]|$)/, '')\n\nfunction social(value: TwitterOverride | undefined, template: RegExp | undefined): TwitterOverride | undefined {\n if (!value) return undefined\n const out: TwitterOverride = {}\n const title = text(value.title, template)\n const description = text(value.description, template)\n const image = value.image?.trim()\n const card = value.card?.trim()\n if (title) out.title = title\n if (description) out.description = description\n if (image) out.image = image\n if (card) out.card = card\n return Object.keys(out).length ? out : undefined\n}\n\n/** Share-card values: the rendered ones, each missing one from the block. */\nfunction merged(rendered: TwitterOverride | undefined, stored: TwitterOverride | undefined): TwitterOverride | undefined {\n if (!rendered) return stored\n if (!stored) return rendered\n return { ...stored, ...rendered }\n}\n\n/**\n * One page's SEO fields from the blocks each plugin holds for it. The serving\n * plugin's block wins; without one the first plugin (Yoast, Rank Math,\n * AIOSEO, SEOPress) that has data does. Its values are read, in order, from:\n *\n * 1. the block itself when the running plugin rendered it (`resolved`);\n * 2. `rendered` — the exporter's own rendering of the plugin's templates, for\n * a plugin that is not the one serving the head. Final text: a variable it\n * could not render is already out of the string, and only listed in\n * `unresolved` for a person to read;\n * 3. the stored values, with the plugin's unrendered templates dropped.\n *\n * 2 and 3 are taken field by field: a value the rendering does not carry\n * falls back to the block's stored literal.\n *\n * Whatever the source, a string that still holds a template token in the\n * plugin's syntax is dropped: a token is never printed. The same holds for\n * the JSON-LD: a resolved block's graph is the plugin's rendering; any other\n * block's graph — the exporter's rendered nodes, else its own top-level graph\n * — is read node by node, and a node still holding a token is left out.\n *\n * Robots come from `robots_served` — what the page actually carried after\n * WordPress and the plugin reconciled their settings — then `rendered.robots`,\n * then the plugin's own `robots`.\n */\nexport function seoFromRawEntry(\n blocks: Partial<Record<SeoProvider, RawSeoEntry>> | undefined,\n options: SeoFromRawOptions = {},\n): SeoFields {\n if (!blocks) return {}\n const order = [\n ...(options.serving && options.serving !== 'wordpress-core' ? [options.serving] : []),\n ...SEO_PROVIDERS,\n ]\n const provider = order.find((p) => blocks[p] !== undefined)\n if (!provider) return {}\n const entry = blocks[provider]!\n // A block the running plugin rendered is final text; anything else is\n // checked for its plugin's template tokens.\n const template = entry.resolved === true ? undefined : TEMPLATE_RE[provider]\n const rendered = entry.resolved === true ? undefined : entry.rendered\n const out: SeoFields = {}\n\n // Field by field: what the exporter rendered, else the block's own value —\n // a stored literal still counts where the rendering has nothing to say, and\n // a stored template is still dropped by `text`.\n const title = text(rendered?.title, template) ?? text(entry.title, template)\n if (title) out.seo_title = title\n const description = text(rendered?.description, template) ?? text(entry.description, template)\n if (description) out.description = description\n\n const canonical = (rendered?.canonical ?? entry.canonical)?.trim()\n if (canonical && !(options.url && addressKey(canonical) === addressKey(options.url))) out.canonical = canonical\n\n const served = entry.robots_served?.map((d) => d.trim().toLowerCase())\n const robots = rendered?.robots ?? entry.robots\n const noindex = served ? served.includes('noindex') : robots?.index === 'noindex'\n const nofollow = served ? served.includes('nofollow') : robots?.follow === 'nofollow'\n if (noindex) out.noindex = true\n if (nofollow) out.nofollow = true\n\n const og = merged(social(rendered?.open_graph, template), social(entry.open_graph, template))\n if (og) out.open_graph = og\n // The card is a setting, not rendered text: it comes from the block.\n const tw = merged(social(rendered?.twitter, template), social(entry.twitter, template))\n if (tw) out.twitter = tw\n\n // The plugin's rendered graph; else the stored schema nodes the exporter\n // rendered (Rank Math), as a graph of their own.\n // JSON-LD: the graph the running plugin rendered, from a resolved block\n // only. Otherwise a block's top-level graph may be the stored nodes, still\n // holding templates, so the exporter's rendering is read instead — node by\n // node, and a node that still holds a template token is left out.\n if (entry.resolved === true) {\n const graph = entry.schema?.graph\n if (graph && typeof graph === 'object') out.schema = graph\n } else if (template) {\n // The rendering; without one, the block's own graph — both node by node.\n const top = entry.schema?.graph\n const source = Array.isArray(rendered?.schema?.graph)\n ? rendered.schema.graph\n : ldNodes(top)\n const nodes = source.filter((node) => !holdsToken(node, template))\n if (nodes.length) out.schema = { '@context': 'https://schema.org', '@graph': nodes }\n }\n return out\n}\n\n/** A JSON-LD value's nodes: a @graph object's graph, a node array, or one node. */\nfunction ldNodes(value: unknown): unknown[] {\n if (!value || typeof value !== 'object') return []\n if (Array.isArray(value)) return value\n const graph = (value as { '@graph'?: unknown })['@graph']\n return Array.isArray(graph) ? graph : [value]\n}\n\n/** Whether any string anywhere in a JSON value holds a template token. */\nfunction holdsToken(value: unknown, template: RegExp): boolean {\n if (typeof value === 'string') return hasToken(value, template)\n if (Array.isArray(value)) return value.some((v) => holdsToken(v, template))\n if (value && typeof value === 'object') return Object.values(value).some((v) => holdsToken(v, template))\n return false\n}\n","// @contentrain/emitter-astro — the open half of the migration pipeline.\n//\n// A ProjectIR (route model, layout families, component variants, query\n// bindings, design tokens) plus prepared content goes in; a complete Astro\n// project comes out as a pure file map. The analysis that *produces* a good\n// ProjectIR is hard and lives elsewhere; rendering one is deliberately\n// boring — which is exactly why it can be open, portable, and replaceable\n// by community emitters for other frameworks.\n\nimport type { RouteModel } from '@contentrain/types'\nimport type { EmitInput, EmitPost, EmitResult, RawRedirect } from './types.js'\nimport { DEFAULT_COLLECTION } from './types.js'\nimport { scaffoldFiles } from './scaffold.js'\nimport { imagesEnabled } from './images.js'\nimport { componentMarkers, familyFiles } from './layouts.js'\nimport { collectionItems, routeFiles, sharedCollectionPage } from './pages.js'\nimport { componentFiles, isRuntimeImplemented } from './components.js'\nimport { chromeComponents } from './chrome.js'\nimport { SEO_COMPONENT } from './seo.js'\nimport { entryPath, withAlternates } from './alternates.js'\nimport { UI_STRINGS_DIR, uiStringsDir } from './ui-strings.js'\nimport { wrapLegacyCss } from './css.js'\nimport { stableJson, patternToPagePath } from './util.js'\nimport { noindexPaths } from './noindex.js'\nimport { publishedContent, withheldTarget, withheldWarnings } from './published.js'\nimport { astroRedirectsConfig, builtAddresses, HOST_RULE_LIMIT, hostRedirectFiles, planRedirects } from './redirects.js'\nimport type { ManualRedirect } from './redirects.js'\nimport { FEED_PATH, FEED_REDIRECT_FROM, archiveFeedEndpoint, archiveFeedFile, archiveFeedSources, feedEndpoint, linkSources, llmsEndpoint, type LinkSource } from './feed.js'\n\n/**\n * A supplied trail the build will not print — the same rule as \\`validTrail\\`\n * in the emitted runtime, which a test holds this to.\n */\nfunction badTrail(trail: EmitPost['breadcrumbs']): boolean {\n return trail !== undefined && !(trail.length > 0 && trail.every((c) =>\n typeof c?.name === 'string' && c.name.trim() !== '' && typeof c.path === 'string' && !c.path.includes(String.fromCharCode(92)) && ![...c.path].some((char) => char.charCodeAt(0) <= 32) && /^\\/(?!\\/)/.test(c.path)))\n}\n\n/**\n * A source JSON-LD value the Seo component cannot print (see\n * `sourceStructuredData`): not an object, or no node in it names a type.\n */\nfunction badSchema(schema: unknown): boolean {\n if (schema === undefined) return false\n if (!schema || typeof schema !== 'object') return true\n const graph = (schema as { '@graph'?: unknown })['@graph']\n const nodes: unknown[] = Array.isArray(schema) ? schema : Array.isArray(graph) ? graph : [schema]\n return !nodes.some((n) => {\n const type = n && typeof n === 'object' && !Array.isArray(n) ? (n as Record<string, unknown>)['@type'] : undefined\n return (Array.isArray(type) ? type : [type]).some((t) => typeof t === 'string')\n })\n}\n\n/** Redirect config sorted by `from`, the order astro.config and the host files list it in. */\nfunction sortedConfig<T>(config: Record<string, T>): Record<string, T> {\n return Object.fromEntries(Object.entries(config).toSorted(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))\n}\n\n/** A redirect's from as its config key: percent-encoding decoded, as planRedirects writes it. */\nfunction decodedPath(from: string): string {\n try {\n return decodeURI(from)\n } catch {\n return from\n }\n}\n\nexport function emitAstroProject(input: EmitInput): EmitResult {\n const { ir } = input\n const warnings: string[] = []\n const files: Record<string, string> = {}\n const add = (batch: Record<string, string>) => {\n for (const [path, content] of Object.entries(batch)) {\n if (files[path] !== undefined && files[path] !== content) {\n // Pages are reported by the route loop below, which knows *which two\n // routes* collided and what is lost. Saying \"duplicate file\" here as\n // well would bury that under the vaguer message.\n if (!path.startsWith('src/pages/')) {\n warnings.push(`duplicate file with different content: ${path} — keeping the first`)\n }\n continue\n }\n files[path] = content\n }\n }\n\n // Only published entries are built: drafts, entries in review and ones\n // scheduled for later are held back before anything reads the content.\n let now = Date.now()\n if (input.options?.now !== undefined) {\n const at = Date.parse(input.options.now)\n if (Number.isNaN(at)) warnings.push(`options.now \"${input.options.now}\" is not a date — publish_at is compared with the time of this emit`)\n else now = at\n }\n const published = publishedContent(ir.routes, input.content ?? {}, now, ir.site.url, input.options?.requireStatus === true)\n const content = published.content\n warnings.push(...withheldWarnings(published.withheld, published.links))\n // A rule to a held-back entry would redirect to a page the build does not\n // have. It goes back to the producer with the rest of the manual rules.\n const withheldManual: ManualRedirect[] = []\n const toPublished = (rules: RawRedirect[] | undefined) => rules?.filter((redirect) => {\n const target = withheldTarget(redirect.to, published.gone, ir.site.url)\n if (target) withheldManual.push({ redirect, reason: `to ${target}, an entry that is not published — the build has no page there` })\n return !target\n })\n const siteRedirects = toPublished(input.redirects)\n const hostOnlyRedirects = toPublished(input.hostRedirects)\n if (withheldManual.length) {\n warnings.push(`unpublished: ${withheldManual.length} redirects point at unpublished entries — not written (EmitResult.redirects.manual has each)`)\n }\n\n // The site's own redirect rules. Only plain ones become config; the rest,\n // and any on an address this site builds a page at, go back to the producer.\n const siteLang = ir.site.locales?.[0] ?? 'en'\n // The feed and llms.txt name every page by its absolute address.\n const linkable = ir.site.url ? linkSources(ir.routes, siteLang) : []\n const feedSource = input.options?.feed !== false ? linkable.find((s) => s.collection === DEFAULT_COLLECTION) : undefined\n const llmsOn = input.options?.llms !== false && linkable.length > 0\n // Every archive page (a category, a tag, an author) gets its feed, as in WordPress.\n const archiveFeeds = feedSource ? archiveFeedSources(ir.routes, siteLang) : []\n const archiveFeedRoutes = new Set(archiveFeeds.map((a) => a.routeId))\n if (!ir.site.url && (input.options?.feed !== false || input.options?.llms !== false)) {\n warnings.push('site.url is empty — no RSS feed and no llms.txt are built; both name pages by absolute address')\n }\n const redirectPlan = siteRedirects ? planRedirects(siteRedirects, builtAddresses(ir.routes, content)) : undefined\n if (redirectPlan?.manual.length) {\n warnings.push(`redirects: ${redirectPlan.manual.length} of ${siteRedirects!.length} rules not written to astro.config — set them up at the host (EmitResult.redirects.manual has each with its reason)`)\n }\n\n // Per-page SEO is on unless the producer owns those tags itself.\n const seo = input.options?.seo !== false\n const noindex = noindexPaths(ir.routes, content)\n // Readers subscribed to WordPress's /feed/ follow a 301 to the feed the\n // build writes — a real one from the host's redirect file; a static\n // redirect page, which feed readers do not follow, is only the fallback.\n // A rule the source itself holds for /feed/ wins.\n // The feed addresses WordPress served: /feed/ and each archive page's\n // <archive>/feed/, 301 to the feeds the build writes. They pass the same\n // checks as the source's rules — never over a page the site builds, never\n // a case twin of one — and a source rule for the same address wins.\n const built = builtAddresses(ir.routes, content)\n const sourceConfig = redirectPlan?.config ?? {}\n const claimed = new Set(Object.keys(sourceConfig).map((from) => from.replace(/\\/+$/, '').toLowerCase()))\n const feedRules: RawRedirect[] = []\n const addFeedRule = (from: string, to: string) => {\n const key = from.replace(/\\/+$/, '').toLowerCase()\n if (claimed.has(key)) return\n claimed.add(key)\n feedRules.push({ from, to, status: 301 })\n }\n if (feedSource) addFeedRule(FEED_REDIRECT_FROM, FEED_PATH)\n for (const source of archiveFeeds) {\n for (const page of content.queries?.[source.query] ?? []) {\n const address = entryPath(source.pattern, page.params)\n if (address) addFeedRule(`${address}feed/`, `${address}feed.xml`)\n }\n }\n const feedPlan = planRedirects(feedRules, built)\n if (feedPlan.manual.length) {\n warnings.push(`feed: ${feedPlan.manual.length} feed redirects not written — ${feedPlan.manual.map((m) => `${m.redirect.from} (${m.reason})`).join('; ')}`)\n }\n const redirectConfig = { ...sourceConfig, ...feedPlan.config }\n const hasRedirects = Object.keys(redirectConfig).length > 0\n add(scaffoldFiles(ir, input.options ?? {}, noindex, hasRedirects ? astroRedirectsConfig(sortedConfig(redirectConfig)) : null, input.runtime))\n // Host-only rules: the same checks, then an address the site's own rules or\n // the feeds already redirect is theirs. Never in astro.config.\n const hostOnlyPlan = hostOnlyRedirects ? planRedirects(hostOnlyRedirects, built, { hostOnly: true }) : undefined\n const hostOnlyConfig: typeof redirectConfig = {}\n const hostOnlyRule = new Map<string, RawRedirect>()\n const hostOnlyManual: ManualRedirect[] = [...(hostOnlyPlan?.manual ?? [])]\n if (hostOnlyPlan) {\n const byFrom = new Map(hostOnlyPlan.written.map((rule) => [decodedPath(rule.from), rule]))\n for (const [from, rule] of Object.entries(hostOnlyPlan.config)) {\n const redirect = byFrom.get(from)!\n const key = from.replace(/\\/+$/, '').toLowerCase()\n if (claimed.has(key)) {\n hostOnlyManual.push({ redirect, reason: `another rule already redirects ${from}` })\n continue\n }\n claimed.add(key)\n hostOnlyConfig[from] = rule\n hostOnlyRule.set(from, redirect)\n }\n }\n // Host files have a rule limit and keep the first rules: the source's own\n // come first, the feeds' after, so an overflow drops a feed redirect.\n // Among the feeds, /feed/ first, then the archives in their data order.\n const feedOrder = new Map(feedRules.map((rule, index) => [decodedPath(rule.from), index]))\n const feedKeys = Object.keys(feedPlan.config).toSorted((a, b) => (feedOrder.get(a) ?? 0) - (feedOrder.get(b) ?? 0))\n // Host-only rules come last: past a host's limit they are the ones left out.\n const hostOrder = { ...sortedConfig(sourceConfig), ...Object.fromEntries(feedKeys.map((key) => [key, feedPlan.config[key]!])), ...sortedConfig(hostOnlyConfig) }\n const hostRedirects = Object.keys(hostOrder).length ? hostRedirectFiles(hostOrder, input.options?.redirectHost) : undefined\n // A host-only rule the host file cannot hold has no meta-refresh page behind\n // it — it would not be served at all — so it is set up by hand.\n const hostOnly = (from: string) => hostOnlyRule.has(from)\n for (const from of hostRedirects?.skipped ?? []) {\n if (hostOnly(from)) hostOnlyManual.push({ redirect: hostOnlyRule.get(from)!, reason: 'from contains \":\" or \"*\", which host redirect files read as a pattern' })\n }\n // Past a limited file: with its host named nothing serves the rule, so it is\n // manual; with no host named the Netlify file still holds it, so it is\n // written, and named in host_over_limit for the Vercel file it is missing from.\n const namedHost = input.options?.redirectHost\n const hostOnlyOver = (hostRedirects?.over_limit ?? []).filter(hostOnly)\n if (namedHost !== undefined) {\n for (const from of hostOnlyOver) hostOnlyManual.push({ redirect: hostOnlyRule.get(from)!, reason: `left out of ${hostRedirects!.written.join(', ')} at its rule limit (${HOST_RULE_LIMIT / 2} rules) — a host-only rule has no meta-refresh page to fall back on` })\n }\n else if (hostOnlyOver.length) {\n warnings.push(`hostRedirects: ${hostOnlyOver.length} host-only rules not written to vercel.json at its rule limit (${HOST_RULE_LIMIT / 2} rules) — public/_redirects (Netlify) has them; on Vercel they are not served. Name the host (options.redirectHost) to have them reported as manual`)\n }\n const unservedHostOnly = new Set(hostOnlyManual.map((m) => m.redirect))\n if (hostOnlyManual.length) {\n warnings.push(`hostRedirects: ${hostOnlyManual.length} of ${hostOnlyRedirects!.length} host-only rules not written — set them up at the host (EmitResult.redirects.manual has each with its reason)`)\n }\n if (hostRedirects) {\n add(hostRedirects.files)\n const overLimit = hostRedirects.over_limit.filter((from) => !hostOnly(from))\n const skippedPatterns = hostRedirects.skipped.filter((from) => !hostOnly(from))\n if (overLimit.length) {\n const limitedFiles = hostRedirects.written.filter((f) => !f.includes('(netlify)')).join(', ')\n const feedOver = overLimit.filter((from) => from in feedPlan.config).length\n const which = feedOver ? ` (${feedOver} of them feed redirects — /feed/ and archive feeds come after the site's own rules)` : ''\n warnings.push(`redirects: ${overLimit.length} rules not written to ${limitedFiles}${which} — ${HOST_RULE_LIMIT / 2} rules (each path with and without its slash) fill Cloudflare Pages' 2,000 static and Vercel's 2,048 redirect limit. They are served by the meta-refresh fallback only; move them to the host's dynamic rules for a real status`)\n }\n if (skippedPatterns.length) {\n warnings.push(`redirects: ${skippedPatterns.length} rules contain \":\" or \"*\", which host redirect files read as patterns — served by the meta-refresh fallback only: ${skippedPatterns.join(', ')}`)\n }\n }\n\n // Without trailing slashes the build writes /hello.html; Vercel serves it\n // at /hello only with cleanUrls (Netlify and Cloudflare Pages do by default).\n if (input.options?.trailingSlash === false && (input.options.redirectHost ?? 'vercel') === 'vercel') {\n const vercel = files['vercel.json'] ? JSON.parse(files['vercel.json']) as Record<string, unknown> : {}\n files['vercel.json'] = `${JSON.stringify({ cleanUrls: true, trailingSlash: false, ...vercel }, null, 2)}\\n`\n }\n\n if (noindex.length && !seo) {\n warnings.push(`${noindex.length} noindex pages: options.seo is false, so no robots meta is emitted — the producer's head must carry it (they are still left out of the sitemap)`)\n }\n if (seo) {\n add({ 'src/components/Seo.astro': SEO_COMPONENT })\n if (!ir.site.url) {\n warnings.push('site.url is empty — canonical links and absolute og:url/og:image are omitted; set it so search engines and share cards resolve')\n }\n }\n if (input.options?.sitemap !== false && !ir.site.url) {\n warnings.push('site.url is empty — no sitemap is generated and robots.txt names none; set it so search engines can find every page')\n }\n\n const familiesById = new Map(ir.families.map((f) => [f.id, f]))\n const lang = ir.site.locales?.[0] ?? 'en'\n // Header/footer regions first: families that share one share the component.\n const chrome = chromeComponents(ir.families)\n add(chrome.files)\n warnings.push(...chrome.warnings)\n const definitions = new Map((ir.components ?? []).map((c) => [c.id, c]))\n // A component marker can sit inside a post's own body (a form in a page's\n // content). The layout that renders that body must mount it, so collect the\n // ids per family from the collections its routes render.\n const bodyMarkersByFamily = new Map<string, Set<string>>()\n for (const route of ir.routes) {\n if (route.kind !== 'single' && route.collection === undefined) continue\n const posts = collectionItems(content, route.collection ?? DEFAULT_COLLECTION)\n const ids = bodyMarkersByFamily.get(route.family) ?? new Set<string>()\n for (const post of posts) for (const id of componentMarkers(post.body)) ids.add(id)\n if (ids.size) bodyMarkersByFamily.set(route.family, ids)\n }\n // A placement may bind its region to a query. Validated before the families\n // are emitted, because the layout emits an import for the query's data file:\n // an import of a file the producer never supplied is a build error with no\n // explanation in it. A binding that does not check out is dropped here, so\n // the region stays the cloned markup the warning says it stays.\n const boundQueries = new Set<string>()\n const queryBound = new Set<string>()\n for (const family of ir.families) {\n for (const placement of family.components ?? []) {\n if (!placement.query) continue\n const pages = content.queries?.[placement.query]\n if (!pages) {\n warnings.push(\n `family ${family.id}: placement \"${placement.component}\" binds query \"${placement.query}\", `\n + 'which is not in input.content.queries — binding dropped, region left as cloned markup',\n )\n continue\n }\n if (pages.length !== 1) {\n warnings.push(\n `family ${family.id}: placement \"${placement.component}\" binds query \"${placement.query}\", `\n + `which has ${pages.length} result sets — a page region has no route parameter to choose between `\n + 'them, so the build stops rather than rendering one of them everywhere',\n )\n } else if (!pages[0]!.item_template && !pages[0]!.sections?.length) {\n warnings.push(\n `family ${family.id}: placement \"${placement.component}\" binds query \"${placement.query}\", `\n + 'which has no item_template or sections — plain fallback list rendered (not fidelity)',\n )\n }\n // The layout imports this file. A route that renders the same query also\n // writes it, but a region is not a route: bound without one, nothing else\n // would, and the import would resolve to nothing.\n add({ [`src/data/queries/${placement.query}.json`]: stableJson(pages) })\n boundQueries.add(placement.query)\n queryBound.add(placement.component)\n }\n }\n\n for (const family of ir.families) {\n const fam = familyFiles(family, lang, chrome.byFamily.get(family.id), definitions, bodyMarkersByFamily.get(family.id) ?? [], { seo, siteName: ir.site.title, boundQueries, images: imagesEnabled(input.options ?? {}), feedSite: feedSource ? ir.site.url : undefined, trailingSlash: input.options?.trailingSlash })\n add(fam.files)\n warnings.push(...fam.warnings)\n }\n\n for (const css of input.css ?? []) {\n const wrapped = wrapLegacyCss(css.path, css.content)\n if (wrapped.warning) warnings.push(wrapped.warning)\n const base = css.path.split('/').pop() ?? css.path\n add({ [`public/styles/legacy/${base}`]: wrapped.content })\n }\n const providedCss = new Set((input.css ?? []).map((c) => c.path.split('/').pop() ?? c.path))\n const missingCss = (refs: string[] | undefined, owner: string) => {\n for (const ref of refs ?? []) {\n if (!providedCss.has(ref.split('/').pop() ?? ref)) {\n warnings.push(`${owner}: css file \"${ref}\" not provided in input.css`)\n }\n }\n }\n for (const family of ir.families) missingCss(family.css.files, `family ${family.id}`)\n const allPosts = [\n ...(content.posts ?? []),\n ...Object.values(content.collections ?? {}).flat(),\n ]\n for (const post of allPosts) missingCss(post.css, `post ${post.slug}`)\n for (const [queryId, queryPages] of Object.entries(content.queries ?? {})) {\n for (const qp of queryPages) missingCss(qp.css, `query ${queryId}`)\n }\n\n // Two routes can want the same Astro page. WordPress serves posts and pages\n // from the same root and tells them apart in the database; a static\n // generator cannot, so `/:slug` for posts and `/:slug` for pages resolve to\n // one file. Left to `add`, the second route's pages simply vanish — every\n // page of it, with a warning that names a file rather than a route, so the\n // producer learns a file was duplicated and not that a section of the site\n // is missing.\n //\n // The pages are unrecoverable at this point either way: the emitter cannot\n // know which route should own the path. What it can do is refuse to produce\n // a site that is quietly wrong.\n // A breadcrumb trail with a bad crumb prints nothing at build; say so here.\n const trailOwners: Array<[string, EmitPost[] | undefined]> = [\n ...Object.entries(content.collections ?? {}),\n ...(content.posts ? [[DEFAULT_COLLECTION, content.posts] as [string, EmitPost[]]] : []),\n ]\n for (const [name, posts] of trailOwners) {\n const bad = (posts ?? []).filter((p) => badTrail(p.breadcrumbs)).length\n if (bad) warnings.push(`collection ${name}: ${bad} breadcrumb trails dropped — every crumb needs a name and a site-root-relative path`)\n const badLd = (posts ?? []).filter((p) => badSchema(p.schema)).length\n if (badLd) warnings.push(`collection ${name}: ${badLd} schema values are not JSON-LD objects — those pages print the generated structured data instead`)\n }\n for (const [queryId, queryPages] of Object.entries(content.queries ?? {})) {\n const bad = queryPages.filter((qp) => badTrail(qp.breadcrumbs)).length\n if (bad) warnings.push(`query ${queryId}: ${bad} breadcrumb trails dropped — every crumb needs a name and a site-root-relative path`)\n const badLd = queryPages.filter((qp) => badSchema(qp.schema)).length\n if (badLd) warnings.push(`query ${queryId}: ${badLd} schema values are not JSON-LD objects — those pages print the generated structured data instead`)\n }\n\n // hreflang alternates ride on the entry data, so they are attached before the\n // routes write it. The Seo component renders them; without it the producer\n // owns the head, alternates included.\n let routeContent = content\n if (seo) {\n const alternates = withAlternates(ir.routes, routeContent, lang)\n routeContent = alternates.content\n warnings.push(...alternates.warnings)\n }\n const routeGroups = new Map<string, RouteModel[]>()\n for (const route of ir.routes) {\n const path = patternToPagePath(route.pattern)\n if (path) routeGroups.set(path, [...(routeGroups.get(path) ?? []), route])\n }\n const sharedPages = new Map<string, string>()\n for (const [path, group] of routeGroups) {\n if (!group.every(route => familiesById.has(route.family))) continue\n const page = sharedCollectionPage(group, routeContent, lang, seo)\n if (page) sharedPages.set(`src/pages/${path}`, page)\n }\n const pageOwner = new Map<string, RouteModel>()\n const collisions: { path: string, first: RouteModel, second: RouteModel }[] = []\n for (const route of ir.routes) {\n const result = routeFiles(route, familiesById.get(route.family), routeContent, lang, seo, archiveFeedRoutes.has(route.id))\n for (const path of sharedPages.keys()) delete result.files[path]\n for (const path of Object.keys(result.files)) {\n if (!path.startsWith('src/pages/')) continue\n const owner = pageOwner.get(path)\n if (owner === undefined) {\n pageOwner.set(path, route)\n continue\n }\n // Any second claim on a page path is a collision, even when the two\n // routes would render the same thing: Astro serves one file per path, so\n // one of the two routes does not exist in the built site. Comparing the\n // emitted bytes instead would be a weaker test that happens to agree —\n // the page carries its route id in a comment, so two routes never\n // produce identical output anyway.\n collisions.push({ path, first: owner, second: route })\n }\n add(result.files)\n warnings.push(...result.warnings)\n }\n\n for (const [path, page] of sharedPages) files[path] = page\n\n for (const { path, first, second } of collisions) {\n warnings.push(\n `route ${second.id}: pattern \"${second.pattern}\" emits the same Astro page as route ${first.id} `\n + `(\"${first.pattern}\") — ${path}. Every page of \"${second.id}\" would be dropped, so the page now `\n + `fails the build instead. Give one route a distinct pattern, or expand the narrower one into literal routes.`,\n )\n files[path] = collisionPage(path, first, second)\n }\n\n const stringsDir = uiStringsDir(input.options?.uiStrings?.dir)\n if (stringsDir === undefined) {\n warnings.push(`options.uiStrings.dir \"${input.options?.uiStrings?.dir}\" is not a project-relative directory — using ${UI_STRINGS_DIR}`)\n }\n const components = componentFiles(ir.components ?? [], input.runtime, queryBound, stringsDir ?? UI_STRINGS_DIR)\n add(components.files)\n warnings.push(...components.warnings)\n // Comments and forms speak the page's language only if the dictionary has\n // it. The build reports a missing file too; saying it here puts it in the\n // same report as everything else the producer has to act on.\n if (components.files['src/lib/ui-strings.ts']) {\n const pageLangs = [...new Set([lang, ...ir.routes.map((r) => r.locale).filter((l): l is string => Boolean(l))])]\n .filter((l) => !l.toLowerCase().startsWith('en'))\n const declared = input.options?.uiStrings?.locales\n const uncovered = declared ? pageLangs.filter((l) => !declared.includes(l) && !declared.includes(l.split('-')[0]!)) : pageLangs\n if (uncovered.length) {\n warnings.push(\n `ui-strings: no dictionary ${declared ? 'declared' : 'declared in options.uiStrings.locales'} for ${uncovered.join(', ')} — `\n + `comments and forms on those pages show English text unless ${stringsDir ?? UI_STRINGS_DIR}/{locale}.json exists at build`,\n )\n }\n }\n\n // A mounted comments thread is keyed by the post's entry address; a post\n // without one renders no thread. Say so once per collection, not per page.\n const commentsMounted = (ir.components ?? []).some((c) => isRuntimeImplemented(c, input.runtime) && c.type === 'comments')\n if (commentsMounted) {\n const collections: Record<string, EmitPost[]> = { ...content.collections }\n if (content.posts && !collections[DEFAULT_COLLECTION]) collections[DEFAULT_COLLECTION] = content.posts\n for (const [name, posts] of Object.entries(collections)) {\n const unbound = posts.filter((p) => !p.entry).length\n if (unbound) warnings.push(`collection ${name}: ${unbound} of ${posts.length} posts carry no entry address — the comments component renders nothing on those pages`)\n }\n }\n\n // Endpoints over the data files the routes wrote; a collection whose file\n // was not written (its route was dropped) has no pages to list.\n const feedInput = { ir, siteLocale: lang, description: input.options?.siteDescription, trailingSlash: input.options?.trailingSlash !== false }\n const written = (source: LinkSource) => files[`src/data/${source.collection}.json`] !== undefined\n if (feedSource) {\n if (written(feedSource)) {\n add({ 'src/pages/feed.xml.ts': feedEndpoint(feedSource, feedInput) })\n } else {\n warnings.push(`feed: no posts data file was written, so no feed is built — the head's feed link points at ${FEED_PATH}, which 404s`)\n }\n }\n for (const source of archiveFeeds) {\n const file = archiveFeedFile(source.pattern)\n if (file && feedSource && files[`src/data/queries/${source.query}.json`] !== undefined) {\n add({ [file]: archiveFeedEndpoint(source, feedSource.pattern, feedInput) })\n }\n }\n if (llmsOn) {\n const listed = linkable.filter(written)\n if (listed.length) add({ 'src/pages/llms.txt.ts': llmsEndpoint(listed, feedInput) })\n }\n\n return {\n files,\n warnings,\n ...(redirectPlan || hostOnlyPlan\n ? {\n redirects: {\n written: [...(redirectPlan?.written ?? []), ...[...hostOnlyRule.values()].filter((rule) => !unservedHostOnly.has(rule))],\n manual: [...(redirectPlan?.manual ?? []), ...hostOnlyManual, ...withheldManual],\n host_files: hostRedirects?.written ?? [],\n // A host-only rule past a named host's limit is in `manual` instead: nothing serves it.\n host_over_limit: (hostRedirects?.over_limit ?? []).filter((from) => !hostOnly(from) || namedHost === undefined),\n },\n }\n : {}),\n ...(published.withheld.length || published.links.length ? { withheld: { entries: published.withheld, links: published.links } } : {}),\n }\n}\n\n/**\n * Replaces a page two routes both claim. The emitter cannot choose between\n * them, so the page states the conflict and throws — `astro build` stops with\n * the two route ids in the message, instead of shipping a site that is missing\n * a section nobody was told about.\n */\nfunction collisionPage(path: string, first: RouteModel, second: RouteModel): string {\n const message = `Route collision: \"${first.id}\" (${first.pattern}) and \"${second.id}\" (${second.pattern}) `\n + `both resolve to ${path}. A static build cannot serve both from one file — give one route a distinct `\n + `pattern, or expand the narrower one into literal routes.`\n const thrown = `throw new Error(${JSON.stringify(message)})`\n // On a dynamic path the throw has to live in `getStaticPaths`. Astro collects\n // paths before it renders anything, so a throw in the frontmatter would never\n // run — the build would fail on the missing `getStaticPaths` instead, with\n // Astro's generic message in place of the one that names the two routes.\n // On a static path the opposite holds: exporting `getStaticPaths` at all is\n // itself an error (\"only supported in dynamic routes\").\n return path.includes('[')\n ? `---\\nexport function getStaticPaths() {\\n ${thrown}\\n}\\n---\\n`\n : `---\\n${thrown}\\n---\\n`\n}\n\n/** Write an EmitResult to disk. Separate from emit so the core stays pure. */\nexport async function writeEmit(result: EmitResult, dir: string): Promise<void> {\n const { mkdir, writeFile } = await import('node:fs/promises')\n const { dirname, join } = await import('node:path')\n const dirs = new Set(Object.keys(result.files).map((p) => dirname(join(dir, p))))\n await Promise.all([...dirs].map((d) => mkdir(d, { recursive: true })))\n await Promise.all(\n Object.entries(result.files).map(([path, content]) => writeFile(join(dir, path), content, 'utf8')),\n )\n}\n\nexport type {\n EmitInput,\n EmitResult,\n EmitContent,\n EmitPost,\n QueryPage,\n EmitCssFile,\n EmitOptions,\n EmitTermRef,\n AuthorProfile,\n Breadcrumb,\n ImageMeta,\n SocialOverride,\n TwitterOverride,\n RuntimeBinding,\n RawRedirect,\n EntrySourceRef,\n WithheldEntry,\n WithheldLink,\n} from './types.js'\nexport { wrapLegacyCss } from './css.js'\nexport { FEED_ITEMS, FEED_PATH, LLMS_LINKS, linkSources, rewriteFeedLinks } from './feed.js'\nexport type { FeedLinkResult, LinkSource } from './feed.js'\nexport { pascalCase, patternToPagePath, stableJson } from './util.js'\nexport { componentMarkers } from './layouts.js'\nexport type { MountRef } from './layouts.js'\nexport { isRuntimeImplemented, RUNTIME_IMPLEMENTED } from './components.js'\nexport { bodySeoLeaks, publisherIdOf, stripSeoTags, websiteIdOf, SEO_COMPONENT } from './seo.js'\nexport { seoFromRawEntry } from './seo-entry.js'\nexport type { SeoFields, SeoFromRawOptions } from './seo-entry.js'\nexport { entryPath, withAlternates } from './alternates.js'\nexport type { Alternate, AlternatesResult } from './alternates.js'\nexport type { StripResult } from './seo.js'\nexport { EMBED_TS } from './embed.js'\nexport { UI_STRING_DEFAULTS, UI_STRINGS_DIR, UI_STRINGS_MODEL } from './ui-strings.js'\nexport type { UiStringKey } from './ui-strings.js'\nexport { checkBalance, balanceWarning } from './balance.js'\nexport { noindexPaths } from './noindex.js'\nexport { holdReason, publishedContent } from './published.js'\nexport type { PublishedContent } from './published.js'\nexport { astroRedirectsConfig, builtAddresses, hostRedirectFiles, planRedirects, REDIRECT_STATUSES } from './redirects.js'\nexport type { HostRedirectFiles, ManualRedirect, RedirectHost, RedirectPlan, RedirectStatus } from './redirects.js'\nexport type { BalanceReport } from './balance.js'\n"],"mappings":"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"}