@getvitops/astro 8.2.0-alpha.0 → 8.3.0-alpha.0
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.
- package/CHANGELOG.md +584 -26
- package/dist/config-adapters-CbTywgEY.mjs +71 -0
- package/dist/config-adapters-CbTywgEY.mjs.map +1 -0
- package/dist/exports/tracking.d.mts +1 -1
- package/dist/exports/tracking.mjs +1 -1
- package/dist/index.d.mts +83 -43
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +351 -60
- package/dist/index.mjs.map +1 -1
- package/dist/middleware.d.mts +0 -2
- package/dist/middleware.d.mts.map +1 -1
- package/dist/middleware.mjs +1 -2
- package/dist/routes/conversion.d.mts +71 -15
- package/dist/routes/conversion.d.mts.map +1 -1
- package/dist/routes/conversion.mjs +79 -68
- package/dist/routes/conversion.mjs.map +1 -1
- package/dist/{tracking-M2PqlMpw.mjs → tracking-Cb3A0BrC.mjs} +2 -3
- package/dist/tracking-Cb3A0BrC.mjs.map +1 -0
- package/dist/{tracking-BNKKBqOe.d.mts → tracking-D2xOzjFD.d.mts} +5 -25
- package/dist/tracking-D2xOzjFD.d.mts.map +1 -0
- package/package.json +18 -6
- package/src/ads.ts +15 -22
- package/src/components/CookieConsent.astro +2 -1
- package/src/components/Entries.astro +58 -0
- package/src/components/Head.astro +6 -37
- package/src/components/ReviewCardBody.astro +16 -2
- package/src/components/SiteFooter.astro +79 -0
- package/src/components/SiteNav.astro +163 -0
- package/src/components/SiteNavList.astro +73 -0
- package/src/components/sitenav.ts +260 -0
- package/src/inline-scripts.ts +53 -0
- package/src/json-ld.ts +19 -0
- package/src/schemas/Article.astro +2 -1
- package/src/schemas/BookActions.astro +2 -1
- package/src/schemas/Breadcrumb.astro +2 -1
- package/src/schemas/Carousel.astro +2 -1
- package/src/schemas/CourseList.astro +2 -1
- package/src/schemas/Dataset.astro +2 -1
- package/src/schemas/DiscussionForum.astro +2 -1
- package/src/schemas/EducationQA.astro +2 -1
- package/src/schemas/EmployerAggregateRating.astro +2 -1
- package/src/schemas/Event.astro +2 -1
- package/src/schemas/FAQ.astro +2 -1
- package/src/schemas/FactCheck.astro +2 -1
- package/src/schemas/ImageMetadata.astro +2 -1
- package/src/schemas/JobPosting.astro +2 -1
- package/src/schemas/LocalBusiness.astro +2 -1
- package/src/schemas/MathSolver.astro +2 -1
- package/src/schemas/MovieCarousel.astro +2 -1
- package/src/schemas/Organization.astro +2 -1
- package/src/schemas/Product.astro +2 -1
- package/src/schemas/ProfilePage.astro +2 -1
- package/src/schemas/QAPage.astro +2 -1
- package/src/schemas/Recipe.astro +2 -1
- package/src/schemas/Review.astro +2 -1
- package/src/schemas/SoftwareApp.astro +2 -1
- package/src/seo.ts +6 -5
- package/src/tracking.ts +5 -9
- package/virtual.d.ts +17 -0
- package/dist/tracking-BNKKBqOe.d.mts.map +0 -1
- package/dist/tracking-M2PqlMpw.mjs.map +0 -1
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["SAFE_ID"],"sources":["../src/analytics.ts","../src/ads.ts","../src/fonts.ts","../src/i18n.ts","../src/integration.ts","../src/lastmod.ts","../src/seo.ts"],"sourcesContent":["/**\n * The `<Analytics />` resolution layer — provider config in, the exact tags to\n * emit out.\n *\n * Kept separate from `Analytics.astro` for the same reason `seo.ts` is separate\n * from `Seo.astro`: everything decidable is a pure function and asserted in\n * `analytics.test.ts`, and the component only renders what this returns.\n *\n * Two properties here are the point of the whole file:\n *\n * - **Which consent category a provider needs is derived, never declared.** It\n * follows from whether that provider sets cookies, which follows from its own\n * configuration (Matomo with `disableCookies` is genuinely cookieless; Matomo\n * with `cookies: true` is not). A consumer cannot mark Google Analytics\n * `necessary` to skip a banner, because `setsCookies` isn't theirs to set.\n * - **The same fact drives the legal documents.** `@getvitops/generator`'s\n * `legal/providers.ts` maps the *site config's* `analytics` block onto named\n * processors with cookie lists. If a provider is configured here but not\n * there, the site runs a tag its own cookie notice doesn't disclose — so the\n * integration warns, and `warnings` is returned as data so that check is a\n * test rather than a log line someone might scroll past.\n */\n\n/**\n * When a tag is allowed to load. Third-party analytics is never worth a\n * millisecond of the critical path, so the default puts every tag after `load`.\n */\nexport type AnalyticsStrategy = 'idle' | 'async' | 'interaction';\n\n/**\n * The consent vocabulary, mirroring `@getvitops/core`'s `consent/store.ts`.\n * Mirrored rather than imported because this value is `JSON.stringify`d into the\n * virtual module and the component reads it at build time, while core's copy is a\n * browser runtime — `consent.test.ts` and `analytics.test.ts` pin both ends.\n */\nexport type ConsentCategory = 'necessary' | 'analytics' | 'marketing' | 'preferences';\n\nexport type OptionalConsentCategory = Exclude<ConsentCategory, 'necessary'>;\n\nexport interface GoogleAnalyticsOptions {\n /** Measurement ID, e.g. `G-XXXXXXXXXX`. */\n id: string;\n /**\n * Which category gates it (default `analytics`). Set `marketing` when the\n * property feeds Google Ads — the tag is the same, the permission isn't.\n */\n category?: OptionalConsentCategory;\n /** Extra `gtag('config', id, …)` parameters, e.g. `{ send_page_view: false }`. */\n config?: Record<string, string | number | boolean>;\n}\n\nexport interface ClarityOptions {\n /** Clarity project ID. */\n id: string;\n category?: OptionalConsentCategory;\n}\n\nexport interface MatomoOptions {\n /** Instance base URL — self-hosted, or `https://<you>.matomo.cloud`. */\n url: string;\n /** Site ID within that instance. */\n siteId: string | number;\n /**\n * Let Matomo set its cookies (default **false**).\n *\n * The default is what makes Matomo the cookieless option: with\n * `disableCookies` it stores no identifier on the device, needs no banner, and\n * the generated cookie notice can state positively that it sets none. Turning\n * this on gives you returning-visitor metrics and a consent obligation.\n */\n cookies?: boolean;\n}\n\nexport interface PlausibleOptions {\n /** The domain as registered in Plausible, e.g. `example.com`. */\n domain: string;\n /** Script URL — override for a self-hosted instance or a custom-events build. */\n src?: string;\n}\n\nexport interface GetvitopsAnalyticsOptions {\n googleAnalytics?: string | GoogleAnalyticsOptions;\n clarity?: string | ClarityOptions;\n matomo?: MatomoOptions;\n plausible?: string | PlausibleOptions;\n /** When tags load (default `'idle'`). */\n strategy?: AnalyticsStrategy;\n}\n\n/**\n * The consent gate, configured on the integration rather than inside `analytics`.\n *\n * That placement is the design, not an accident: analytics is one consumer of\n * consent, not its owner. A/B assignment, account personalisation and third-party\n * embeds gate on the same choice via `data-consent`, and a site can enable the\n * banner with no analytics configured at all.\n */\nexport interface GetvitopsConsentOptions {\n /**\n * Categories the banner offers. Defaults to the ones the configured providers\n * actually need — offering a choice nothing is waiting on is noise.\n */\n categories?: OptionalConsentCategory[];\n /** Link the banner to the cookie notice (`vitops legal` renders one). */\n policyUrl?: string;\n}\n\n/**\n * A third-party tag, fully resolved — everything `<GatedTags />` needs to render\n * one, with nothing left to decide.\n *\n * Shared by `<Analytics />` and `<Ads />` on purpose: the inert-markup rule\n * (`type=\"text/plain\"`, the URL on `data-src`, never a live `src`) is the whole\n * consent gate, and it must have exactly one implementation. What differs between\n * the two is where the tag comes from and who decides its category, which is why\n * they have separate resolvers over one shape rather than one resolver over two\n * vocabularies.\n *\n * `key` is left to each resolver: analytics keys are its four providers, ads keys\n * are `AdProvider`. Widening it into one union here would let a provider from\n * either side satisfy a check meant for the other.\n */\nexport interface GatedTag {\n /** The provider as a reader would recognise it — used in warnings. */\n provider: string;\n category: ConsentCategory;\n setsCookies: boolean;\n /** Names (or `prefix*` patterns) to clear when consent is withdrawn. */\n cookies: string[];\n strategy: AnalyticsStrategy;\n /**\n * Emitted inert (`type=\"text/plain\"`) for the runtime to activate, rather than\n * as a live `<script>`. False only for the one case that needs no runtime at\n * all: a cookieless provider with `strategy: 'async'`.\n */\n gated: boolean;\n /** Bootstrap JS. Runs before `src`, so a queue is populated before it drains. */\n inline: string;\n src: string | null;\n attrs: Record<string, string>;\n}\n\n/** One analytics tag. */\nexport interface ResolvedTag extends GatedTag {\n key: 'googleAnalytics' | 'clarity' | 'matomo' | 'plausible';\n}\n\nexport interface ResolvedAnalytics {\n tags: ResolvedTag[];\n /** Does anything here need `@getvitops/core/consent` loaded? */\n needsRuntime: boolean;\n /** Configuration problems, as prose. The integration logs these. */\n warnings: string[];\n}\n\nexport interface ResolveAnalyticsContext {\n /** Is the consent gate enabled on this site (`vitops({ consent })`)? */\n consent?: boolean;\n /**\n * Enabled `site.experiments` entries, keyed the same way, each carrying the\n * resolved cookie name `vitopsAbTesting()`'s middleware assigns\n * (`_ab_<key>` unless overridden). When set, each provider's bootstrap\n * reads these cookies client-side and reports the assignment via that\n * provider's own custom-dimension/property mechanism — a no-op when this\n * is empty, so a site not using experiments sees no behavior change.\n */\n experiments?: Record<string, { cookieName: string }>;\n /** `site.analytics.matomo.abVariantDimensions` — Matomo's pre-created custom-dimension index per experiment key. */\n matomoAbVariantDimensions?: Record<string, number>;\n}\n\n/**\n * A small inline snippet reading each experiment's cookie into a plain\n * object, skipping any experiment whose cookie isn't set (visitor not yet\n * assigned, or the experiment disabled after this page was cached). Shared\n * across providers so the cookie-parsing logic — and its `SAFE_ID` guard on\n * both the experiment key and cookie name, the same discipline `bad()`\n * already applies to provider IDs landing in an inline `<script>` — exists\n * exactly once.\n */\nfunction experimentCookieReads(experiments: Record<string, { cookieName: string }>): string {\n const entries = Object.entries(experiments).filter(([key, e]) => !bad(key) && !bad(e.cookieName));\n if (!entries.length) return '';\n return (\n `function _abrc(n){var m=document.cookie.match(new RegExp('(?:^|; )'+n+'=([^;]*)'));return m?decodeURIComponent(m[1]):null;}` +\n `var _ab={};` +\n entries\n .map(([key, e]) => `_ab[${JSON.stringify(key)}]=_abrc(${JSON.stringify(e.cookieName)});`)\n .join('') +\n `for(var _k in _ab){if(_ab[_k]==null)delete _ab[_k];}`\n );\n}\n\n/**\n * IDs land inside an inline `<script>`, so they are checked rather than trusted.\n * A `</script>` in a measurement ID is not a plausible accident, but the cost of\n * the guard is one regex and the cost of not having it is arbitrary script\n * injection from a config file.\n */\nconst SAFE_ID = /^[\\w.-]+$/;\n\nfunction bad(value: string): boolean {\n return !SAFE_ID.test(value);\n}\n\n/** JSON is a subset of JS object-literal syntax; `<` can't appear unescaped. */\nfunction literal(value: Record<string, string | number | boolean>): string {\n return JSON.stringify(value).replaceAll('<', '\\\\u003c');\n}\n\nfunction googleAnalytics(\n input: string | GoogleAnalyticsOptions,\n experiments: Record<string, { cookieName: string }> = {},\n): ResolvedTag | string {\n const opts = typeof input === 'string' ? { id: input } : input;\n if (bad(opts.id)) return `googleAnalytics: \"${opts.id}\" is not a valid measurement ID. Skipped.`;\n const staticConfig = opts.config ?? {};\n const reads = experimentCookieReads(experiments);\n // With no experiments configured, the config object is static, exactly as\n // before — same output byte-for-byte, no runtime cost added. With\n // experiments, the automatic pageview's `gtag('config', …)` call is built\n // at runtime instead, merging each assigned variant in under its\n // experiment key so it reaches GA4 as a custom parameter on that same hit\n // (registered as a custom dimension in GA4 Admin by parameter name).\n const gtagConfig = reads\n ? `${reads}gtag('config','${opts.id}',Object.assign(${literal(staticConfig)},_ab));`\n : `gtag('config','${opts.id}'${opts.config ? `,${literal(staticConfig)}` : ''});`;\n return {\n key: 'googleAnalytics',\n provider: 'Google Analytics',\n category: opts.category ?? 'analytics',\n setsCookies: true,\n cookies: ['_ga', '_ga_*', '_gid', '_gat', '_gat_*'],\n strategy: 'idle',\n gated: true,\n inline:\n `window.dataLayer=window.dataLayer||[];` +\n `function gtag(){dataLayer.push(arguments)}` +\n `gtag('js',new Date());` +\n gtagConfig,\n src: `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(opts.id)}`,\n attrs: {},\n };\n}\n\nfunction clarity(input: string | ClarityOptions): ResolvedTag | string {\n const opts = typeof input === 'string' ? { id: input } : input;\n if (bad(opts.id)) return `clarity: \"${opts.id}\" is not a valid project ID. Skipped.`;\n const category = opts.category ?? 'analytics';\n // Clarity's snippet injects its own <script>, so there is no `src` for us to\n // schedule — activation *is* the load.\n //\n // The consentv2 call is not redundant with having gated the tag: Microsoft\n // enforces consent signals for EEA/UK/CH traffic from the project side, and\n // without one Clarity degrades to no-consent mode even though the visitor\n // agreed. `ad_Storage` follows the category, because that is the only thing\n // that tells us whether this tag was permitted for advertising.\n return {\n key: 'clarity',\n provider: 'Microsoft Clarity',\n category,\n setsCookies: true,\n cookies: ['_clck', '_clsk', 'MUID', 'CLID', 'ANONCHK', 'SM'],\n strategy: 'idle',\n gated: true,\n inline:\n `(function(c,l,a,r,i,t,y){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};` +\n `t=l.createElement(r);t.async=1;t.src=\"https://www.clarity.ms/tag/\"+i;` +\n `y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y)})` +\n `(window,document,\"clarity\",\"script\",\"${opts.id}\");` +\n `window.clarity('consentv2',{ad_Storage:'${\n category === 'marketing' ? 'granted' : 'denied'\n }',analytics_Storage:'granted'});`,\n src: null,\n attrs: {},\n };\n}\n\nfunction matomo(\n opts: MatomoOptions,\n experiments: Record<string, { cookieName: string }> = {},\n dimensions: Record<string, number> = {},\n): ResolvedTag | string {\n const siteId = String(opts.siteId);\n if (bad(siteId)) return `matomo: \"${siteId}\" is not a valid site ID. Skipped.`;\n let base: URL;\n try {\n base = new URL(opts.url);\n } catch {\n return `matomo: \"${opts.url}\" is not a valid URL. Skipped.`;\n }\n const url = `${base.origin}${base.pathname.replace(/\\/*$/, '')}/`;\n const cookies = opts.cookies === true;\n\n // Only experiments with a recorded dimension index push anything — one\n // with no index is silently skipped here (validateConfig's\n // `experimentDimensionWarnings` is what surfaces that as a build-time\n // warning, not a hard error: the pageview still ships correctly).\n const dimensionEntries = Object.entries(experiments).filter(([key]) => dimensions[key] != null);\n const reads = experimentCookieReads(Object.fromEntries(dimensionEntries));\n const dimensionPushes = reads\n ? reads +\n dimensionEntries\n .map(\n ([key]) =>\n `if(_ab[${JSON.stringify(key)}]!=null)_paq.push(['setCustomDimension',${dimensions[key]},_ab[${JSON.stringify(key)}]]);`,\n )\n .join('')\n : '';\n\n // `disableCookies` and the experiment dimension pushes must both land\n // *before* trackPageView — the former so the first request of the visit\n // doesn't set the cookies it exists to avoid, the latter because a\n // dimension pushed after trackPageView misses that pageview entirely.\n return {\n key: 'matomo',\n provider: 'Matomo',\n category: cookies ? 'analytics' : 'necessary',\n setsCookies: cookies,\n cookies: cookies ? ['_pk_id*', '_pk_ses*', '_pk_ref*', '_pk_cvar*', '_pk_hsr*'] : [],\n strategy: 'idle',\n gated: true,\n inline:\n `var _paq=window._paq=window._paq||[];` +\n (cookies ? '' : `_paq.push(['disableCookies']);`) +\n dimensionPushes +\n `_paq.push(['trackPageView']);_paq.push(['enableLinkTracking']);` +\n `(function(){var u=\"${url}\";` +\n `_paq.push(['setTrackerUrl',u+'matomo.php']);_paq.push(['setSiteId','${siteId}']);` +\n `var d=document,g=d.createElement('script'),s=d.getElementsByTagName('script')[0];` +\n `g.async=true;g.src=u+'matomo.js';s.parentNode.insertBefore(g,s)})();`,\n src: null,\n attrs: {},\n };\n}\n\nfunction plausible(\n input: string | PlausibleOptions,\n experiments: Record<string, { cookieName: string }> = {},\n): ResolvedTag | string {\n const opts = typeof input === 'string' ? { domain: input } : input;\n if (bad(opts.domain)) return `plausible: \"${opts.domain}\" is not a valid domain. Skipped.`;\n // Plausible's current API (plausible.init({customProperties})) needs no\n // special script variant and no manual-pageview mode — customProperties\n // attaches to every tracked event, including the automatic pageview, so\n // there's no double-counting risk to design around. The queueing stub\n // mirrors gtag's own inline bootstrap above: the real script (loaded via\n // `src`, async) picks up this call once it's ready, regardless of which\n // one finishes loading first.\n const reads = experimentCookieReads(experiments);\n const inline = reads\n ? `window.plausible=window.plausible||function(){(window.plausible.q=window.plausible.q||[]).push(arguments)};` +\n `plausible.init({customProperties:function(){${reads}return _ab;}});`\n : '';\n return {\n key: 'plausible',\n provider: 'Plausible Analytics',\n // Cookieless and storing no cross-site identifier, so there is nothing for a\n // banner to ask about. It still goes through the runtime for scheduling.\n category: 'necessary',\n setsCookies: false,\n cookies: [],\n strategy: 'idle',\n gated: true,\n inline,\n src: opts.src ?? 'https://plausible.io/js/script.js',\n attrs: { 'data-domain': opts.domain },\n };\n}\n\n/**\n * Resolve the configured providers into the tags to emit.\n *\n * An invalid provider is dropped with a warning rather than throwing: one\n * malformed ID should not take down a build, and a missing tag is visible in the\n * output while a failed build is just a red wall.\n */\nexport function resolveAnalytics(\n opts: GetvitopsAnalyticsOptions | undefined,\n ctx: ResolveAnalyticsContext = {},\n): ResolvedAnalytics {\n const warnings: string[] = [];\n const tags: ResolvedTag[] = [];\n if (!opts) return { tags, needsRuntime: false, warnings };\n\n const strategy = opts.strategy ?? 'idle';\n const experiments = ctx.experiments ?? {};\n const results = [\n opts.googleAnalytics === undefined ? null : googleAnalytics(opts.googleAnalytics, experiments),\n opts.clarity === undefined ? null : clarity(opts.clarity),\n opts.matomo === undefined\n ? null\n : matomo(opts.matomo, experiments, ctx.matomoAbVariantDimensions ?? {}),\n opts.plausible === undefined ? null : plausible(opts.plausible, experiments),\n ];\n\n for (const result of results) {\n if (result === null) continue;\n if (typeof result === 'string') {\n warnings.push(result);\n continue;\n }\n // A cookieless tag on `async` needs nothing from the runtime, so it is emitted\n // as an ordinary <script async> and that site ships no consent bundle at all.\n const gated = result.category !== 'necessary' || strategy !== 'async';\n tags.push({ ...result, strategy, gated });\n }\n\n const setting = tags.filter((t) => t.setsCookies);\n if (setting.length && !ctx.consent) {\n warnings.push(\n `${setting.map((t) => t.provider).join(' and ')} ${setting.length === 1 ? 'sets' : 'set'} ` +\n `cookies (${setting.flatMap((t) => t.cookies).join(', ')}) and \\`consent\\` is off, so ` +\n `${setting.length === 1 ? 'it loads' : 'they load'} for every visitor with no way to ` +\n 'decline. Set `consent: true`, or switch to a cookieless provider (Plausible, or Matomo ' +\n 'with its default `cookies: false`).',\n );\n }\n\n return { tags, needsRuntime: tags.some((t) => t.gated), warnings };\n}\n\n/** Which optional categories a set of tags actually needs a visitor to decide. */\nexport function consentCategories(tags: GatedTag[]): OptionalConsentCategory[] {\n const used = new Set<OptionalConsentCategory>();\n for (const tag of tags) {\n if (tag.category !== 'necessary') used.add(tag.category);\n }\n return (['analytics', 'marketing', 'preferences'] as const).filter((c) => used.has(c));\n}\n","/**\n * The `<Ads />` resolution layer — the site's ad properties in, the exact tags to\n * emit out.\n *\n * A sibling of `analytics.ts`, deliberately not part of it. The two render the\n * same gated markup, but they answer different questions and four things differ:\n *\n * - **Where they come from.** Analytics providers are integration options\n * (`vitops({ analytics })`); ad properties are `site.ads` in the site config,\n * because the ad account is a fact about the site that `vitops ads setup`,\n * `vitops ads lint` and the generated cookie notice all read too.\n * - **Who decides the consent category.** `resolveAnalytics` *derives* it from\n * whether the provider sets cookies, so a consumer cannot mark Google Analytics\n * `necessary` to dodge a banner. Every ad pixel sets cookies and is advertising,\n * so the answer is `marketing` unless the config says `analytics` — a stated\n * fact, not a derivation.\n * - **What the key means.** `ResolvedTag['key']` is the four analytics providers,\n * and `analytics.ts` pairs that set against the generator's processor table.\n * Widening it to eight ad platforms would make that check answer for tags it\n * does not govern.\n * - **When they fire.** Analytics is gated per environment by\n * `environments.<env>.analytics`; a preview deployment sending pageviews is\n * survivable, one firing conversion pixels is not, so ads gets its own switch.\n *\n * Everything decidable is here and unit-tested; `Ads.astro` renders what this\n * returns and chooses nothing.\n */\nimport {\n AD_PLATFORMS,\n AD_PROVIDER_KEYS,\n normalizeConversionLabels,\n type AdProvider,\n type ConversionLabelMap,\n} from '@getvitops/utils/ads';\nimport type { AnalyticsStrategy, ConsentCategory, GatedTag } from './analytics.ts';\n\n/**\n * One ad property, mirroring a `site.ads` entry field for field.\n *\n * Structural rather than `SiteAdProperty` imported from the generator, matching\n * how `GetvitopsSeoOptions` and `GetvitopsTrackingOptions` already work: the\n * component surface must stay usable by a consumer who passes a plain object.\n */\nexport interface GetvitopsAdProperty {\n accountId?: string;\n pixelId?: string;\n /**\n * Google Ads only: the conversion action label(s) paired with `pixelId` (the\n * `AW-…` conversion id) — a bare string (back-compat, treated as `call`) or a\n * map of action → label. `<Tracking />`'s tel:/mailto: click handler and\n * `vitopsFormConversion()` each fire `gtag('event', 'conversion', …)` for\n * their own action at the moment of the interaction — see `gtagBootstrap` in\n * `@getvitops/utils/ads`.\n */\n conversionLabel?: string | ConversionLabelMap;\n /** Default `marketing`. `analytics` for a property used purely for measurement. */\n category?: 'marketing' | 'analytics';\n /** False keeps the property on record without emitting its tag. */\n enabled?: boolean;\n}\n\nexport type GetvitopsAdsOptions = Partial<Record<AdProvider, GetvitopsAdProperty>>;\n\n/** One ad tag. `key` is the platform, so a caller can find one without matching prose. */\nexport interface ResolvedAdTag extends GatedTag {\n key: AdProvider;\n}\n\nexport interface ResolvedAds {\n tags: ResolvedAdTag[];\n /** Does anything here need `@getvitops/core/consent` loaded? */\n needsRuntime: boolean;\n /** Configuration problems, as prose. The integration logs these. */\n warnings: string[];\n}\n\nexport interface ResolveAdsContext {\n /** Is the consent gate enabled on this site (`vitops({ consent })`)? */\n consent?: boolean;\n /** Categories the banner offers, so we can catch a demand with no row. */\n consentCategories?: readonly string[];\n /** `environments.<env>.ads` — false switches every tag off for this build. */\n enabled?: boolean;\n /** When the tags load. Default `idle`: a pixel is never worth the critical path. */\n strategy?: AnalyticsStrategy;\n}\n\n/**\n * IDs land inside an inline `<script>`, so they are checked rather than trusted —\n * the same guard, for the same reason, as `analytics.ts`'s `SAFE_ID`. A `</script>`\n * in a pixel ID is not a plausible accident; arbitrary script injection from a\n * config file is not a plausible thing to allow either.\n */\nconst SAFE_ID = /^[\\w.-]+$/;\n\n/** Resolve the configured ad properties into the tags `<Ads />` emits. */\nexport function resolveAds(\n ads: GetvitopsAdsOptions | undefined,\n ctx: ResolveAdsContext = {},\n): ResolvedAds {\n const warnings: string[] = [];\n const tags: ResolvedAdTag[] = [];\n const strategy = ctx.strategy ?? 'idle';\n\n const configured = AD_PROVIDER_KEYS.filter((p) => ads?.[p] != null);\n if (configured.length === 0) return { tags, needsRuntime: false, warnings };\n\n // A whole-environment switch, checked before anything else: a preview deploy\n // must be able to carry the same config without firing a single pixel.\n if (ctx.enabled === false) return { tags, needsRuntime: false, warnings };\n\n for (const provider of configured) {\n const entry = ads![provider]!;\n if (entry.enabled === false) continue;\n\n const platform = AD_PLATFORMS[provider];\n const id = entry[platform.tag.needs];\n if (!id) {\n warnings.push(\n `ads: ${provider} has no ${platform.tag.needs}, so no tag is emitted. ` +\n `Run \\`vitops ads setup\\` to be prompted for it.`,\n );\n continue;\n }\n if (!SAFE_ID.test(id)) {\n warnings.push(`ads: ${provider} ${platform.tag.needs} \"${id}\" is not a valid ID. Skipped.`);\n continue;\n }\n\n // `conversionLabel` lands inside the same inline `<script>` as `id` (Google's\n // bootstrap embeds it to expose a conversion `send_to` target per action) —\n // same injection surface, so every configured label gets the same check.\n // Unlike an invalid `id`, an invalid label doesn't sink the whole tag: it\n // just means that one action's conversion doesn't fire, which is worth a\n // warning rather than dropping every other configured action too.\n let safeEntry = entry;\n const labels = normalizeConversionLabels(entry.conversionLabel);\n const invalidActions = Object.entries(labels)\n .filter(([, label]) => label && !SAFE_ID.test(label))\n .map(([action]) => action);\n if (invalidActions.length) {\n for (const action of invalidActions) {\n warnings.push(\n `ads: ${provider} conversionLabel.${action} \"${labels[action as keyof ConversionLabelMap]}\" ` +\n 'is not a valid label. Conversion firing skipped for that action.',\n );\n }\n const cleanLabels = Object.fromEntries(\n Object.entries(labels).filter(([action]) => !invalidActions.includes(action)),\n );\n // Omit the key entirely rather than setting it `undefined` —\n // `exactOptionalPropertyTypes` treats the two differently.\n const { conversionLabel: _drop, ...rest } = entry;\n safeEntry = Object.keys(cleanLabels).length\n ? { ...rest, conversionLabel: cleanLabels }\n : rest;\n }\n\n const category: ConsentCategory = entry.category ?? 'marketing';\n tags.push({\n key: provider,\n provider: platform.name,\n category,\n // Every platform in the table sets cookies; the tag is gated whenever the\n // site has a gate. With no gate the tag still renders — and the warning\n // below says what that means, rather than silently declining to advertise.\n setsCookies: true,\n cookies: platform.tag.cookies,\n strategy,\n gated: ctx.consent === true,\n inline: platform.tag.bootstrap(id, { provider, ...safeEntry }),\n src: platform.tag.src ? platform.tag.src(id) : null,\n attrs: {},\n });\n }\n\n if (tags.length === 0) return { tags, needsRuntime: false, warnings };\n\n if (ctx.consent !== true) {\n warnings.push(\n `ads: ${tags.map((t) => t.provider).join(', ')} ${tags.length === 1 ? 'sets' : 'set'} ` +\n 'advertising cookies, and `consent` is off — so they load for every visitor with no way ' +\n 'to decline. Set `consent: true`.',\n );\n } else {\n const missing = [...new Set(tags.map((t) => t.category))].filter(\n (c) => ctx.consentCategories && !ctx.consentCategories.includes(c),\n );\n if (missing.length)\n warnings.push(\n `ads: the tags demand the ${missing.join(', ')} consent categor${missing.length === 1 ? 'y' : 'ies'}, ` +\n `but the banner offers ${ctx.consentCategories?.join(', ') || 'nothing'}. Add ` +\n `${missing.join(', ')} to \\`consent.categories\\`, or the tags wait on a question the ` +\n 'banner never asks.',\n );\n }\n\n return { tags, needsRuntime: true, warnings };\n}\n","/**\n * Font LOADING — turning declarations into Astro's `fonts:` config.\n *\n * The design system NAMES fonts (`design-system.json`'s `fonts` block → `--font-<name>`\n * tokens) and loads none of them: a stack is a `font-family` list, with no `@font-face`,\n * no preload, and no metrics-matched fallback behind it. Something has to load them, and\n * the failure mode when nothing does is invisible — installing a `@fontsource*` package\n * and importing its CSS renders correctly while silently giving up subsetting, preload\n * and the `size-adjust`/`ascent-override` metrics that keep CLS down.\n *\n * So this module is the seam, in the same spirit as `vitopsHosting()`: declare the family\n * once and let the integration wire it, rather than hand-rolling the config.\n *\n * The declaration shape is `SiteFont` from the generator — deliberately not a new type.\n * The site config has carried a `fonts` array (provider / weights / subsets / preload)\n * since it was written; what was missing was anything that read it.\n */\nimport type { SiteFont } from '@getvitops/generator';\nimport { fontProviders } from 'astro/config';\n\n/**\n * Provider names, matching `fontProviders`' keys 1:1 (verified against astro@7.1.2).\n * The generator's `SiteFontSchema` enumerates the same set, so a mismatch is a\n * compile-time-invisible drift — `resolveFonts` throws on an unknown name rather than\n * quietly dropping the family and leaving the token resolving to a fallback stack.\n */\nconst PROVIDERS = new Set(Object.keys(fontProviders));\n\n/**\n * Providers a declaration can construct with no arguments.\n *\n * `adobe` is the one that can't: `fontProviders.adobe({ id })` takes a project API id,\n * and the point of that id being an argument is that it comes from the environment. A\n * JSON declaration has nowhere to put `process.env.ADOBE_ID` without either inlining the\n * key into a committed file or inventing an interpolation syntax, so Adobe families are\n * declared in `astro.config` directly — see the error thrown below.\n */\nconst NEEDS_OPTIONS = new Set(['adobe']);\n\n/**\n * What `<Head />` needs to emit `<Font />` for each family the integration registered.\n *\n * `preload` is passed to the component verbatim: Astro's `PreloadFilter` is\n * `boolean | Array<{weight?, style?, subset?}>`, and the declaration's per-face form is a\n * subset of that — so \"preload the 700 upright only\" survives instead of flattening to\n * \"preload everything\", which would preload the whole family.\n */\nexport interface HeadFont {\n cssVariable: string;\n preload: NonNullable<SiteFont['preload']> | false;\n}\n\nexport interface ResolvedFonts {\n /** Spread into `updateConfig({ fonts })`. Astro's own `FontFamily` shape. */\n fonts: Record<string, unknown>[];\n /** Passed through the virtual head module so `<Head />` can render `<Font />`. */\n head: HeadFont[];\n}\n\n/**\n * Map declarations onto Astro's `fonts:` entries.\n *\n * `preload` is dropped from the Astro entry on purpose — it is not part of the config\n * schema there, it is a prop on the `<Font />` component. It rides along in `head`\n * instead, which is what makes preload a property of the declaration rather than\n * something the consumer hand-picks in their layout.\n */\nexport function resolveFonts(decls: readonly SiteFont[]): ResolvedFonts {\n const fonts: Record<string, unknown>[] = [];\n const head: HeadFont[] = [];\n const seen = new Set<string>();\n\n for (const decl of decls) {\n if (!PROVIDERS.has(decl.provider))\n throw new Error(\n `[vitops] fonts: unknown provider \"${decl.provider}\" for \"${decl.name}\". ` +\n `Astro provides: ${[...PROVIDERS].sort().join(', ')}.`,\n );\n if (NEEDS_OPTIONS.has(decl.provider))\n throw new Error(\n `[vitops] fonts: provider \"${decl.provider}\" needs an API id from your environment ` +\n `(\"${decl.name}\"), and a JSON declaration has nowhere to hold one. Declare this family ` +\n `in astro.config instead — fonts: [{ provider: fontProviders.${decl.provider}` +\n `({ id: process.env.ADOBE_ID }), name: '${decl.name}', cssVariable: '${decl.cssVariable}' }] — ` +\n 'and render its <Font /> yourself; the design-system token can still point at it.',\n );\n if (!decl.cssVariable.startsWith('--'))\n throw new Error(\n `[vitops] fonts: cssVariable \"${decl.cssVariable}\" (${decl.name}) must start with \"--\".`,\n );\n // Two families on one variable is the documented failure of Astro's Fonts API:\n // it warns and the last one wins, so the first family silently never loads.\n if (seen.has(decl.cssVariable))\n throw new Error(\n `[vitops] fonts: two families both declare ${decl.cssVariable} ` +\n `(the second is \"${decl.name}\"). Astro resolves one family per variable — ` +\n `give them separate variables, or drop one.`,\n );\n seen.add(decl.cssVariable);\n\n const { provider, preload, ...rest } = decl;\n fonts.push({\n ...rest,\n provider: (fontProviders as unknown as Record<string, () => unknown>)[provider]!(),\n });\n head.push({ cssVariable: decl.cssVariable, preload: preload ?? false });\n }\n\n return { fonts, head };\n}\n","/**\n * Reconciles `site.locales`/`site.defaultLocale` against Astro's own `i18n`\n * config block.\n *\n * The two are a second, currently-disconnected locale list — EmDash reads\n * locales straight from `astro.config.mjs`'s `i18n` block (see AGENTS.md's\n * \"i18n is Astro-config-driven, not `Config`-driven\" note), while our own\n * SEO/legal code reads `site.locales`/`defaultLocale`. Nothing here merges\n * them into one source: `generate()`/`resolveConfig()` stay pure and\n * filesystem-only (see AGENTS.md's \"Config records facts\" framing — teaching\n * the generator to parse a consumer's `astro.config.mjs` would cost that),\n * so this only says when the two disagree, at the one layer that already\n * legitimately reads both — the Astro integration's `astro:config:setup`\n * hook, which has Astro's resolved `config.i18n` in hand alongside whatever\n * `site.json` `readSiteAds`/`readSiteConsentTracking` beside it already read.\n *\n * A **warning**, not the hard error `consentTrackingConflicts` throws for a\n * consent contradiction: plenty of legitimate sites configure `site.locales`\n * (for `<Seo>`/legal prose) with no Astro i18n routing at all, and the\n * reverse — i18n routing with no `site`/`css` option wired up here — is\n * equally legitimate. Either absence is silence, not a mismatch.\n */\n\n/** Astro's own `i18n` config shape, narrowed to what this needs. */\nexport interface AstroI18nLike {\n defaultLocale: string;\n locales: (string | { path: string; codes: string[] })[];\n}\n\nfunction codesOf(entry: string | { path: string; codes: string[] }): string[] {\n return typeof entry === 'string' ? [entry] : entry.codes;\n}\n\n/** Every BCP-47 code Astro's `i18n` block actually recognises. */\nexport function astroLocaleCodes(i18n: AstroI18nLike): Set<string> {\n return new Set(i18n.locales.flatMap(codesOf));\n}\n\n/**\n * Warning strings for a mismatch — `[]` when either side is absent (nothing\n * to reconcile) or the two agree.\n */\nexport function localeConsistencyWarnings(\n site: { locales: Record<string, unknown>; defaultLocale: string } | null | undefined,\n i18n: AstroI18nLike | undefined,\n): string[] {\n if (!site || !i18n) return [];\n const warnings: string[] = [];\n const astroCodes = astroLocaleCodes(i18n);\n const siteCodes = new Set(Object.keys(site.locales));\n const onlyInSite = [...siteCodes].filter((c) => !astroCodes.has(c));\n const onlyInAstro = [...astroCodes].filter((c) => !siteCodes.has(c));\n\n if (onlyInSite.length || onlyInAstro.length)\n warnings.push(\n `i18n: site.locales and astro.config.mjs's i18n.locales disagree` +\n (onlyInSite.length\n ? ` — site.locales has ${onlyInSite.map((c) => `\"${c}\"`).join(', ')} that astro's i18n doesn't`\n : '') +\n (onlyInAstro.length\n ? `${onlyInSite.length ? ', and' : ' —'} astro's i18n has ${onlyInAstro.map((c) => `\"${c}\"`).join(', ')} that site.locales doesn't`\n : '') +\n `. EmDash reads locales from astro.config.mjs directly, so a locale missing there never ` +\n `reaches its admin regardless of what site.locales says.`,\n );\n if (i18n.defaultLocale !== site.defaultLocale)\n warnings.push(\n `i18n: astro.config.mjs's i18n.defaultLocale (\"${i18n.defaultLocale}\") does not match ` +\n `site.defaultLocale (\"${site.defaultLocale}\") — SEO/legal prose renders in one locale by ` +\n `default while EmDash's own routing defaults to the other.`,\n );\n return warnings;\n}\n","/**\n * @getvitops/astro — the Astro integration. One entry wires the design system's\n * `<head>` contributions into a consumer site:\n * - favicons + PWA manifest (generated into public/),\n * - the web-component runtime bundles (copied into public/vitops/),\n * - (opt-in) the design-system CSS, generated + auto-injected (no manual import).\n * The `<Head />` component (shipped alongside) renders the tags, reading resolved\n * config from the `virtual:getvitops/head` module this integration provides.\n */\nimport { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, join, relative, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { LegalOutput, SiteFont, StylesheetFormat } from '@getvitops/generator';\nimport { isConfig, renderRobotsTxt, resolveConfig } from '@getvitops/generator';\nimport {\n type FaviconLink,\n collectIconRefs,\n faviconLinks,\n generateFavicons,\n generateIconInclude,\n resolveIcon,\n scanFiles,\n writeFaviconManifest,\n} from '@getvitops/utils';\n// Aliased: this module's own default export is `vitops` (the integration).\nimport vitopsPlugin from '@getvitops/vite';\n// Type only — the encoder itself is loaded by the Vite plugin, in the pass that\n// runs it, so a project without a `media` option never reaches ffmpeg.\nimport type { OutputKind as MediaOutputKind } from '@getvitops/utils/media';\nimport type { AstroIntegration } from 'astro';\nimport {\n consentCategories,\n type GetvitopsAnalyticsOptions,\n type GetvitopsConsentOptions,\n type OptionalConsentCategory,\n resolveAnalytics,\n} from './analytics.ts';\nimport { type GetvitopsAdsOptions, resolveAds } from './ads.ts';\nimport { type HeadFont, resolveFonts } from './fonts.ts';\nimport { localeConsistencyWarnings } from './i18n.ts';\nimport type { GetvitopsSeoOptions } from './seo.ts';\nimport { TRACKING_ENDPOINT, type GetvitopsTrackingOptions, resolveTracking } from './tracking.ts';\n\nexport interface GetvitopsFaviconOptions {\n /** Source SVG or PNG. */\n source: string;\n lowResSource?: string;\n /** App name → enables `site.webmanifest`. */\n name?: string;\n /** PWA theme color (also `<meta name=\"theme-color\">`). */\n themeColor?: string;\n backgroundColor?: string;\n}\n\n/**\n * Video encoding. A sibling of `favicon`, not a widening of `css`.\n *\n * Mirrors `VitopsMediaOptions` in `@getvitops/vite`, which is where it runs — see\n * the `media` field on `GetvitopsOptions` for why.\n */\nexport interface GetvitopsMediaOptions {\n /** Directory of unprocessed video, walked recursively. Default: `'raw'`. */\n raw?: string;\n /** Where encoded outputs go. Default: `'src/assets/processed'`. */\n out?: string;\n /** Cap the output width, keeping aspect ratio. Default 1920; 0 disables. */\n maxWidth?: number;\n /** Quality on VP9's CRF scale (0–63, lower is better). Default 32. */\n crf?: number;\n /** Optional bitrate ceiling, e.g. `'2M'`. Absent means constant quality. */\n maxBitrate?: string;\n /** Keep the audio track. Default false — the common case is a muted loop. */\n audio?: boolean;\n /** Timestamp (seconds) the poster frame comes from. Default 0. */\n posterTime?: number;\n /** Which outputs to produce. Default: webm, mp4 and a poster. */\n outputs?: MediaOutputKind[];\n /** Cache file. Default `.vitops/media-manifest.json`. */\n manifest?: string;\n}\n\nexport interface GetvitopsCssOptions {\n /**\n * Path to the config (default `'design-system.json'`).\n *\n * May be a `design-system.json` **or** the larger site config that embeds one\n * (`company.json` / `site.json`) — they are told apart by shape. Pointing this\n * at a site config makes the top-level `site` option redundant and lets\n * `legal` and `fonts` default their own `input` to it, so the path is declared\n * once.\n */\n input?: string;\n /**\n * Which `designSystem.themes` entry to build, when `input` is a site config.\n * Default: the config's `defaultTheme`, else `default`.\n */\n theme?: string;\n /**\n * Output format (default 'tailwind'). Narrower than the generator's `Format`\n * on purpose: this option exists to produce a stylesheet to inject, and the\n * `design` format emits only `DESIGN.md`. Run that one via the CLI\n * (`vitops generate --format design --out .`).\n */\n format?: StylesheetFormat;\n /** Directory the generated CSS is written to (default 'src/styles'). */\n out?: string;\n /**\n * Auto-inject the generated stylesheet into every SSR page (default true).\n * Set false when other integrations add routes that must not inherit the\n * design system (e.g. EmDash's `/_emdash/admin`) — then import the generated\n * file (`<out>/tailwind.css` or `<out>/styles.css`) from your site layout so\n * only your own pages (and previews rendered through them) are styled.\n */\n inject?: boolean;\n /**\n * Also flip to dark when the visitor's OS asks and they have made no explicit\n * choice — `@media (prefers-color-scheme: dark)`.\n *\n * Off by default: turning it on flips the site dark for dark-OS visitors, which\n * is a visible change to an existing site. Turning it **on** is what makes\n * `<wc-color-scheme-toggle>`'s \"System\" position do anything (System removes the\n * theme attribute, so without this block it falls through to light), and it is\n * the only way a no-JS page gets the OS appearance at all.\n *\n * The site config's `designSystem.defaultColorScheme: \"system\"` says the same thing and is the\n * better home for it when you have one — this option wins if both are set, and\n * exists because requiring a whole `Config` to set one boolean would be out\n * of proportion.\n */\n systemColorScheme?: boolean;\n}\n\nexport interface GetvitopsSiteOptions {\n /** Path to the site config (JSON). */\n input: string;\n /** Environment whose A/B variant applies (default 'production'). */\n siteEnv?: string;\n}\n\nexport interface GetvitopsLegalOptions {\n /**\n * Path to the site config (JSON) the documents are rendered from. Optional\n * when the top-level `site` option is set — it defaults to that file, since\n * they are almost always the same one.\n */\n input?: string;\n /**\n * Where to write them (default 'src/content/legal'). The default is a content\n * collection: the documents are markdown, and a collection is what lets a page\n * render one through your own layout rather than the integration injecting a\n * route (which nothing in this package does).\n */\n out?: string;\n /** Output format (default 'md'). */\n format?: LegalOutput;\n /** Environment whose A/B variant applies (default 'production'). */\n siteEnv?: string;\n}\n\n/** `changefreq`, mirroring @astrojs/sitemap's `ChangeFreq`. */\nexport type GetvitopsSitemapChangeFreq =\n | 'always'\n | 'hourly'\n | 'daily'\n | 'weekly'\n | 'monthly'\n | 'yearly'\n | 'never';\n\n/** One sitemap entry, mirroring @astrojs/sitemap's `SitemapItem`. */\nexport interface GetvitopsSitemapEntry {\n url: string;\n lastmod?: string;\n changefreq?: GetvitopsSitemapChangeFreq;\n priority?: number;\n /** Alternate-language variants. `lang` is required, as upstream requires it. */\n links?: { lang: string; hreflang?: string; url: string }[];\n}\n\n/**\n * Forwarded verbatim to `@astrojs/sitemap`. Deliberately a hand-written subset of\n * its `SitemapOptions` rather than a re-export: aliasing would put an\n * `import … from '@astrojs/sitemap'` — and, transitively via `SitemapItem`, from\n * `sitemap` — into this package's published .d.mts, which cannot resolve for the\n * consumers who don't install the optional peer. Worse, `skipLibCheck` (on by\n * default via astro's base tsconfig) *suppresses* that error, silently degrading\n * the option to `any` instead of failing loudly.\n *\n * Need something not listed here (e.g. `chunks`)? Add `sitemap()` to your own\n * `integrations` array — getvitops detects it and leaves yours in charge.\n */\nexport interface GetvitopsSitemapOptions {\n /** Keep a page out of the sitemap. Receives the page's full absolute URL. */\n filter?: (page: string) => boolean;\n /** Absolute URLs of pages Astro doesn't build but the site serves. */\n customPages?: string[];\n /** Absolute URLs of externally-generated sitemaps to list in the index. */\n customSitemaps?: string[];\n /** Max entries per file before it splits (default 45000). */\n entryLimit?: number;\n /** Filename prefix (default 'sitemap' → `sitemap-index.xml`). */\n filenameBase?: string;\n changefreq?: GetvitopsSitemapChangeFreq;\n lastmod?: Date;\n priority?: number;\n /** Per-entry rewrite; return `undefined` to drop the entry. */\n serialize?: (\n item: GetvitopsSitemapEntry,\n ) => GetvitopsSitemapEntry | Promise<GetvitopsSitemapEntry | undefined> | undefined;\n /** Emit `<xhtml:link rel=\"alternate\">` for localised routes. */\n i18n?: { defaultLocale: string; locales: Record<string, string> };\n /** XSL stylesheet to prettify the XML (path relative to `site`, or absolute URL). */\n xslURL?: string;\n /** XML namespaces to exclude (all included by default). */\n namespaces?: { news?: boolean; xhtml?: boolean; image?: boolean; video?: boolean };\n}\n\nexport interface GetvitopsOptions {\n /**\n * Your site config — the one place to name it.\n *\n * A `Config` records site-level facts, several of which other options here\n * need: `designSystem.defaultColorScheme` decides whether the generated colour layer\n * carries a `prefers-color-scheme` block, `fonts` can supply the webfont\n * declarations, and `legal` renders from it. Set this and each of those reads\n * it, rather than repeating the path per feature. Every one can still be given\n * explicitly, which wins.\n *\n * Needs `css`, since the generation runs in the Vite plugin `css` registers.\n */\n site?: GetvitopsSiteOptions;\n favicon?: GetvitopsFaviconOptions;\n /**\n * Encode raw video into WebM + an MP4 fallback + a poster frame.\n *\n * Import the results like any other asset, so Vite content-hashes them:\n *\n * ```astro\n * import hero from '../assets/processed/hero.webm';\n * import poster from '../assets/processed/hero.jpg';\n * <video poster={poster.src} autoplay muted loop playsinline>\n * <source src={hero} type=\"video/webm\" />\n * </video>\n * ```\n *\n * Cached on source content and encode settings, so a rebuild costs a hash per\n * file rather than an encode. **Commit the outputs and\n * `.vitops/media-manifest.json`** — ffmpeg output is not reproducible across\n * versions, so re-encoding in CI churns the diff on every toolchain bump.\n *\n * Needs `ffmpeg` on PATH, and fails the build without it rather than skipping:\n * a page referencing a video that was never encoded is broken, not degraded.\n *\n * Needs `css`, since it runs in the Vite plugin `css` registers.\n */\n media?: GetvitopsMediaOptions;\n /** Copy + link the web-component bundles (default true). */\n webComponents?: boolean;\n /** Generate + auto-inject the design-system CSS. Off unless provided. */\n css?: GetvitopsCssOptions;\n /**\n * Ship the live theme editor (default false).\n *\n * Copies `editor.js` into `public/vitops/`, loads it from `<Head />`, and mirrors\n * the generated `design-manifest.json` next to it so `<wc-theme-editor>` resolves\n * its default `/vitops/design-manifest.json`. Requires `css` (the manifest is a\n * `css`-format output) — place the `<wc-theme-editor>` tag in your layout yourself.\n *\n * Editing is live on any deploy; **saving back** to `design-system.json` needs the\n * dev server, so on a static build that button simply isn't rendered.\n */\n editor?: boolean;\n /**\n * Render the site's legal documents (privacy policy, terms, cookie notice)\n * from a site config, and re-render when it changes. Off unless provided.\n *\n * Requires `css`, which is what registers the Vite plugin that runs the\n * generation — without it the documents would be written once and never\n * refresh. Not using `css`? Run `vitops legal` from the CLI instead; it is the\n * same renderer and works in any stack.\n */\n legal?: GetvitopsLegalOptions;\n /**\n * Load webfonts through Astro's Fonts API and emit `<Font />` for each from\n * `<Head />`. Off unless provided.\n *\n * This is the seam the design system deliberately lacks. `design-system.json`'s\n * `fonts` block holds **stacks only** — `--font-<name>` tokens and nothing else: no\n * `@font-face`, no preload, no metrics-matched fallback. Declaring the family here\n * makes the token resolvable *and* loaded, so the token keeps pointing at a\n * `cssVariable` rather than at a literal stack the browser has no file for:\n *\n * ```js\n * vitops({ fonts: [{ name: 'League Spartan', provider: 'fontsource',\n * cssVariable: '--font-league-spartan',\n * weights: ['100 900'], subsets: ['latin'], preload: true }] })\n * ```\n * ```jsonc\n * // design-system.json\n * \"fonts\": { \"display\": \"var(--font-league-spartan), sans-serif\" }\n * ```\n *\n * Pass a **string** instead to read the array from a site config's `fonts` block —\n * the same declarations, kept next to the rest of the site's facts. (`siteEnv`\n * picks the A/B variant, as in `legal`.) With the top-level `site` option set,\n * plain `true` does the same thing without repeating the path.\n *\n * Independent of `css`: the loading runs in `astro:config:setup`, not in the Vite\n * plugin. Both `<Head />` and the config entry are required — Astro's `fonts:`\n * config resolves the files, and `<Font />` is what puts the `@font-face` on the\n * page — so a declaration without `<Head />` in your layout loads nothing.\n */\n fonts?: boolean | string | SiteFont[] | GetvitopsFontsOptions;\n /**\n * Emit `sitemap-index.xml` + `sitemap-0.xml` via `@astrojs/sitemap`, and link it\n * from `<Head />`. Off unless provided; `true` uses its defaults.\n *\n * Needs `@astrojs/sitemap` (an optional peer — install it yourself) and the\n * `site` astro.config option, since a sitemap lists absolute URLs.\n *\n * Skipped with a warning when `emdash()` is registered — EmDash serves its own\n * database-driven `/sitemap.xml` — and deferred to when you already list\n * `@astrojs/sitemap` yourself. Note it enumerates **prerendered** routes only,\n * so on-demand pages of an `output: 'server'` site are not listed.\n */\n sitemap?: boolean | GetvitopsSitemapOptions;\n /**\n * Site-level defaults for `<Seo />` (`@getvitops/astro/Seo.astro`) — site name,\n * title template, Open Graph + Twitter defaults, verification tokens. Off unless\n * provided; per-page props override every field.\n *\n * Needs the `site` astro.config option for canonical and `og:url`; without it\n * `<Seo />` omits every absolute-URL tag rather than deriving one from the\n * request, and this warns at build time.\n *\n * On an EmDash site prefer `<EmDashHead>`, which covers the same tags from the\n * CMS — using both double-emits them.\n */\n seo?: GetvitopsSeoOptions;\n /**\n * Resolve semantic icon names centrally and, on a server build, derive which\n * icons to bundle by scanning your source. Off unless provided; `true` uses\n * the defaults below.\n *\n * Two separate jobs, and it is worth knowing which one you are buying:\n *\n * 1. **Naming.** `<Icon name=\"menu\" />` resolves through the semantic map, so\n * the icon set is a config value rather than something spelled out at every\n * call site. A name containing `:` passes through untouched — that is the\n * escape hatch for a set-specific glyph.\n * 2. **Bundle size.** astro-icon is zero-config on a static build, but under\n * `output: 'server'` it bundles EVERY icon in a set unless given an\n * `include` map. That is the list consumers end up hand-maintaining. Here it\n * is derived by scanning `scan` for icon references, merged with anything\n * you declare. On a static build no `include` is passed at all, because\n * there is nothing to trim.\n *\n * Names the scanner cannot read statically (`<Icon name={expr} />`) are\n * reported with file and line, not guessed at — declare them in `include`.\n */\n icons?: boolean | GetvitopsIconsOptions;\n /**\n * Analytics tags for `<Analytics />` — Google Analytics, Microsoft Clarity,\n * Matomo, Plausible. Off unless provided.\n *\n * Every tag loads off the critical path (`strategy`, default `'idle'`: after\n * `load`, on an idle callback) and, when it sets cookies, only after consent.\n *\n * Which consent category a provider needs is derived from whether it sets\n * cookies, not declared — so configuring a cookie-setting provider without\n * `consent` warns rather than silently tracking everyone.\n */\n analytics?: GetvitopsAnalyticsOptions;\n /**\n * The consent gate: `@getvitops/core/consent` + `<CookieConsent />`. Off unless\n * provided; `true` uses its defaults.\n *\n * A sibling of `analytics`, not part of it. The gate is general — anything\n * marked `data-consent=\"<category>\"` waits on the same choice, so A/B\n * assignment, personalisation and third-party embeds use it too, and a site can\n * enable it with no analytics configured at all.\n *\n * Pair it with `legal` so the cookie notice describes the categories this\n * actually offers.\n */\n consent?: boolean | GetvitopsConsentOptions;\n /**\n * Ad-click attribution for `<Tracking />` — capture click IDs and UTMs from the\n * landing URL into the `_ac` cookie, so a conversion can be attributed to the\n * ad that produced it. Off unless provided.\n *\n * `_ac` is a 90-day identifier, so writing it waits on consent (`marketing` by\n * default) — and **asking for that permission is what raises the banner**, on\n * the arrival that carried a click ID and on no other. A visitor who arrives\n * organically has nothing to attribute and is never asked.\n *\n * Read the cookie back with `parseTrackingCookie` from\n * `@getvitops/utils/tracking`, and turn a conversion into a notification with\n * `@getvitops/utils/notify`.\n */\n tracking?: boolean | GetvitopsTrackingOptions;\n}\n\nexport interface GetvitopsFontsOptions {\n /** Path to a site config (JSON) whose `fonts` array holds the declarations. */\n input?: string;\n /** Declarations written inline; merged after anything `input` supplies. */\n families?: SiteFont[];\n /** Environment whose A/B variant applies to `input` (default 'production'). */\n siteEnv?: string;\n}\n\nexport interface GetvitopsIconsOptions {\n /** Icon set for UI chrome. Default `'fa7-solid'`. */\n ui?: string;\n /** Icon set for brand marks. Default `'simple-icons'`. */\n brand?: string;\n /**\n * Weight for suffix-weighted sets like Phosphor (`'bold'`, `'fill'`, …).\n * Ignored by sets that split weights across collections, e.g. Font Awesome.\n */\n weight?: string;\n /**\n * Icons to bundle, in `generateIconInclude`'s shape:\n * `{ semantic: ['menu'], 'simple-icons': ['zoho'] }`.\n *\n * Merged with what the scan finds. Declared names that don't resolve **throw**\n * (a config error); scanned names that don't resolve only **warn** (a source\n * typo shouldn't kill the dev server).\n */\n include?: Parameters<typeof generateIconInclude>[0];\n /**\n * How `<Icon />` draws the glyph.\n *\n * `'inline'` (default) reads it from the `@iconify-json/*` collection and\n * inlines the `<svg>` — no runtime request, no icon-integration peer.\n * `'sprite'` emits `<use href=\"…/icons.svg#id\">` instead, for pages served\n * where the sprite is already on the origin.\n *\n * Note this is separate from `register`: whether astro-icon is registered for\n * YOUR OWN `<Icon>` usage is a different question from how ours renders.\n */\n engine?: 'inline' | 'sprite';\n /**\n * Register the icon integration for you (default true). Set false when you\n * already list `icon()` in `integrations` yourself — the scan, the sprite and\n * the resolver still run either way.\n */\n register?: boolean;\n /** Directories to scan, relative to the project root. Default `['src']`; `false` disables. */\n scan?: string[] | false;\n /** Public href of the sprite, for the `'sprite'` engine. Default `'/vitops/icons.svg'`. */\n sprite?: string;\n /** Aliases applied before the semantic map — legacy names, or values stored in CMS content. */\n overrides?: Record<string, string>;\n}\n\n/**\n * What `<Tracking />` emits — pulled out to its own named type (rather than\n * inline in `HeadData`, which `head.test.ts`'s regex-based drift guard reads\n * line-by-line) purely so the formatter's line-wrapping doesn't turn each of\n * its fields into a false top-level `HeadData` key match.\n */\ninterface HeadTrackingData {\n enabled: boolean;\n category: string;\n cookies: string[];\n /** First enabled `site.experiments` entry's cookie name — see tracking.ts. */\n abVariantCookie: string | null;\n}\n\n/**\n * Serialised into the virtual module with `JSON.stringify`, so every field here\n * must be JSON-representable — no functions. (That is why the sitemap's *href*\n * lives here and its options, which carry `filter`/`serialize`, do not.)\n */\ninterface HeadData {\n favicons: boolean;\n faviconLinks: FaviconLink[];\n themeColor: string | null;\n webComponents: boolean;\n wcBase: string;\n editor: boolean;\n /** `<link rel=\"sitemap\">` target, or null when no sitemap was registered. */\n sitemap: string | null;\n /**\n * Families registered by `vitops({ fonts })`, for `<Font />`. Only ours: a\n * `cssVariable` Astro cannot resolve makes `<Font />` throw, so this must never\n * include a family declared elsewhere (a user-level `fonts:`, or EmDash's).\n */\n fonts: HeadFont[];\n /** `config.site`, so `<Seo />` has a canonical base even without `Astro.site`. */\n site: string | null;\n /** Site-level `<Seo />` defaults. Function-free, hence serialisable. */\n seo: GetvitopsSeoOptions;\n /** `<Analytics />` providers. IDs and flags only — function-free. */\n analytics: GetvitopsAnalyticsOptions;\n /** Is the consent gate active? Decides whether tags are gated at all. */\n consent: boolean;\n /** Categories `<CookieConsent />` offers unless a page overrides them. */\n consentCategories: OptionalConsentCategory[];\n /** Cookie-notice URL for the banner, or null. */\n consentPolicyUrl: string | null;\n /** Does `<Head />` need to load `consent.js`? (gate enabled, or a tag needs scheduling) */\n consentRuntime: boolean;\n /**\n * What `<Tracking />` emits, or null when tracking is off. Resolved here rather\n * than in the component because its warnings belong beside the others, and the\n * offered consent categories it checks against are known only at this point.\n */\n /** `abVariantCookie` is the first enabled `site.experiments` entry's cookie name — see tracking.ts. */\n tracking: HeadTrackingData | null;\n /** Enabled `site.experiments`, keyed the same way, each resolved to its cookie name — for `<Analytics />`'s reporting hook. */\n experiments: Record<string, { cookieName: string }>;\n /** `site.analytics.matomo.abVariantDimensions` — passed through for the same reporting hook. */\n matomoAbVariantDimensions: Record<string, number>;\n /**\n * What `<Ads />` emits: the `site.ads` properties plus whether this environment\n * fires them at all. Read from the site config here rather than in the component\n * because that is where the config is already parsed, and because the offered\n * consent categories must know a `marketing` demand exists before the banner is\n * built.\n */\n ads: { enabled: boolean; properties: GetvitopsAdsOptions } | null;\n}\n\n/**\n * Which providers `vitops({ analytics })` configures that the site config's\n * own `analytics` block does not.\n *\n * The two are separate surfaces on purpose — the integration must not import\n * `Config` — but they describe the same site, and a disagreement between them\n * is a compliance defect rather than a style issue: `vitops legal` derives the\n * cookie notice from the site config, so a provider missing there is a tag the\n * site runs and its own notice never mentions. That is precisely what the\n * generator's processor table exists to prevent.\n *\n * Read is best-effort — the `legal` step already reports an unreadable config,\n * and a second copy of that error helps nobody.\n */\nconst SITE_CONFIG_KEYS: Record<string, string> = {\n googleAnalytics: 'googleAnalyticsId',\n clarity: 'clarityId',\n matomo: 'matomo',\n plausible: 'plausibleDomain',\n};\n\nfunction undisclosedProviders(configPath: string, analytics: GetvitopsAnalyticsOptions): string[] {\n let declared: Record<string, unknown>;\n try {\n // `site.analytics`, matching the three-section config. Reading the wrong path\n // here does not fail quietly in the harmless direction: an empty `declared`\n // makes *every* configured provider look undisclosed, so the warning would\n // fire on correct configs and stop being worth reading.\n const parsed = JSON.parse(readFileSync(configPath, 'utf8')) as {\n site?: { analytics?: unknown };\n };\n declared = (parsed.site?.analytics ?? {}) as Record<string, unknown>;\n } catch {\n return [];\n }\n return Object.keys(SITE_CONFIG_KEYS).filter(\n (key) =>\n analytics[key as keyof GetvitopsAnalyticsOptions] !== undefined &&\n !declared[SITE_CONFIG_KEYS[key] as string],\n );\n}\n\n/**\n * Read `site.locales`/`site.defaultLocale` out of the site config, for\n * `localeConsistencyWarnings` to check against Astro's own `i18n` block.\n *\n * Best-effort, matching `readSiteAds`'s shape exactly: a malformed or missing\n * config is silence here, since every other consumer of this file already\n * validates it loudly elsewhere in the same hook.\n */\nfunction readSiteLocales(\n configPath: string,\n siteEnv: string,\n): { locales: Record<string, unknown>; defaultLocale: string } | null {\n try {\n const cfg = resolveConfig(JSON.parse(readFileSync(configPath, 'utf8')), siteEnv);\n return { locales: cfg.site.locales, defaultLocale: cfg.site.defaultLocale };\n } catch {\n return null;\n }\n}\n\n/**\n * Read `site.ads` and this environment's ads switch out of the site config.\n *\n * Best-effort, like `undisclosedProviders`: every other consumer of this file\n * validates it loudly elsewhere in the same hook, and a second copy of that error\n * helps nobody. `resolveConfig` is used rather than a bare `JSON.parse` so an A/B\n * variant's `overrides` are applied first — a variant that changes which pixels\n * run is exactly the kind of thing overrides exist for.\n *\n * The environment switch cascades `ads` → `analytics` → true. An environment that\n * has already said \"no analytics here\" has said the interesting half of it; making\n * a consumer repeat themselves to stop conversion pixels would mean a preview\n * deploy fires them by default, which is the wrong way for this to fail.\n */\nfunction readSiteAds(\n configPath: string,\n siteEnv: string,\n): { enabled: boolean; properties: GetvitopsAdsOptions } | null {\n try {\n const cfg = resolveConfig(JSON.parse(readFileSync(configPath, 'utf8')), siteEnv);\n const properties = cfg.site.ads;\n if (!properties || Object.keys(properties).length === 0) return null;\n const env = cfg.site.environments?.[siteEnv];\n return {\n enabled: env?.ads ?? env?.analytics ?? true,\n properties: properties as GetvitopsAdsOptions,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Read `site.experiments` + `site.analytics.matomo.abVariantDimensions` out of\n * the site config, resolving each experiment's cookie name (default\n * `_ab_<key>`, matching `vitopsAbTesting()`'s own default) so `<Analytics />`\n * doesn't need to know the fallback rule itself.\n *\n * Mirrors `readSiteAds`'s shape: best-effort, only enabled experiments\n * (a disabled one has no cookie to read and shouldn't cost every provider's\n * bootstrap a no-op check), `null` when nothing's configured.\n */\nfunction readSiteExperiments(\n configPath: string,\n siteEnv: string,\n): {\n experiments: Record<string, { cookieName: string }>;\n matomoAbVariantDimensions: Record<string, number>;\n /** The first enabled experiment's cookie name — see tracking.ts's own docblock for why only one. */\n primaryCookie: string | null;\n} | null {\n try {\n const cfg = resolveConfig(JSON.parse(readFileSync(configPath, 'utf8')), siteEnv);\n const configured = cfg.site.experiments;\n if (!configured || Object.keys(configured).length === 0) return null;\n const experiments: Record<string, { cookieName: string }> = {};\n let primaryCookie: string | null = null;\n for (const [key, experiment] of Object.entries(configured)) {\n if (!experiment.enabled) continue;\n const cookieName = experiment.cookieName ?? `_ab_${key}`;\n experiments[key] = { cookieName };\n primaryCookie ??= cookieName;\n }\n if (Object.keys(experiments).length === 0) return null;\n return {\n experiments,\n matomoAbVariantDimensions: cfg.site.analytics?.matomo?.abVariantDimensions ?? {},\n primaryCookie,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Is `site.crm` configured and not explicitly disabled?\n *\n * Mirrors `readSiteAds`'s \"read the config, decide nothing else\" shape — this\n * only answers the one question `hasConversionRoute`'s warning needs, not the\n * whole `CrmConfig`, which the consumer's own route file builds from the same\n * config however it likes.\n */\nfunction siteCrmEnabled(configPath: string, siteEnv: string): boolean {\n try {\n const cfg = resolveConfig(JSON.parse(readFileSync(configPath, 'utf8')), siteEnv);\n return Boolean(cfg.site.crm && cfg.site.crm.enabled !== false);\n } catch {\n return false;\n }\n}\n\n/**\n * The consent + tracking facts a site config already states.\n *\n * These were the one pair the integration did not read. `site.tracking` and\n * `site.legal.cookieConsent` drive the **generated cookie notice**, while\n * `vitops({ consent, tracking })` drove the **runtime** — two hand-synced\n * declarations of the same fact with nothing checking them against each other.\n * The failure that produces is precisely the one the rest of the toolchain is\n * fastidious about avoiding: a site can ship a cookie notice naming categories\n * its banner never offers, or promising attribution that no capture script\n * implements, and the build is clean. That is a compliance defect, not a\n * documentation gap.\n *\n * Read as DEFAULTS, the same as `css` / `fonts` / `favicon` / `ads` — the\n * config is where a site's facts live, and an integration option is how you\n * override one. A genuine contradiction is an error rather than a warning; see\n * `consentTrackingConflicts`.\n */\nfunction readSiteConsentTracking(\n configPath: string,\n siteEnv: string,\n): { tracking?: GetvitopsTrackingOptions; consentEnabled?: boolean; categories?: string[] } | null {\n try {\n const cfg = resolveConfig(JSON.parse(readFileSync(configPath, 'utf8')), siteEnv);\n const t = cfg.site.tracking;\n const cc = cfg.site.legal?.cookieConsent;\n if (!t && !cc) return null;\n return {\n ...(t ? { tracking: t as GetvitopsTrackingOptions } : {}),\n ...(cc ? { consentEnabled: cc.enabled } : {}),\n ...(cc?.categories ? { categories: cc.categories } : {}),\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Contradictions between the config and an explicit integration option.\n *\n * Absent-vs-present is a default and resolves silently. `true` vs `false` on the\n * same fact is not resolvable — one of the two documents is wrong, and picking\n * either quietly is how a notice and a banner get out of step. The message names\n * both sides so the fix does not require guessing which one won.\n */\nexport function consentTrackingConflicts(\n site: { tracking?: { enabled?: boolean | undefined } | undefined; consentEnabled?: boolean },\n opts: { consent?: unknown; tracking?: unknown },\n configPath: string,\n): string[] {\n const out: string[] = [];\n const optConsent = opts.consent === undefined ? undefined : !!opts.consent;\n if (\n site.consentEnabled !== undefined &&\n optConsent !== undefined &&\n site.consentEnabled !== optConsent\n )\n out.push(\n `consent: ${configPath} sets site.legal.cookieConsent.enabled: ${site.consentEnabled}, ` +\n `but vitops({ consent: ${optConsent} }) says the opposite. The generated cookie notice ` +\n `follows the config and the banner follows the option, so one of them would describe a ` +\n `site that does not exist. Set them the same, or drop the option and let the config decide.`,\n );\n const optTracking =\n opts.tracking === undefined\n ? undefined\n : opts.tracking === true\n ? true\n : opts.tracking === false\n ? false\n : ((opts.tracking as { enabled?: boolean }).enabled ?? true);\n if (\n site.tracking?.enabled !== undefined &&\n optTracking !== undefined &&\n site.tracking.enabled !== optTracking\n )\n out.push(\n `tracking: ${configPath} sets site.tracking.enabled: ${site.tracking.enabled}, but ` +\n `vitops({ tracking: ${optTracking} }) says the opposite. The cookie notice discloses \\`_ac\\` ` +\n `from the config while the capture script ships from the option — so the site would either ` +\n `disclose a cookie it never sets, or set one it never discloses.`,\n );\n return out;\n}\n\n/**\n * Does a route answer `TRACKING_ENDPOINT`?\n *\n * Checked by filename rather than through Astro's route manifest, because this\n * runs in `astro:config:setup` — before routes are collected. The extension set\n * covers the ways a consumer can author the same endpoint; a false negative\n * costs a warning, a false positive costs the whole point of the check, so this\n * errs toward looking in more places.\n */\nfunction hasConversionRoute(root: string): boolean {\n const base = resolve(root, 'src/pages', TRACKING_ENDPOINT.replace(/^\\//, ''));\n return ['.ts', '.js', '.mjs', '.astro'].some(\n (ext) => existsSync(`${base}${ext}`) || existsSync(join(base, `index${ext}`)),\n );\n}\n\nconst VIRTUAL_ID = 'virtual:getvitops/head';\nconst RESOLVED_ID = `\\0${VIRTUAL_ID}`;\n\nfunction virtualHeadPlugin(data: HeadData) {\n return {\n name: '@getvitops/astro:virtual-head',\n resolveId(id: string) {\n return id === VIRTUAL_ID ? RESOLVED_ID : null;\n },\n load(id: string) {\n return id === RESOLVED_ID ? `export default ${JSON.stringify(data)};` : null;\n },\n };\n}\n\n/**\n * What `<Icon />` needs to resolve a name at render time.\n *\n * JSON only, same rule as `HeadData` — which is precisely why there is no\n * `resolver` field. `resolveIcon` is a pure function in `@getvitops/utils` that\n * the component imports statically; what varies per site is the *data* it takes.\n */\ninterface IconsData {\n engine: 'inline' | 'sprite' | 'none';\n /**\n * Absolute project root, for resolving `@iconify-json/*`.\n *\n * Explicit rather than `process.cwd()`: the collections are a dependency of\n * the SITE, and the cwd is wherever the process happened to start — a task\n * runner invoking astro from a monorepo root resolves nothing and every icon\n * renders as an empty box, with no error to explain it.\n */\n root: string;\n ui: string;\n brand: string;\n weight: string | null;\n overrides: Record<string, string>;\n sprite: string | null;\n}\n\nconst ICONS_ID = 'virtual:getvitops/icons';\nconst RESOLVED_ICONS_ID = `\\0${ICONS_ID}`;\n\nfunction virtualIconsPlugin(data: IconsData) {\n return {\n name: '@getvitops/astro:virtual-icons',\n resolveId(id: string) {\n return id === ICONS_ID ? RESOLVED_ICONS_ID : null;\n },\n load(id: string) {\n return id === RESOLVED_ICONS_ID ? `export default ${JSON.stringify(data)};` : null;\n },\n };\n}\n\n/** Defaults live here so the component and the integration can't drift. */\nconst ICON_DEFAULTS = {\n ui: 'fa7-solid',\n brand: 'simple-icons',\n sprite: '/vitops/icons.svg',\n scan: ['src'],\n} as const;\n\n/**\n * Probe an optional icon integration so it can be registered for the consumer's\n * OWN `<Icon>` usage. Nothing here decides how our `<Icon />` renders — that is\n * `engine`, and it never needs a peer.\n */\n/**\n * Indirection so the bundler can't resolve the specifier statically. See the\n * import site in the `tailwind` branch for why that matters.\n */\nconst TAILWIND_VITE = '@tailwindcss/vite';\n\nasync function probeIconPackage(name: 'astro-icon' | 'astro-iconset'): Promise<boolean> {\n try {\n await import(/* @vite-ignore */ name);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Does the file at `path` hold a site config rather than a bare design system?\n *\n * Swallows a missing or unparseable file and answers `false`: this only decides\n * whether the other options may default their paths to this one, and the real\n * read happens in the Vite plugin, which reports a parse error properly. Failing\n * the build here would turn \"your JSON has a trailing comma\" into an error about\n * an option the consumer never set.\n */\nfunction readsAsConfig(path: string): boolean {\n try {\n return isConfig(JSON.parse(readFileSync(path, 'utf8')));\n } catch {\n return false;\n }\n}\n\nexport default function vitops(opts: GetvitopsOptions = {}): AstroIntegration {\n return {\n name: '@getvitops/astro',\n hooks: {\n 'astro:config:setup': async ({ config, updateConfig, injectScript, logger }) => {\n const root = fileURLToPath(config.root);\n const publicDir = fileURLToPath(config.publicDir);\n const webComponents = opts.webComponents !== false;\n const editor = opts.editor === true;\n if (editor && !opts.css)\n logger.warn('editor: true needs a `css` config — it reads design-manifest.json.');\n if (opts.legal && !opts.css)\n logger.warn(\n 'legal: needs a `css` config — the generation runs in the Vite plugin that `css` ' +\n 'registers. Run `npx vitops legal` instead if you do not use `css`.',\n );\n if (opts.media && !opts.css)\n logger.warn(\n 'media: needs a `css` config — the encoding runs in the Vite plugin that `css` ' +\n 'registers. Run `npx vitops media` instead if you do not use `css`.',\n );\n // Where the site config is, if there is one. `css.input` counts when it\n // is itself a site config — that is the common setup once a consumer\n // keeps their tokens inside `company.json`, and making every option\n // repeat the path is exactly the friction this is meant to remove.\n // The effective path, default included, so the answer matches what the\n // Vite plugin will actually read rather than only covering the case\n // where the option was written out.\n const cssInput = opts.css ? (opts.css.input ?? 'design-system.json') : undefined;\n const siteInput =\n opts.site?.input ??\n (cssInput != null && readsAsConfig(resolve(root, cssInput)) ? cssInput : undefined);\n if (opts.site && !opts.css)\n logger.warn(\n 'site: needs a `css` config — it is read during generation, which runs in the Vite ' +\n 'plugin that `css` registers. Without it the site config changes nothing here.',\n );\n // `legal` only needs its own `input` when it points somewhere other than\n // the site config; the two are almost always the same file.\n const legalInput = opts.legal?.input ?? siteInput;\n if (opts.legal && !legalInput)\n throw new Error(\n '[vitops] legal: needs `legal.input` (a site config path), the top-level `site` ' +\n 'option, or a `css.input` that is itself a site config.',\n );\n // See i18n.ts's own docblock for why this is a warning, not the hard\n // error a consent contradiction gets: either side can legitimately be\n // configured without the other.\n if (siteInput) {\n const siteLocales = readSiteLocales(\n resolve(root, siteInput),\n opts.site?.siteEnv ?? 'production',\n );\n for (const warning of localeConsistencyWarnings(siteLocales, config.i18n))\n logger.warn(warning);\n }\n if (opts.seo) {\n // Warn once here rather than letting every page silently drop its\n // canonical — a missing absolute URL is invisible in the output.\n if (!config.site)\n logger.warn(\n 'seo: needs the `site` astro.config option (your deployed URL) — without it <Seo /> ' +\n 'omits canonical, og:url and any relative og:image rather than guessing an origin.',\n );\n if (config.integrations.some((i) => i.name === 'emdash'))\n logger.warn(\n 'seo: emdash() is registered, and its <EmDashHead> emits the same title/description/' +\n 'canonical/og tags from the CMS. Use one or the other — rendering both duplicates ' +\n 'every tag.',\n );\n }\n // Analytics + consent are resolved here rather than in the component:\n // resolveAnalytics is pure, its warnings belong beside the others, and\n // step 2 needs to know whether the consent bundle has to be copied.\n //\n // The site config supplies defaults for both, and a genuine\n // contradiction fails the build — see `readSiteConsentTracking`.\n const siteConsent = siteInput\n ? readSiteConsentTracking(resolve(root, siteInput), opts.site?.siteEnv ?? 'production')\n : null;\n if (siteConsent) {\n const conflicts = consentTrackingConflicts(siteConsent, opts, siteInput as string);\n if (conflicts.length)\n throw new Error(\n `vitops: the site config and the integration options disagree about consent.\\n\\n` +\n conflicts.map((c) => ` • ${c}`).join('\\n\\n'),\n );\n }\n const consentEnabled =\n opts.consent !== undefined ? !!opts.consent : !!siteConsent?.consentEnabled;\n const consentOpts: GetvitopsConsentOptions =\n typeof opts.consent === 'object'\n ? opts.consent\n : // A `categories` list the notice already states is the banner's\n // row set: the two must agree, and the notice is the document a\n // regulator reads.\n siteConsent?.categories\n ? ({ categories: siteConsent.categories } as GetvitopsConsentOptions)\n : {};\n const analytics = resolveAnalytics(opts.analytics, { consent: consentEnabled });\n // What the banner *can* ask, not what it will. Since `<wc-consent>` reveals\n // only the rows something has demanded and the visitor hasn't answered, an\n // unused row costs a hidden `<label>` and nothing else — so the default is\n // deliberately wider than what we can detect. `analytics` covers\n // consumer-authored `data-consent` markup (an A/B split, an embed) that no\n // build-time scan can see; `preferences` covers core's own\n // `<wc-color-scheme-toggle>`, which demands it when a scheme is picked.\n //\n // This used to be a guess standing in for a fact — falling back to\n // `['analytics']` and then showing that banner to everyone. The runtime now\n // has the fact, so this only has to be generous enough not to be in its way.\n // `marketing` is the one that isn't free: it is the category a visitor is\n // most likely to read as \"this site advertises at me\", so it is offered\n // only when something configured actually needs it — a tag that says so,\n // or attribution capture, which demands it at runtime the moment someone\n // arrives on an ad click. This is the build-time half of that pair: the\n // banner must have a row for the category the capture script will ask for.\n const trackingOpts: GetvitopsTrackingOptions | undefined =\n opts.tracking === true\n ? { enabled: true }\n : opts.tracking ||\n // `site.tracking` is what the cookie notice already discloses `_ac`\n // from, so reading it here is what makes the two halves agree by\n // default instead of by hand.\n (siteConsent?.tracking?.enabled ? siteConsent.tracking : undefined);\n // Ad properties come from the site config, not an integration option — the\n // ad account is a fact about the site, and `vitops ads setup`, `ads lint`\n // and the cookie notice read the same block. Resolved before the categories\n // are chosen, because a pixel is a `marketing` demand and the banner must\n // have a row for it.\n const adsConfig = siteInput\n ? readSiteAds(resolve(root, siteInput), opts.site?.siteEnv ?? 'production')\n : null;\n const experimentsConfig = siteInput\n ? readSiteExperiments(resolve(root, siteInput), opts.site?.siteEnv ?? 'production')\n : null;\n const detectedCategories = consentCategories([\n ...analytics.tags,\n ...resolveAds(adsConfig?.properties, { consent: consentEnabled, enabled: true }).tags,\n ]);\n const defaultCategories: OptionalConsentCategory[] = ['analytics', 'preferences'];\n const trackingCategory = trackingOpts?.enabled\n ? (trackingOpts.category ?? 'marketing')\n : null;\n if (detectedCategories.includes('marketing') || trackingCategory === 'marketing')\n defaultCategories.push('marketing');\n const offeredCategories: OptionalConsentCategory[] =\n consentOpts.categories ?? defaultCategories;\n for (const warning of analytics.warnings) logger.warn(warning);\n\n const tracking = resolveTracking(trackingOpts, {\n consent: consentEnabled,\n consentCategories: offeredCategories,\n });\n for (const warning of tracking.warnings) logger.warn(warning);\n // The capture script beacons `tel:` conversions at a fixed path, but the\n // route that answers it is the consumer's to write. Nothing connected\n // the two: a missing or differently-named route means every beacon 404s\n // and conversions vanish with no error on either side. The route file is\n // a build-time fact, so this is checkable.\n if (tracking.enabled && !hasConversionRoute(root)) {\n logger.warn(\n `tracking is on, so the capture script beacons \\`tel:\\` conversions to ` +\n `${TRACKING_ENDPOINT}, but no route answers that path. Add ` +\n `\\`src/pages/api/track.ts\\` exporting \\`createConversionRoute(...)\\` from ` +\n `\\`@getvitops/astro/routes\\` — otherwise every call conversion is silently lost.`,\n );\n }\n // The same \"config asserts a fact nothing implements\" gap Turnstile hit:\n // `site.crm` names a provider, but nothing writes to it unless a route\n // passes the `crm` option to `createConversionRoute()`.\n if (\n siteInput &&\n siteCrmEnabled(resolve(root, siteInput), opts.site?.siteEnv ?? 'production') &&\n !hasConversionRoute(root)\n ) {\n logger.warn(\n `site.crm is configured, but no route answers ${TRACKING_ENDPOINT}. Add ` +\n `\\`src/pages/api/track.ts\\` exporting \\`createConversionRoute({ crm: {...}, ... })\\` ` +\n `from \\`@getvitops/astro/routes\\` — otherwise submissions never reach the CRM.`,\n );\n }\n\n // Resolved a second time, now against the categories the banner will\n // actually offer and this environment's switch, so its warnings are about\n // the build that ships rather than the probe above.\n const ads = resolveAds(adsConfig?.properties, {\n consent: consentEnabled,\n consentCategories: offeredCategories,\n enabled: adsConfig?.enabled ?? true,\n });\n for (const warning of ads.warnings) logger.warn(warning);\n\n // `legalInput`, not `opts.legal.input` — the path is now a cascade\n // (explicit legal input → `site.input` → a `css.input` that reads as a\n // site config), so a consumer who declares `legal: {}` and lets it fall\n // through still gets the disclosure check rather than skipping it.\n if (opts.analytics && legalInput) {\n const undisclosed = undisclosedProviders(resolve(root, legalInput), opts.analytics);\n if (undisclosed.length)\n logger.warn(\n `analytics: ${undisclosed.join(', ')} ${undisclosed.length === 1 ? 'is' : 'are'} ` +\n \"configured here but absent from your site config's `analytics` block, so the \" +\n 'cookie notice `legal` generates will not disclose ' +\n `${undisclosed.length === 1 ? 'it' : 'them'}. Add ` +\n `${undisclosed.map((k) => SITE_CONFIG_KEYS[k]).join(', ')} to ${legalInput}.`,\n );\n }\n\n const hasManifest = !!(opts.favicon?.name && opts.favicon?.themeColor);\n let hasSvg = false;\n\n // 1. Favicons + manifest → public/\n if (opts.favicon) {\n const source = resolve(root, opts.favicon.source);\n hasSvg = source.endsWith('.svg');\n await generateFavicons({\n source,\n outputDir: publicDir,\n ...(opts.favicon.lowResSource\n ? { lowResSource: resolve(root, opts.favicon.lowResSource) }\n : {}),\n // Also the manifest's `background_color`. The maskable outputs are\n // composited onto it, so the raster and the manifest agree.\n ...(opts.favicon.backgroundColor\n ? { backgroundColor: opts.favicon.backgroundColor }\n : {}),\n });\n if (opts.favicon.name && opts.favicon.themeColor) {\n await writeFaviconManifest(publicDir, {\n name: opts.favicon.name,\n themeColor: opts.favicon.themeColor,\n ...(opts.favicon.backgroundColor\n ? { backgroundColor: opts.favicon.backgroundColor }\n : {}),\n });\n }\n logger.info('generated favicons + manifest → public/');\n }\n\n // 1b. Webfonts → Astro's `fonts:` config (+ <Font /> data for <Head />)\n let headFonts: HeadFont[] = [];\n if (opts.fonts) {\n const given: GetvitopsFontsOptions = Array.isArray(opts.fonts)\n ? { families: opts.fonts }\n : typeof opts.fonts === 'string'\n ? { input: opts.fonts }\n : opts.fonts === true\n ? {}\n : opts.fonts;\n // `fonts: true` (or an options object with no `input`) reads the site\n // config's own `fonts` array — one place to declare a family, next to\n // the rest of the site's facts. That config is wherever it is: the\n // `site` option, or `css.input` when it is one.\n const o: GetvitopsFontsOptions = {\n ...given,\n ...(given.input == null && given.families == null && siteInput\n ? { input: siteInput, ...(opts.site?.siteEnv ? { siteEnv: opts.site.siteEnv } : {}) }\n : {}),\n };\n const declared: SiteFont[] = [];\n if (o.input) {\n // resolveConfig validates and throws with field paths, so a typo'd\n // provider fails the build here rather than resolving to a fallback stack.\n const cfg = resolveConfig(\n JSON.parse(readFileSync(resolve(root, o.input), 'utf8')),\n o.siteEnv ?? 'production',\n );\n declared.push(...(cfg.site.fonts ?? []));\n }\n declared.push(...(o.families ?? []));\n\n if (declared.length === 0) {\n logger.warn(\n 'fonts: nothing declared — no webfonts will load. Add families, or drop the option.',\n );\n } else {\n // A user-level `fonts:` is already on `config` here; another integration's\n // is not (its updateConfig may not have run yet). Astro concatenates the\n // arrays, and two entries on one variable means the last wins silently —\n // so flag the half we can actually see.\n const existing = new Set(\n ((config as { fonts?: { cssVariable?: string }[] }).fonts ?? []).map(\n (f) => f.cssVariable,\n ),\n );\n const { fonts, head } = resolveFonts(declared);\n for (const f of head)\n if (existing.has(f.cssVariable))\n logger.warn(\n `fonts: ${f.cssVariable} is already declared in your astro.config \\`fonts\\` ` +\n 'array. Astro resolves one family per variable, so one of the two will be ' +\n 'dropped — remove it from one place.',\n );\n updateConfig({ fonts });\n headFonts = head;\n const preloaded = head.filter((f) => f.preload !== false).length;\n logger.info(\n `fonts: ${fonts.length} famil${fonts.length === 1 ? 'y' : 'ies'} registered ` +\n `via Astro's Fonts API (${preloaded} preloaded) — <Head /> emits <Font /> for each`,\n );\n }\n }\n\n // 2. Web-component bundles → public/vitops/\n //\n // `consent.js` is copied independently of `webComponents`: the gate is\n // what decides whether third-party tags run, so a site that turned the\n // element runtime off and still asked for consent must not silently lose\n // it — that failure mode is tags loading for everyone.\n const consentRuntime = consentEnabled || analytics.needsRuntime;\n if (webComponents || consentRuntime) {\n const corePkg = fileURLToPath(import.meta.resolve('@getvitops/core/package.json'));\n const coreDist = join(dirname(corePkg), 'dist');\n const dest = join(publicDir, 'vitops');\n if (existsSync(coreDist)) {\n const bundles = webComponents\n ? ['polyfills.js', 'deferred.js', 'elements.js', 'polyfills']\n : [];\n if (consentRuntime) bundles.push('consent.js');\n if (editor) bundles.push('editor.js');\n for (const f of bundles) {\n const src = join(coreDist, f);\n if (existsSync(src)) cpSync(src, join(dest, f), { recursive: true });\n }\n logger.info('copied web-component bundles → public/vitops/');\n } else {\n logger.warn(\n `@getvitops/core dist not found (${coreDist}); run build:core. Skipping WC copy.`,\n );\n }\n }\n\n // 3. Sitemap — opt-in, delegated to the official @astrojs/sitemap.\n //\n // Registered through updateConfig rather than imported at module scope,\n // because the package is an optional peer. Astro core has no\n // addIntegration() (that is Starlight's plugin API), but appending here is\n // supported: runHookConfigSetup re-reads `integrations.length` each\n // iteration and assigns the merged array back onto the settings, and\n // @astrojs/sitemap declares no `astro:config:setup` hook at all — only\n // config:done / routes:resolved / build:done, which all run afterwards.\n //\n // Runs before the virtual head module so <Head /> can link the result.\n let sitemapHref: string | null = null;\n if (opts.sitemap) {\n const registered = config.integrations.map((i) => i.name);\n if (registered.includes('@astrojs/sitemap')) {\n logger.info(\n 'sitemap: @astrojs/sitemap is already in your integrations — leaving yours in charge.',\n );\n } else if (registered.includes('emdash')) {\n logger.warn(\n 'sitemap: emdash() already serves /sitemap.xml from the database, which also covers ' +\n 'on-demand pages a static sitemap cannot. Skipping — drop the `sitemap` option, or ' +\n 'add @astrojs/sitemap to `integrations` yourself to run both.',\n );\n } else if (!config.site) {\n logger.warn(\n 'sitemap: needs the `site` astro.config option (your deployed URL) — a sitemap lists ' +\n 'absolute URLs. Skipping.',\n );\n } else {\n // Optional peer, loaded the same way as @tailwindcss/vite below.\n //\n // The import is cast because the option surface here is this package's\n // narrowed mirror, not @astrojs/sitemap's own `SitemapOptions`: the two\n // disagree on `serialize`'s entry type (upstream's `changefreq` is the\n // `EnumChangefreq` enum, ours the string union a consumer actually\n // wants to write). The shapes are identical at runtime — the value is\n // forwarded verbatim. Cast the module rather than widening the local to\n // `(o?: unknown)`, which is the wrong variance: a target accepting\n // `unknown` promises to accept *any* argument, which upstream doesn't.\n type SitemapFactory = (o?: GetvitopsSitemapOptions) => AstroIntegration;\n let sitemap: SitemapFactory;\n try {\n ({ default: sitemap } = (await import('@astrojs/sitemap')) as unknown as {\n default: SitemapFactory;\n });\n } catch {\n throw new Error(\n '[vitops] sitemap requires `@astrojs/sitemap` in your devDependencies — ' +\n 'install it, or drop the `sitemap` option.',\n );\n }\n if (config.output === 'server')\n logger.warn(\n \"sitemap: output: 'server' — @astrojs/sitemap lists prerendered routes only, so \" +\n 'on-demand pages are left out. Add `export const prerender = true` to the pages ' +\n 'you want indexed, or list them in `sitemap.customPages`.',\n );\n const sitemapOpts = opts.sitemap === true ? undefined : opts.sitemap;\n updateConfig({ integrations: [sitemap(sitemapOpts)] });\n sitemapHref = `/${sitemapOpts?.filenameBase ?? 'sitemap'}-index.xml`;\n logger.info(`sitemap registered → ${sitemapHref.slice(1)}`);\n }\n }\n\n // 3b. The IndexNow key file.\n //\n // Derived from the site config rather than an option of its own: the key\n // is already declared there for `vitops search notify`, and a second place to\n // write it is a second place for it to disagree. Nothing else is needed\n // to opt in — a config with `seo.indexing.indexNow.key` has, by saying so,\n // asked for the file that makes the key usable.\n //\n // Not a secret. IndexNow verifies host ownership by fetching this file\n // back, so publishing it *is* the mechanism.\n if (siteInput) {\n try {\n const cfg = resolveConfig(\n JSON.parse(readFileSync(resolve(root, siteInput), 'utf8')),\n opts.site?.siteEnv ?? 'production',\n );\n const key = cfg.site.seo?.indexing?.indexNow?.key;\n if (key) {\n mkdirSync(publicDir, { recursive: true });\n writeFileSync(join(publicDir, `${key}.txt`), `${key}\\n`);\n logger.info(`IndexNow key file → public/${key}.txt`);\n }\n\n // 3c. robots.txt — the same environment.robots fact search notify's\n // noindex gate already reads. Unlike the key file, this is written\n // only if the consumer has not already placed one: today's\n // `RobotsSchema` is a single directive string, with no way to\n // express path-level rules (`Disallow: /admin`), so a hand-authored\n // file may carry more than the config can express and must not be\n // silently overwritten every build.\n const robotsPath = join(publicDir, 'robots.txt');\n if (!existsSync(robotsPath)) {\n mkdirSync(publicDir, { recursive: true });\n writeFileSync(\n robotsPath,\n renderRobotsTxt(cfg, { env: opts.site?.siteEnv ?? 'production' }),\n );\n logger.info('robots.txt → public/robots.txt');\n }\n } catch (err) {\n // Never fatal. Every other consumer of this config validates it\n // loudly elsewhere in this hook; failing the build a second time,\n // here, would just bury the first message.\n logger.warn(`IndexNow key file / robots.txt skipped: ${(err as Error).message}`);\n }\n }\n\n // 4. Icons — resolve names centrally, and derive the bundle on a server build.\n //\n // Everything here has to finish BEFORE the updateConfig that appends the\n // icon integration, because an appended integration's own\n // astro:config:setup runs after this hook returns — it cannot be handed\n // options computed later. That is why the scan is synchronous rather than\n // deferred to the Vite plugin's buildStart, which runs far too late.\n let iconsData: IconsData | null = null;\n if (opts.icons) {\n const o: GetvitopsIconsOptions = opts.icons === true ? {} : opts.icons;\n const ui = o.ui ?? ICON_DEFAULTS.ui;\n const brand = o.brand ?? ICON_DEFAULTS.brand;\n const spriteHref = o.sprite ?? ICON_DEFAULTS.sprite;\n\n // Declared icons first. An unresolvable name here THROWS — it's a\n // config error, and generateIconInclude has always been loud about it.\n // `exactOptionalPropertyTypes` is on, so an optional key has to be\n // absent rather than explicitly undefined.\n const weightOpt = o.weight ? { weight: o.weight } : {};\n const include: Record<string, string[]> = o.include\n ? generateIconInclude({ ui, brand, ...weightOpt, ...o.include })\n : {};\n const add = (prefix: string, name: string) => {\n const list = (include[prefix] ??= []);\n if (!list.includes(name)) list.push(name);\n };\n\n // Then whatever the source actually references.\n const scanDirs = o.scan === false ? [] : (o.scan ?? [...ICON_DEFAULTS.scan]);\n const unresolved: string[] = [];\n const dynamic: { file: string; line: number; expr: string }[] = [];\n for (const dir of scanDirs) {\n const abs = resolve(root, dir);\n if (!existsSync(abs)) continue;\n const found = collectIconRefs(scanFiles(abs));\n dynamic.push(\n ...found.dynamic.map((d) => ({\n file: relative(root, d.file),\n line: d.line,\n expr: d.expr,\n })),\n );\n for (const name of found.names) {\n // Already qualified — group it under its own prefix verbatim.\n const colon = name.indexOf(':');\n if (colon > 0) {\n add(name.slice(0, colon), name.slice(colon + 1));\n continue;\n }\n // A scanned name that doesn't resolve only WARNS. It is far more\n // likely a local SVG (astro-icon reads src/icons/*.svg by bare\n // name) or a sprite id than a mistake, and a dev server that dies\n // on a typo in a template is worse than one that tells you.\n try {\n const q = resolveIcon(name, ui, weightOpt);\n add(q.slice(0, q.indexOf(':')), q.slice(q.indexOf(':') + 1));\n } catch {\n unresolved.push(name);\n }\n }\n }\n\n // `include` only matters where a bundle is at stake. astro-icon is\n // zero-config on a static build; passing a list there would trim\n // nothing and risk dropping a glyph the scan couldn't see.\n const wantsInclude = config.output !== 'static';\n if (dynamic.length && wantsInclude) {\n logger.warn(\n `icons: ${dynamic.length} icon name(s) are computed at runtime and cannot be ` +\n `bundled automatically:\\n` +\n dynamic.map((d) => ` ${d.file}:${d.line} ${d.expr}`).join('\\n') +\n `\\n Declare them in \\`icons.include\\` (or set \\`icons.scan: false\\` to silence this).`,\n );\n }\n if (unresolved.length)\n logger.warn(\n `icons: ${unresolved.length} name(s) are not in the semantic map for '${ui}': ` +\n `${unresolved.join(', ')}. Left as-is — fine for a local src/icons/*.svg or a ` +\n `sprite id, a typo otherwise.`,\n );\n\n const engine: IconsData['engine'] = o.engine ?? 'inline';\n if (o.sprite && !opts.css)\n logger.warn(\n 'icons: the sprite is written by the same pass that generates the CSS, so ' +\n '`sprite` without `css` produces no file. Add `css`, or run `vitops icons --sprite`.',\n );\n\n const registered = config.integrations.map((i) => i.name);\n const already = registered.find((n) => n === 'astro-icon' || n === 'astro-iconset');\n if (o.register === false) {\n logger.info('icons: register: false — resolving names only, not registering anything.');\n } else if (already) {\n // Only the registration is skipped. The resolver, the scan and the\n // sprite are not astro-icon's job, so they still run.\n logger.info(\n `icons: ${already} is already in your integrations — leaving yours in charge.`,\n );\n } else {\n // Registered for the consumer's OWN `<Icon>` (astro-icon's), and to\n // hand it the derived include. Independent of how ours renders —\n // `<Icon />` inlines from the collection and needs no integration —\n // so a missing package is silence, not an error.\n const pkg = (await probeIconPackage('astro-icon'))\n ? 'astro-icon'\n : (await probeIconPackage('astro-iconset'))\n ? 'astro-iconset'\n : null;\n if (pkg) {\n type IconFactory = (o?: Record<string, unknown>) => AstroIntegration;\n const { default: icon } = (await import(/* @vite-ignore */ pkg)) as unknown as {\n default: IconFactory;\n };\n updateConfig({ integrations: [icon(wantsInclude ? { include } : {})] });\n logger.info(\n wantsInclude\n ? `icons: ${pkg} registered — bundling ${Object.values(include).flat().length} icon(s)`\n : `icons: ${pkg} registered (output: 'static' — no include needed)`,\n );\n }\n }\n\n iconsData = {\n engine,\n root,\n ui,\n brand,\n weight: o.weight ?? null,\n overrides: o.overrides ?? {},\n sprite: engine === 'sprite' || o.sprite ? spriteHref : null,\n };\n }\n\n // 5. Virtual head module for <Head/>\n updateConfig({\n vite: {\n plugins: [\n virtualIconsPlugin(\n iconsData ?? {\n engine: 'none',\n root,\n ui: ICON_DEFAULTS.ui,\n brand: ICON_DEFAULTS.brand,\n weight: null,\n overrides: {},\n sprite: null,\n },\n ),\n virtualHeadPlugin({\n favicons: !!opts.favicon,\n faviconLinks: opts.favicon ? faviconLinks({ hasSvg, manifest: hasManifest }) : [],\n themeColor: opts.favicon?.themeColor ?? null,\n webComponents,\n wcBase: '/vitops',\n editor,\n sitemap: sitemapHref,\n fonts: headFonts,\n site: config.site ?? null,\n seo: opts.seo ?? {},\n analytics: opts.analytics ?? {},\n consent: consentEnabled,\n consentCategories: offeredCategories,\n consentPolicyUrl: consentOpts.policyUrl ?? null,\n consentRuntime,\n tracking: tracking.enabled\n ? {\n enabled: true,\n category: tracking.category,\n cookies: tracking.cookies,\n abVariantCookie: experimentsConfig?.primaryCookie ?? null,\n }\n : null,\n experiments: experimentsConfig?.experiments ?? {},\n matomoAbVariantDimensions: experimentsConfig?.matomoAbVariantDimensions ?? {},\n ads: adsConfig,\n }),\n ],\n },\n });\n\n // 6. CSS — generate + compile + auto-inject (consumer imports nothing).\n if (opts.css) {\n const format: StylesheetFormat = opts.css.format ?? 'tailwind';\n // Typed out above, but a plain-JS config reaches here unchecked and\n // would otherwise fail late with an unresolvable `styles.css` import.\n if ((format as string) === 'design')\n throw new Error(\n \"[vitops] css.format: 'design' emits only DESIGN.md, not a stylesheet. \" +\n 'Run `npx vitops generate --format design --out .` for that file, and set ' +\n \"css.format to 'tailwind' | 'css' | 'bricks' here.\",\n );\n const out = opts.css.out ?? 'src/styles';\n const plugins = [\n vitopsPlugin({\n input: opts.css.input ?? 'design-system.json',\n ...(opts.css.theme != null ? { theme: opts.css.theme } : {}),\n format,\n out,\n // Mirrored inside the generate pass so the served copy can never be\n // missing on a cold start or stale after a config edit.\n ...(editor ? { editorManifestDir: publicDir } : {}),\n // Same mirroring rule as the manifest: the bytes are produced by\n // this pass, so the copy belongs inside it rather than in the hook.\n ...(iconsData?.sprite ? { spriteDir: publicDir } : {}),\n // Same reasoning: inside the pass, so the documents track the site\n // config in dev instead of going stale after the first build.\n ...(opts.legal ? { legal: { ...opts.legal, input: legalInput as string } } : {}),\n // Byte-producing, so it belongs in the pass rather than in this hook.\n // Its own cache makes a rebuild cheap; nothing here needs to gate it.\n ...(opts.media ? { media: opts.media } : {}),\n // Read during generation for the site-level facts the stylesheet\n // depends on — currently `designSystem.defaultColorScheme`, which decides\n // whether the colour layer carries a prefers-color-scheme block.\n ...(opts.site ? { site: opts.site } : {}),\n ...(opts.css.systemColorScheme != null\n ? { systemColorScheme: opts.css.systemColorScheme }\n : {}),\n }),\n ];\n // Tailwind is an optional peer — only the `tailwind` format needs it,\n // so it's loaded here rather than imported at module scope. (The\n // plugin is typed against a newer vite than @getvitops/vite's peer;\n // the Plugin shapes are structurally compatible at runtime — Astro\n // forwards them verbatim — so bridge the two type identities.)\n //\n // The specifier goes through a constant, with `@vite-ignore`, for the\n // same reason `probeIconPackage` does it: a statically analysable\n // `import('@tailwindcss/vite')` makes the bundler FOLLOW the module\n // even on a `css`-format site that never reaches this branch, and the\n // chain ends at @tailwindcss/oxide's native `.node` binding, which it\n // cannot parse. That failed the whole build with an opaque\n // \"stream did not contain valid UTF-8\" — for a site not using Tailwind\n // at all. `optional peer` has to mean optional to the bundler too.\n if (format === 'tailwind') {\n let tailwindcss: () => unknown[];\n try {\n ({ default: tailwindcss } = (await import(\n /* @vite-ignore */ TAILWIND_VITE\n )) as unknown as { default: () => unknown[] });\n } catch {\n throw new Error(\n \"[vitops] css.format: 'tailwind' requires `tailwindcss` and `@tailwindcss/vite` \" +\n 'in your devDependencies — install them, or switch to css.format: ' +\n \"'css' for the standalone bundle.\",\n );\n }\n plugins.push(...(tailwindcss() as unknown as typeof plugins));\n }\n updateConfig({ vite: { plugins } });\n const cssFile = format === 'tailwind' ? 'tailwind.css' : 'styles.css';\n if (opts.css.inject !== false) {\n const cssPath = resolve(root, out, cssFile);\n injectScript('page-ssr', `import ${JSON.stringify(cssPath)};`);\n logger.info(`design-system CSS (${format}) auto-injected from ${out}/${cssFile}`);\n } else {\n logger.info(\n `design-system CSS (${format}) generated at ${out}/${cssFile} (inject: false — import it from your layout)`,\n );\n }\n }\n },\n },\n };\n}\n","/**\n * Real per-page `<lastmod>` for the sitemap, derived from git.\n *\n * This is the one lever that reliably moves Google. It is also load-bearing for\n * `vitops search notify`, which decides what to submit by diffing lastmod against the\n * previous build — with no lastmod, that diff can see pages appear and disappear\n * but never see one *change*, so an edited page is never resubmitted.\n *\n * A plain exported helper rather than magic inside the `sitemap` option, because\n * it carries a caveat the caller needs to see: it shells out to `git`, so it\n * returns nothing useful from a shallow CI clone (`fetch-depth: 1` — the default\n * in actions/checkout) or from a tarball with no history. In that case it warns\n * and emits no dates at all.\n *\n * import vitops, { gitLastmod } from '@getvitops/astro';\n * export default defineConfig({\n * site: 'https://acme.ca',\n * integrations: [vitops({ sitemap: { serialize: await gitLastmod() } })],\n * });\n *\n * **No date beats a wrong date.** An unmatched URL is left alone rather than\n * stamped with the build time: Google weighs lastmod only while it stays\n * consistent with what actually changed, and a site that stamps every page on\n * every deploy teaches it to stop believing the field — site-wide, not just for\n * the pages that were wrong.\n */\nimport { execFile } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { promisify } from 'node:util';\nimport type { GetvitopsSitemapEntry } from './integration.ts';\n\nconst exec = promisify(execFile);\n\n/** Route-bearing sources: file-based pages, and the collections they render. */\nconst DEFAULT_DIRS = ['src/pages', 'src/content', 'src/data'];\n\nconst PAGE_EXT = /\\.(astro|md|mdx|markdown|html)$/;\nconst CONTENT_EXT = /\\.(md|mdx|markdown|json|yaml|yml)$/;\n\nexport interface GitLastmodOptions {\n /** Directories to read history from (default `src/pages`, `src/content`, `src/data`). */\n dirs?: string[];\n /** Repository root (default `process.cwd()`). */\n cwd?: string;\n /** Called with a human-readable reason when no dates could be derived. */\n onWarn?: (message: string) => void;\n}\n\n/**\n * `src/pages/blog/index.astro` → `/blog`; `src/pages/index.astro` → `/`.\n *\n * Dynamic routes return `undefined`: one source file backs many URLs, and its\n * commit date describes the template rather than any of them. Stamping every\n * generated page with the template's date is exactly the \"wrong date\" case above.\n */\nexport function routeFromPage(file: string): string | undefined {\n const m = /(?:^|\\/)src\\/pages\\/(.+)$/.exec(file);\n if (!m) return undefined;\n let rest = m[1]!;\n if (!PAGE_EXT.test(rest)) return undefined;\n if (rest.includes('[')) return undefined; // dynamic route\n rest = rest.replace(PAGE_EXT, '');\n if (rest === 'index') return '/';\n rest = rest.replace(/\\/index$/, '');\n return `/${rest}`;\n}\n\n/** The slug a content entry is most likely published under. */\nexport function slugFromContent(file: string): string | undefined {\n const m = /(?:^|\\/)src\\/(?:content|data)\\/(.+)$/.exec(file);\n if (!m) return undefined;\n const rest = m[1]!;\n if (!CONTENT_EXT.test(rest)) return undefined;\n const base = rest.split('/').pop()!.replace(CONTENT_EXT, '');\n // `index` names the collection's directory, not a slug of its own.\n return base === 'index' ? undefined : base;\n}\n\nconst normalise = (pathname: string): string => {\n const p = pathname.replace(/\\/+$/, '');\n return p === '' ? '/' : p;\n};\n\n/**\n * Most recent commit date per file, from one `git log` pass.\n *\n * One process rather than one per file: a `git log -1` per page is fine for\n * twenty pages and unusable for two thousand. `--name-only` interleaves each\n * commit's date with its files, and because `git log` is newest-first the first\n * time a path appears is its latest change.\n */\nasync function commitDates(dirs: string[], cwd: string): Promise<Map<string, string>> {\n const dates = new Map<string, string>();\n const { stdout } = await exec(\n 'git',\n ['log', '--date-order', '--pretty=format:%x00%cI', '--name-only', '--', ...dirs],\n { cwd, maxBuffer: 64 * 1024 * 1024 },\n );\n\n let current: string | undefined;\n for (const line of stdout.split('\\n')) {\n if (line.startsWith('\\0')) {\n current = line.slice(1).trim();\n continue;\n }\n const file = line.trim();\n if (!file || !current) continue;\n if (!dates.has(file)) dates.set(file, current);\n }\n return dates;\n}\n\n/**\n * Build a `serialize` function that stamps `lastmod` from git history.\n *\n * Resolution order per URL: an exact file-based route first, then a unique\n * content slug. \"Unique\" is doing real work — a slug appearing in two collections\n * is dropped rather than guessed at, since picking either one would be a coin\n * flip presented as a fact.\n */\nexport async function gitLastmod(\n options: GitLastmodOptions = {},\n): Promise<(item: GetvitopsSitemapEntry) => GetvitopsSitemapEntry> {\n const cwd = options.cwd ?? process.cwd();\n const warn = options.onWarn ?? ((m: string) => console.warn(`[vitops] gitLastmod: ${m}`));\n const dirs = (options.dirs ?? DEFAULT_DIRS).filter((d) => existsSync(`${cwd}/${d}`));\n\n const identity = (item: GetvitopsSitemapEntry) => item;\n\n if (dirs.length === 0) {\n warn(`none of ${(options.dirs ?? DEFAULT_DIRS).join(', ')} exist — no dates emitted`);\n return identity;\n }\n\n let dates: Map<string, string>;\n try {\n dates = await commitDates(dirs, cwd);\n } catch (err) {\n warn(`git log failed (${(err as Error).message}) — no dates emitted`);\n return identity;\n }\n\n if (dates.size === 0) {\n // The shallow-clone case, and the one most likely to go unnoticed: the build\n // succeeds, the sitemap is valid, and every lastmod is simply missing.\n warn(\n 'git history has no commits touching your source — a shallow clone? Set fetch-depth: 0 in your checkout step. No dates emitted.',\n );\n return identity;\n }\n\n const byRoute = new Map<string, string>();\n const bySlug = new Map<string, string>();\n const ambiguous = new Set<string>();\n\n for (const [file, date] of dates) {\n const route = routeFromPage(file);\n if (route) {\n byRoute.set(route, date);\n continue;\n }\n const slug = slugFromContent(file);\n if (!slug) continue;\n if (bySlug.has(slug)) ambiguous.add(slug);\n else bySlug.set(slug, date);\n }\n for (const slug of ambiguous) bySlug.delete(slug);\n\n /*\n * No coverage warning here on purpose. `serialize` is called once per entry\n * with no completion signal, so any \"matched N of M\" it emitted would be a\n * running total printed mid-stream. The same fact is reported properly one step\n * later: `vitops search notify` counts entries without a `<lastmod>` and says so\n * against the finished sitemap.\n */\n return (item: GetvitopsSitemapEntry): GetvitopsSitemapEntry => {\n // An explicit lastmod from the caller wins — they know something we inferred.\n if (item.lastmod) return item;\n\n let pathname: string;\n try {\n pathname = normalise(new URL(item.url).pathname);\n } catch {\n return item;\n }\n\n const date = byRoute.get(pathname) ?? bySlug.get(pathname.split('/').pop() ?? '');\n return date ? { ...item, lastmod: date } : item;\n };\n}\n","/**\n * The `<Seo />` resolution layer — site-level defaults merged with per-page props\n * into the exact tags to emit.\n *\n * Kept separate from `Seo.astro` deliberately. The component this replaces\n * (`SEO.astro`, deleted in c949cae) fused resolution with markup, which is why it\n * was never unit-tested and why its title handling drifted: it computed a title\n * for `og:title` but left `<title>` to the layout, so the two could disagree.\n * Everything decidable lives here and is asserted in `seo.test.ts`; the component\n * only renders what this returns.\n */\n\n/** Twitter card types, per <https://developer.x.com/en/docs/twitter-for-websites/cards>. */\nexport type SeoTwitterCard = 'summary' | 'summary_large_image' | 'app' | 'player';\n\n/** Open Graph object types this component knows how to describe. */\nexport type SeoOgType = 'website' | 'article' | 'profile';\n\n/**\n * Site-level SEO defaults, passed to `vitops({ seo })` and baked into the\n * `virtual:getvitops/head` module. Per-page `<Seo />` props override these.\n *\n * The vocabulary mirrors the site-config schema's `seo` block\n * (`@getvitops/generator`, `src/config.ts`) so a `Config` → these adapter would\n * be a flat field map — but it is not imported from there. This is the\n * integration's own option surface, and it must stay JSON-serialisable: it is\n * `JSON.stringify`d into the virtual module, so no functions, no `URL`s, no dates.\n */\nexport interface GetvitopsSeoOptions {\n /** `og:site_name`, and the `<title>` when a page passes none. */\n siteName?: string;\n /**\n * Page-title pattern; `%s` is the page's own title. e.g. `'%s · Acme'`.\n *\n * Applies to `<title>` only. `og:title`/`twitter:title` get the untemplated\n * title: a social card already shows `og:site_name` and the domain, so the\n * suffix would sit directly above the brand it repeats. Override per page with\n * `ogTitle`.\n *\n * Skipped when a page's title already equals `siteName` — a homepage titled\n * \"Acme\" emits `Acme`, not `Acme · Acme` — and never applied to `defaultTitle`.\n */\n titleTemplate?: string;\n /** `<title>` for pages that pass none. Used verbatim — the template is not applied. */\n defaultTitle?: string;\n /** `<meta name=\"description\">` for pages that pass none. */\n defaultDescription?: string;\n /** Site-wide robots directive, e.g. `'index,follow,max-image-preview:large'`. */\n robots?: string;\n /** Google Search Console verification token. */\n googleSiteVerification?: string;\n /** Bing Webmaster Tools verification token. */\n bingSiteVerification?: string;\n openGraph?: {\n /** Default `og:type` (default `'website'`). */\n type?: SeoOgType;\n /** `og:locale`. Written either way — `en-CA` and `en_CA` both emit `en_CA`. */\n locale?: string;\n /** Additional locales the same content exists in → `og:locale:alternate`. */\n localeAlternates?: string[];\n /** Fallback share image. Relative URLs resolve against `site`. */\n image?: { url: string; alt?: string; width?: number; height?: number; type?: string };\n };\n twitter?: {\n /** Card type when no image resolves (default `'summary'`). */\n card?: SeoTwitterCard;\n /** The site's own handle, `@acme`. */\n site?: string;\n /** Default author handle, `@ada`. */\n creator?: string;\n };\n}\n\n/** Per-page `<Seo />` props. Everything is optional; defaults fill the gaps. */\nexport interface SeoProps {\n /** Page title, before `titleTemplate` is applied. */\n title?: string;\n /**\n * `og:title`/`twitter:title`, when the social headline should differ from\n * `title`. Defaults to `title` **without** `titleTemplate` — see the note on\n * `titleTemplate` for why the suffix is dropped there.\n */\n ogTitle?: string;\n description?: string;\n /** Overrides the computed canonical. Relative values resolve against `site`. */\n canonical?: string;\n /** Overrides `og:site_name` and the title fallback. */\n siteName?: string;\n /** Share image for this page. Relative URLs resolve against `site`. */\n image?: string;\n imageAlt?: string;\n imageWidth?: number;\n imageHeight?: number;\n /** MIME type of the share image, e.g. `'image/png'`. */\n imageType?: string;\n noindex?: boolean;\n nofollow?: boolean;\n noarchive?: boolean;\n nocache?: boolean;\n /** Extra robots directives, e.g. `'max-snippet:-1'`. Appended to the composed value. */\n robotsExtras?: string;\n /** Full robots override. Wins over the flags above and the site default. */\n robots?: string;\n /** `og:type` for this page (default: the site default, else `'website'`). */\n type?: SeoOgType;\n /** Emitted only when `type` is `'article'`. */\n article?: {\n publishedTime?: string;\n modifiedTime?: string;\n expirationTime?: string;\n authors?: string[];\n section?: string;\n tags?: string[];\n };\n /** `og:locale` for this page. */\n locale?: string;\n /** Twitter author handle for this page, `@ada`. */\n twitterCreator?: string;\n /**\n * Translations of *this* page, as `<link rel=\"alternate\" hreflang>`.\n *\n * Explicit only, and it must include the current page. Nothing is inferred:\n * the deleted component derived alternates from the site's locale list and\n * pointed every one of them at that locale's homepage, so every page on the\n * site claimed the homepage as its translation. Only the page knows its own\n * translated URLs. Pass `hreflang: 'x-default'` for the fallback entry.\n */\n alternates?: { hreflang: string; href: string }[];\n}\n\n/** A `<meta property>` tag (Open Graph uses `property`, not `name`). */\nexport interface SeoPropertyTag {\n property: string;\n content: string;\n}\n\n/** A `<meta name>` tag. */\nexport interface SeoNameTag {\n name: string;\n content: string;\n}\n\n/** Everything `<Seo />` renders, already decided. `null` means \"emit nothing\". */\nexport interface ResolvedSeo {\n title: string | null;\n /** `og:title`/`twitter:title` — the page title without `titleTemplate`. */\n socialTitle: string | null;\n description: string | null;\n canonical: string | null;\n robots: string | null;\n openGraph: SeoPropertyTag[];\n twitter: SeoNameTag[];\n verification: SeoNameTag[];\n alternates: { hreflang: string; href: string }[];\n}\n\n/** Render context supplied by the component from `Astro`. */\nexport interface SeoContext {\n /**\n * The deployed origin — `Astro.site`, or `config.site` via the virtual module.\n *\n * `undefined` when neither is set, and then every absolute-URL tag is dropped\n * rather than being derived from the request. `astro-seo` falls back to\n * `Astro.url` here; on a static build that is `http://localhost:4321`, which\n * would bake a localhost canonical into production HTML.\n */\n site?: URL | undefined;\n /** `Astro.url.pathname` — already includes `base`. */\n pathname: string;\n /** `Astro.url.search`, if the canonical should keep the query string. */\n search?: string;\n}\n\n/** `en-CA` → `en_CA`; Open Graph wants the underscore form. */\nconst ogLocale = (locale: string) => locale.replace('-', '_');\n\n/**\n * Absolute URLs pass through; relative ones resolve against `site`, or are\n * dropped when there is no `site` to resolve them against. A relative `og:image`\n * or canonical is worse than none — crawlers resolve it against whatever origin\n * served the page.\n */\nfunction absolute(url: string | undefined, site: URL | undefined): string | null {\n if (!url) return null;\n try {\n return new URL(url).href;\n } catch {\n return site ? new URL(url, site).href : null;\n }\n}\n\n/** Composes the robots directive, or null when nothing meaningful was asked for. */\nfunction resolveRobots(props: SeoProps, defaults: GetvitopsSeoOptions): string | null {\n if (props.robots) return props.robots;\n const { noindex, nofollow, noarchive, nocache, robotsExtras } = props;\n if (noindex || nofollow || noarchive || nocache || robotsExtras) {\n return [\n noindex ? 'noindex' : 'index',\n nofollow ? 'nofollow' : 'follow',\n ...(noarchive ? ['noarchive'] : []),\n ...(nocache ? ['nocache'] : []),\n ...(robotsExtras ? [robotsExtras] : []),\n ].join(', ');\n }\n // No explicit default either: emit nothing. `index, follow` is what crawlers\n // already assume, so stating it adds bytes and no meaning.\n return defaults.robots ?? null;\n}\n\n/**\n * Merges site defaults with page props into the final tag set.\n *\n * Pure — no `Astro`, no filesystem, no config lookup. Everything it needs comes\n * through its three arguments, which is what makes it testable and what keeps the\n * package free of the consumer-global coupling that sank the previous site model.\n */\nexport function resolveSeo(\n defaults: GetvitopsSeoOptions,\n props: SeoProps,\n ctx: SeoContext,\n): ResolvedSeo {\n const { site } = ctx;\n const siteName = props.siteName ?? defaults.siteName ?? null;\n\n // The template applies to a page's own title only, and only when that title\n // isn't already the site name. Two ways to get \"Acme · Acme\" otherwise: running\n // it over `defaultTitle`, and a homepage whose own title happens to equal the\n // site name — which is the common case, not an edge case.\n const templated = props.title && defaults.titleTemplate && props.title !== siteName;\n const fallbackTitle = defaults.defaultTitle ?? siteName;\n const title = props.title\n ? templated\n ? String(defaults.titleTemplate).replaceAll('%s', props.title)\n : props.title\n : fallbackTitle;\n\n /*\n * `og:title`/`twitter:title` take the page's title *without* the template.\n *\n * The two are read in different places. `<title>` is navigational — a browser\n * tab or a search result, competing with a dozen others, so it needs the brand\n * suffix to disambiguate. A social card is self-contained and already shows\n * `og:site_name` and the domain, so repeating the brand there is furniture:\n * \"Installation · Acme\" sitting directly above \"Acme\".\n *\n * They still derive from one value, which is the part that matters — the\n * component this replaces computed them in separate places and they drifted.\n * Pass `ogTitle` when a page wants a genuinely different social headline.\n */\n const socialTitle = props.ogTitle ?? props.title ?? fallbackTitle;\n\n const description = props.description ?? defaults.defaultDescription ?? null;\n\n const canonical = props.canonical\n ? absolute(props.canonical, site)\n : site\n ? new URL(ctx.pathname + (ctx.search ?? ''), site).href\n : null;\n\n const image = absolute(props.image, site) ?? absolute(defaults.openGraph?.image?.url, site);\n // Only inherit the default image's metadata when the page didn't bring its own\n // image — otherwise a page's image would carry the default's alt text.\n const usingDefaultImage = !props.image;\n const defaultImage = defaults.openGraph?.image;\n const imageAlt = props.imageAlt ?? (usingDefaultImage ? defaultImage?.alt : undefined);\n const imageWidth = props.imageWidth ?? (usingDefaultImage ? defaultImage?.width : undefined);\n const imageHeight = props.imageHeight ?? (usingDefaultImage ? defaultImage?.height : undefined);\n const imageType = props.imageType ?? (usingDefaultImage ? defaultImage?.type : undefined);\n\n const type = props.type ?? defaults.openGraph?.type ?? 'website';\n const locale = props.locale ?? defaults.openGraph?.locale;\n\n const openGraph: SeoPropertyTag[] = [];\n const og = (property: string, content: string | number | null | undefined) => {\n if (content !== null && content !== undefined && content !== '')\n openGraph.push({ property, content: String(content) });\n };\n\n og('og:type', type);\n og('og:title', socialTitle);\n og('og:description', description);\n og('og:url', canonical);\n og('og:site_name', siteName);\n og('og:locale', locale ? ogLocale(locale) : null);\n for (const alt of defaults.openGraph?.localeAlternates ?? [])\n og('og:locale:alternate', ogLocale(alt));\n og('og:image', image);\n if (image) {\n og('og:image:alt', imageAlt);\n og('og:image:width', imageWidth);\n og('og:image:height', imageHeight);\n og('og:image:type', imageType);\n }\n if (type === 'article' && props.article) {\n const a = props.article;\n og('article:published_time', a.publishedTime);\n og('article:modified_time', a.modifiedTime);\n og('article:expiration_time', a.expirationTime);\n for (const author of a.authors ?? []) og('article:author', author);\n og('article:section', a.section);\n for (const tag of a.tags ?? []) og('article:tag', tag);\n }\n\n const twitter: SeoNameTag[] = [];\n const tw = (name: string, content: string | null | undefined) => {\n if (content) twitter.push({ name, content });\n };\n // A card with a large image is only honoured if there *is* an image.\n tw('twitter:card', image ? 'summary_large_image' : (defaults.twitter?.card ?? 'summary'));\n tw('twitter:site', defaults.twitter?.site);\n tw('twitter:creator', props.twitterCreator ?? defaults.twitter?.creator);\n tw('twitter:title', socialTitle);\n tw('twitter:description', description);\n tw('twitter:image', image);\n tw('twitter:image:alt', image ? imageAlt : null);\n\n const verification: SeoNameTag[] = [];\n if (defaults.googleSiteVerification)\n verification.push({\n name: 'google-site-verification',\n content: defaults.googleSiteVerification,\n });\n if (defaults.bingSiteVerification)\n verification.push({ name: 'msvalidate.01', content: defaults.bingSiteVerification });\n\n return {\n title,\n socialTitle,\n description,\n canonical,\n robots: resolveRobots(props, defaults),\n openGraph,\n twitter,\n verification,\n alternates: props.alternates ?? [],\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["SAFE_ID","readReviewsCache"],"sources":["../src/analytics.ts","../src/ads.ts","../src/fonts.ts","../src/i18n.ts","../src/seo-defaults.ts","../src/integration.ts","../src/inline-scripts.ts","../src/csp.ts","../src/lastmod.ts","../src/seo.ts"],"sourcesContent":["/**\n * The `<Analytics />` resolution layer — provider config in, the exact tags to\n * emit out.\n *\n * Kept separate from `Analytics.astro` for the same reason `seo.ts` is separate\n * from `Seo.astro`: everything decidable is a pure function and asserted in\n * `analytics.test.ts`, and the component only renders what this returns.\n *\n * Two properties here are the point of the whole file:\n *\n * - **Which consent category a provider needs is derived, never declared.** It\n * follows from whether that provider sets cookies, which follows from its own\n * configuration (Matomo with `disableCookies` is genuinely cookieless; Matomo\n * with `cookies: true` is not). A consumer cannot mark Google Analytics\n * `necessary` to skip a banner, because `setsCookies` isn't theirs to set.\n * - **The same fact drives the legal documents.** `@getvitops/generator`'s\n * `legal/providers.ts` maps the *site config's* `analytics` block onto named\n * processors with cookie lists. If a provider is configured here but not\n * there, the site runs a tag its own cookie notice doesn't disclose — so the\n * integration warns, and `warnings` is returned as data so that check is a\n * test rather than a log line someone might scroll past.\n */\n\n/**\n * When a tag is allowed to load. Third-party analytics is never worth a\n * millisecond of the critical path, so the default puts every tag after `load`.\n */\nexport type AnalyticsStrategy = 'idle' | 'async' | 'interaction';\n\n/**\n * The consent vocabulary, mirroring `@getvitops/core`'s `consent/store.ts`.\n * Mirrored rather than imported because this value is `JSON.stringify`d into the\n * virtual module and the component reads it at build time, while core's copy is a\n * browser runtime — `consent.test.ts` and `analytics.test.ts` pin both ends.\n */\nexport type ConsentCategory = 'necessary' | 'analytics' | 'marketing' | 'preferences';\n\nexport type OptionalConsentCategory = Exclude<ConsentCategory, 'necessary'>;\n\nexport interface GoogleAnalyticsOptions {\n /** Measurement ID, e.g. `G-XXXXXXXXXX`. */\n id: string;\n /**\n * Which category gates it (default `analytics`). Set `marketing` when the\n * property feeds Google Ads — the tag is the same, the permission isn't.\n */\n category?: OptionalConsentCategory;\n /** Extra `gtag('config', id, …)` parameters, e.g. `{ send_page_view: false }`. */\n config?: Record<string, string | number | boolean>;\n}\n\nexport interface ClarityOptions {\n /** Clarity project ID. */\n id: string;\n category?: OptionalConsentCategory;\n}\n\nexport interface MatomoOptions {\n /** Instance base URL — self-hosted, or `https://<you>.matomo.cloud`. */\n url: string;\n /** Site ID within that instance. */\n siteId: string | number;\n /**\n * Let Matomo set its cookies (default **false**).\n *\n * The default is what makes Matomo the cookieless option: with\n * `disableCookies` it stores no identifier on the device, needs no banner, and\n * the generated cookie notice can state positively that it sets none. Turning\n * this on gives you returning-visitor metrics and a consent obligation.\n */\n cookies?: boolean;\n}\n\nexport interface PlausibleOptions {\n /** The domain as registered in Plausible, e.g. `example.com`. */\n domain: string;\n /** Script URL — override for a self-hosted instance or a custom-events build. */\n src?: string;\n}\n\nexport interface GetvitopsAnalyticsOptions {\n googleAnalytics?: string | GoogleAnalyticsOptions;\n clarity?: string | ClarityOptions;\n matomo?: MatomoOptions;\n plausible?: string | PlausibleOptions;\n /** When tags load (default `'idle'`). */\n strategy?: AnalyticsStrategy;\n}\n\n/**\n * The consent gate, configured on the integration rather than inside `analytics`.\n *\n * That placement is the design, not an accident: analytics is one consumer of\n * consent, not its owner. A/B assignment, account personalisation and third-party\n * embeds gate on the same choice via `data-consent`, and a site can enable the\n * banner with no analytics configured at all.\n */\nexport interface GetvitopsConsentOptions {\n /**\n * Categories the banner offers. Defaults to the ones the configured providers\n * actually need — offering a choice nothing is waiting on is noise.\n */\n categories?: OptionalConsentCategory[];\n /** Link the banner to the cookie notice (`vitops legal` renders one). */\n policyUrl?: string;\n}\n\n/**\n * A third-party tag, fully resolved — everything `<GatedTags />` needs to render\n * one, with nothing left to decide.\n *\n * Shared by `<Analytics />` and `<Ads />` on purpose: the inert-markup rule\n * (`type=\"text/plain\"`, the URL on `data-src`, never a live `src`) is the whole\n * consent gate, and it must have exactly one implementation. What differs between\n * the two is where the tag comes from and who decides its category, which is why\n * they have separate resolvers over one shape rather than one resolver over two\n * vocabularies.\n *\n * `key` is left to each resolver: analytics keys are its four providers, ads keys\n * are `AdProvider`. Widening it into one union here would let a provider from\n * either side satisfy a check meant for the other.\n */\nexport interface GatedTag {\n /** The provider as a reader would recognise it — used in warnings. */\n provider: string;\n category: ConsentCategory;\n setsCookies: boolean;\n /** Names (or `prefix*` patterns) to clear when consent is withdrawn. */\n cookies: string[];\n strategy: AnalyticsStrategy;\n /**\n * Emitted inert (`type=\"text/plain\"`) for the runtime to activate, rather than\n * as a live `<script>`. False only for the one case that needs no runtime at\n * all: a cookieless provider with `strategy: 'async'`.\n */\n gated: boolean;\n /** Bootstrap JS. Runs before `src`, so a queue is populated before it drains. */\n inline: string;\n src: string | null;\n attrs: Record<string, string>;\n}\n\n/** One analytics tag. */\nexport interface ResolvedTag extends GatedTag {\n key: 'googleAnalytics' | 'clarity' | 'matomo' | 'plausible';\n}\n\nexport interface ResolvedAnalytics {\n tags: ResolvedTag[];\n /** Does anything here need `@getvitops/core/consent` loaded? */\n needsRuntime: boolean;\n /** Configuration problems, as prose. The integration logs these. */\n warnings: string[];\n}\n\nexport interface ResolveAnalyticsContext {\n /** Is the consent gate enabled on this site (`vitops({ consent })`)? */\n consent?: boolean;\n /**\n * Enabled `site.experiments` entries, keyed the same way, each carrying the\n * resolved cookie name `vitopsAbTesting()`'s middleware assigns\n * (`_ab_<key>` unless overridden). When set, each provider's bootstrap\n * reads these cookies client-side and reports the assignment via that\n * provider's own custom-dimension/property mechanism — a no-op when this\n * is empty, so a site not using experiments sees no behavior change.\n */\n experiments?: Record<string, { cookieName: string }>;\n /** `site.analytics.matomo.abVariantDimensions` — Matomo's pre-created custom-dimension index per experiment key. */\n matomoAbVariantDimensions?: Record<string, number>;\n}\n\n/**\n * A small inline snippet reading each experiment's cookie into a plain\n * object, skipping any experiment whose cookie isn't set (visitor not yet\n * assigned, or the experiment disabled after this page was cached). Shared\n * across providers so the cookie-parsing logic — and its `SAFE_ID` guard on\n * both the experiment key and cookie name, the same discipline `bad()`\n * already applies to provider IDs landing in an inline `<script>` — exists\n * exactly once.\n */\nfunction experimentCookieReads(experiments: Record<string, { cookieName: string }>): string {\n const entries = Object.entries(experiments).filter(([key, e]) => !bad(key) && !bad(e.cookieName));\n if (!entries.length) return '';\n return (\n `function _abrc(n){var m=document.cookie.match(new RegExp('(?:^|; )'+n+'=([^;]*)'));return m?decodeURIComponent(m[1]):null;}` +\n `var _ab={};` +\n entries\n .map(([key, e]) => `_ab[${JSON.stringify(key)}]=_abrc(${JSON.stringify(e.cookieName)});`)\n .join('') +\n `for(var _k in _ab){if(_ab[_k]==null)delete _ab[_k];}`\n );\n}\n\n/**\n * IDs land inside an inline `<script>`, so they are checked rather than trusted.\n * A `</script>` in a measurement ID is not a plausible accident, but the cost of\n * the guard is one regex and the cost of not having it is arbitrary script\n * injection from a config file.\n */\nconst SAFE_ID = /^[\\w.-]+$/;\n\nfunction bad(value: string): boolean {\n return !SAFE_ID.test(value);\n}\n\n/** JSON is a subset of JS object-literal syntax; `<` can't appear unescaped. */\nfunction literal(value: Record<string, string | number | boolean>): string {\n return JSON.stringify(value).replaceAll('<', '\\\\u003c');\n}\n\nfunction googleAnalytics(\n input: string | GoogleAnalyticsOptions,\n experiments: Record<string, { cookieName: string }> = {},\n): ResolvedTag | string {\n const opts = typeof input === 'string' ? { id: input } : input;\n if (bad(opts.id)) return `googleAnalytics: \"${opts.id}\" is not a valid measurement ID. Skipped.`;\n const staticConfig = opts.config ?? {};\n const reads = experimentCookieReads(experiments);\n // With no experiments configured, the config object is static, exactly as\n // before — same output byte-for-byte, no runtime cost added. With\n // experiments, the automatic pageview's `gtag('config', …)` call is built\n // at runtime instead, merging each assigned variant in under its\n // experiment key so it reaches GA4 as a custom parameter on that same hit\n // (registered as a custom dimension in GA4 Admin by parameter name).\n const gtagConfig = reads\n ? `${reads}gtag('config','${opts.id}',Object.assign(${literal(staticConfig)},_ab));`\n : `gtag('config','${opts.id}'${opts.config ? `,${literal(staticConfig)}` : ''});`;\n return {\n key: 'googleAnalytics',\n provider: 'Google Analytics',\n category: opts.category ?? 'analytics',\n setsCookies: true,\n cookies: ['_ga', '_ga_*', '_gid', '_gat', '_gat_*'],\n strategy: 'idle',\n gated: true,\n inline:\n `window.dataLayer=window.dataLayer||[];` +\n `function gtag(){dataLayer.push(arguments)}` +\n `gtag('js',new Date());` +\n gtagConfig,\n src: `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(opts.id)}`,\n attrs: {},\n };\n}\n\nfunction clarity(input: string | ClarityOptions): ResolvedTag | string {\n const opts = typeof input === 'string' ? { id: input } : input;\n if (bad(opts.id)) return `clarity: \"${opts.id}\" is not a valid project ID. Skipped.`;\n const category = opts.category ?? 'analytics';\n // Clarity's snippet injects its own <script>, so there is no `src` for us to\n // schedule — activation *is* the load.\n //\n // The consentv2 call is not redundant with having gated the tag: Microsoft\n // enforces consent signals for EEA/UK/CH traffic from the project side, and\n // without one Clarity degrades to no-consent mode even though the visitor\n // agreed. `ad_Storage` follows the category, because that is the only thing\n // that tells us whether this tag was permitted for advertising.\n return {\n key: 'clarity',\n provider: 'Microsoft Clarity',\n category,\n setsCookies: true,\n cookies: ['_clck', '_clsk', 'MUID', 'CLID', 'ANONCHK', 'SM'],\n strategy: 'idle',\n gated: true,\n inline:\n `(function(c,l,a,r,i,t,y){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};` +\n `t=l.createElement(r);t.async=1;t.src=\"https://www.clarity.ms/tag/\"+i;` +\n `y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y)})` +\n `(window,document,\"clarity\",\"script\",\"${opts.id}\");` +\n `window.clarity('consentv2',{ad_Storage:'${\n category === 'marketing' ? 'granted' : 'denied'\n }',analytics_Storage:'granted'});`,\n src: null,\n attrs: {},\n };\n}\n\nfunction matomo(\n opts: MatomoOptions,\n experiments: Record<string, { cookieName: string }> = {},\n dimensions: Record<string, number> = {},\n): ResolvedTag | string {\n const siteId = String(opts.siteId);\n if (bad(siteId)) return `matomo: \"${siteId}\" is not a valid site ID. Skipped.`;\n let base: URL;\n try {\n base = new URL(opts.url);\n } catch {\n return `matomo: \"${opts.url}\" is not a valid URL. Skipped.`;\n }\n const url = `${base.origin}${base.pathname.replace(/\\/*$/, '')}/`;\n const cookies = opts.cookies === true;\n\n // Only experiments with a recorded dimension index push anything — one\n // with no index is silently skipped here (validateConfig's\n // `experimentDimensionWarnings` is what surfaces that as a build-time\n // warning, not a hard error: the pageview still ships correctly).\n const dimensionEntries = Object.entries(experiments).filter(([key]) => dimensions[key] != null);\n const reads = experimentCookieReads(Object.fromEntries(dimensionEntries));\n const dimensionPushes = reads\n ? reads +\n dimensionEntries\n .map(\n ([key]) =>\n `if(_ab[${JSON.stringify(key)}]!=null)_paq.push(['setCustomDimension',${dimensions[key]},_ab[${JSON.stringify(key)}]]);`,\n )\n .join('')\n : '';\n\n // `disableCookies` and the experiment dimension pushes must both land\n // *before* trackPageView — the former so the first request of the visit\n // doesn't set the cookies it exists to avoid, the latter because a\n // dimension pushed after trackPageView misses that pageview entirely.\n return {\n key: 'matomo',\n provider: 'Matomo',\n category: cookies ? 'analytics' : 'necessary',\n setsCookies: cookies,\n cookies: cookies ? ['_pk_id*', '_pk_ses*', '_pk_ref*', '_pk_cvar*', '_pk_hsr*'] : [],\n strategy: 'idle',\n gated: true,\n inline:\n `var _paq=window._paq=window._paq||[];` +\n (cookies ? '' : `_paq.push(['disableCookies']);`) +\n dimensionPushes +\n `_paq.push(['trackPageView']);_paq.push(['enableLinkTracking']);` +\n `(function(){var u=\"${url}\";` +\n `_paq.push(['setTrackerUrl',u+'matomo.php']);_paq.push(['setSiteId','${siteId}']);` +\n `var d=document,g=d.createElement('script'),s=d.getElementsByTagName('script')[0];` +\n `g.async=true;g.src=u+'matomo.js';s.parentNode.insertBefore(g,s)})();`,\n src: null,\n attrs: {},\n };\n}\n\nfunction plausible(\n input: string | PlausibleOptions,\n experiments: Record<string, { cookieName: string }> = {},\n): ResolvedTag | string {\n const opts = typeof input === 'string' ? { domain: input } : input;\n if (bad(opts.domain)) return `plausible: \"${opts.domain}\" is not a valid domain. Skipped.`;\n // Plausible's current API (plausible.init({customProperties})) needs no\n // special script variant and no manual-pageview mode — customProperties\n // attaches to every tracked event, including the automatic pageview, so\n // there's no double-counting risk to design around. The queueing stub\n // mirrors gtag's own inline bootstrap above: the real script (loaded via\n // `src`, async) picks up this call once it's ready, regardless of which\n // one finishes loading first.\n const reads = experimentCookieReads(experiments);\n const inline = reads\n ? `window.plausible=window.plausible||function(){(window.plausible.q=window.plausible.q||[]).push(arguments)};` +\n `plausible.init({customProperties:function(){${reads}return _ab;}});`\n : '';\n return {\n key: 'plausible',\n provider: 'Plausible Analytics',\n // Cookieless and storing no cross-site identifier, so there is nothing for a\n // banner to ask about. It still goes through the runtime for scheduling.\n category: 'necessary',\n setsCookies: false,\n cookies: [],\n strategy: 'idle',\n gated: true,\n inline,\n src: opts.src ?? 'https://plausible.io/js/script.js',\n attrs: { 'data-domain': opts.domain },\n };\n}\n\n/**\n * Resolve the configured providers into the tags to emit.\n *\n * An invalid provider is dropped with a warning rather than throwing: one\n * malformed ID should not take down a build, and a missing tag is visible in the\n * output while a failed build is just a red wall.\n */\nexport function resolveAnalytics(\n opts: GetvitopsAnalyticsOptions | undefined,\n ctx: ResolveAnalyticsContext = {},\n): ResolvedAnalytics {\n const warnings: string[] = [];\n const tags: ResolvedTag[] = [];\n if (!opts) return { tags, needsRuntime: false, warnings };\n\n const strategy = opts.strategy ?? 'idle';\n const experiments = ctx.experiments ?? {};\n const results = [\n opts.googleAnalytics === undefined ? null : googleAnalytics(opts.googleAnalytics, experiments),\n opts.clarity === undefined ? null : clarity(opts.clarity),\n opts.matomo === undefined\n ? null\n : matomo(opts.matomo, experiments, ctx.matomoAbVariantDimensions ?? {}),\n opts.plausible === undefined ? null : plausible(opts.plausible, experiments),\n ];\n\n for (const result of results) {\n if (result === null) continue;\n if (typeof result === 'string') {\n warnings.push(result);\n continue;\n }\n // A cookieless tag on `async` needs nothing from the runtime, so it is emitted\n // as an ordinary <script async> and that site ships no consent bundle at all.\n const gated = result.category !== 'necessary' || strategy !== 'async';\n tags.push({ ...result, strategy, gated });\n }\n\n const setting = tags.filter((t) => t.setsCookies);\n if (setting.length && !ctx.consent) {\n warnings.push(\n `${setting.map((t) => t.provider).join(' and ')} ${setting.length === 1 ? 'sets' : 'set'} ` +\n `cookies (${setting.flatMap((t) => t.cookies).join(', ')}) and \\`consent\\` is off, so ` +\n `${setting.length === 1 ? 'it loads' : 'they load'} for every visitor with no way to ` +\n 'decline. Set `consent: true`, or switch to a cookieless provider (Plausible, or Matomo ' +\n 'with its default `cookies: false`).',\n );\n }\n\n return { tags, needsRuntime: tags.some((t) => t.gated), warnings };\n}\n\n/** Which optional categories a set of tags actually needs a visitor to decide. */\nexport function consentCategories(tags: GatedTag[]): OptionalConsentCategory[] {\n const used = new Set<OptionalConsentCategory>();\n for (const tag of tags) {\n if (tag.category !== 'necessary') used.add(tag.category);\n }\n return (['analytics', 'marketing', 'preferences'] as const).filter((c) => used.has(c));\n}\n","/**\n * The `<Ads />` resolution layer — the site's ad properties in, the exact tags to\n * emit out.\n *\n * A sibling of `analytics.ts`, deliberately not part of it. The two render the\n * same gated markup, but they answer different questions and four things differ:\n *\n * - **Where they come from.** Analytics providers are integration options\n * (`vitops({ analytics })`); ad properties are `site.ads` in the site config,\n * because the ad account is a fact about the site that `vitops ads setup`,\n * `vitops ads lint` and the generated cookie notice all read too.\n * - **Who decides the consent category.** `resolveAnalytics` *derives* it from\n * whether the provider sets cookies, so a consumer cannot mark Google Analytics\n * `necessary` to dodge a banner. Every ad pixel sets cookies and is advertising,\n * so the answer is `marketing` unless the config says `analytics` — a stated\n * fact, not a derivation.\n * - **What the key means.** `ResolvedTag['key']` is the four analytics providers,\n * and `analytics.ts` pairs that set against the generator's processor table.\n * Widening it to eight ad platforms would make that check answer for tags it\n * does not govern.\n * - **When they fire.** Analytics is gated per environment by\n * `environments.<env>.analytics`; a preview deployment sending pageviews is\n * survivable, one firing conversion pixels is not, so ads gets its own switch.\n *\n * Everything decidable is here and unit-tested; `Ads.astro` renders what this\n * returns and chooses nothing.\n */\nimport {\n AD_PLATFORMS,\n AD_PROVIDER_KEYS,\n normalizeConversionLabels,\n type AdProvider,\n type ConversionLabelMap,\n} from '@getvitops/utils/ads';\nimport type { AdPropertyConfig } from '@getvitops/config';\nimport type { AnalyticsStrategy, ConsentCategory, GatedTag } from './analytics.ts';\n\n/**\n * One ad property, as `<Ads />` reads it: the fields of a `site.ads` entry that decide what is\n * emitted. A subset of `AdPropertyConfig` from `@getvitops/config` — the schema's own inferred\n * type, not a copy — so a plain object is still enough for a consumer to pass, and a field the\n * schema renames fails here instead of silently reading `undefined`.\n */\nexport type GetvitopsAdProperty = Pick<\n AdPropertyConfig,\n | 'accountId'\n | 'pixelId'\n | 'conversionLabel'\n | 'conversionActionId'\n | 'reporting'\n | 'category'\n | 'enabled'\n>;\n\nexport type GetvitopsAdsOptions = Partial<Record<AdProvider, GetvitopsAdProperty>>;\n\n/** One ad tag. `key` is the platform, so a caller can find one without matching prose. */\nexport interface ResolvedAdTag extends GatedTag {\n key: AdProvider;\n}\n\nexport interface ResolvedAds {\n tags: ResolvedAdTag[];\n /** Does anything here need `@getvitops/core/consent` loaded? */\n needsRuntime: boolean;\n /** Configuration problems, as prose. The integration logs these. */\n warnings: string[];\n}\n\nexport interface ResolveAdsContext {\n /** Is the consent gate enabled on this site (`vitops({ consent })`)? */\n consent?: boolean;\n /** Categories the banner offers, so we can catch a demand with no row. */\n consentCategories?: readonly string[];\n /** `environments.<env>.ads` — false switches every tag off for this build. */\n enabled?: boolean;\n /** When the tags load. Default `idle`: a pixel is never worth the critical path. */\n strategy?: AnalyticsStrategy;\n}\n\n/**\n * IDs land inside an inline `<script>`, so they are checked rather than trusted —\n * the same guard, for the same reason, as `analytics.ts`'s `SAFE_ID`. A `</script>`\n * in a pixel ID is not a plausible accident; arbitrary script injection from a\n * config file is not a plausible thing to allow either.\n */\nconst SAFE_ID = /^[\\w.-]+$/;\n\n/** Resolve the configured ad properties into the tags `<Ads />` emits. */\nexport function resolveAds(\n ads: GetvitopsAdsOptions | undefined,\n ctx: ResolveAdsContext = {},\n): ResolvedAds {\n const warnings: string[] = [];\n const tags: ResolvedAdTag[] = [];\n const strategy = ctx.strategy ?? 'idle';\n\n const configured = AD_PROVIDER_KEYS.filter((p) => ads?.[p] != null);\n if (configured.length === 0) return { tags, needsRuntime: false, warnings };\n\n // A whole-environment switch, checked before anything else: a preview deploy\n // must be able to carry the same config without firing a single pixel.\n if (ctx.enabled === false) return { tags, needsRuntime: false, warnings };\n\n for (const provider of configured) {\n const entry = ads![provider]!;\n if (entry.enabled === false) continue;\n\n const platform = AD_PLATFORMS[provider];\n const id = entry[platform.tag.needs];\n if (!id) {\n warnings.push(\n `ads: ${provider} has no ${platform.tag.needs}, so no tag is emitted. ` +\n `Run \\`vitops ads setup\\` to be prompted for it.`,\n );\n continue;\n }\n if (!SAFE_ID.test(id)) {\n warnings.push(`ads: ${provider} ${platform.tag.needs} \"${id}\" is not a valid ID. Skipped.`);\n continue;\n }\n\n // `conversionLabel` lands inside the same inline `<script>` as `id` (Google's\n // bootstrap embeds it to expose a conversion `send_to` target per action) —\n // same injection surface, so every configured label gets the same check.\n // Unlike an invalid `id`, an invalid label doesn't sink the whole tag: it\n // just means that one action's conversion doesn't fire, which is worth a\n // warning rather than dropping every other configured action too.\n let safeEntry = entry;\n const labels = normalizeConversionLabels(entry.conversionLabel);\n const invalidActions = Object.entries(labels)\n .filter(([, label]) => label && !SAFE_ID.test(label))\n .map(([action]) => action);\n if (invalidActions.length) {\n for (const action of invalidActions) {\n warnings.push(\n `ads: ${provider} conversionLabel.${action} \"${labels[action as keyof ConversionLabelMap]}\" ` +\n 'is not a valid label. Conversion firing skipped for that action.',\n );\n }\n const cleanLabels = Object.fromEntries(\n Object.entries(labels).filter(([action]) => !invalidActions.includes(action)),\n );\n // Omit the key entirely rather than setting it `undefined` —\n // `exactOptionalPropertyTypes` treats the two differently.\n const { conversionLabel: _drop, ...rest } = entry;\n safeEntry = Object.keys(cleanLabels).length\n ? { ...rest, conversionLabel: cleanLabels }\n : rest;\n }\n\n const category: ConsentCategory = entry.category ?? 'marketing';\n tags.push({\n key: provider,\n provider: platform.name,\n category,\n // Every platform in the table sets cookies; the tag is gated whenever the\n // site has a gate. With no gate the tag still renders — and the warning\n // below says what that means, rather than silently declining to advertise.\n setsCookies: true,\n cookies: platform.tag.cookies,\n strategy,\n gated: ctx.consent === true,\n inline: platform.tag.bootstrap(id, { provider, ...safeEntry }),\n src: platform.tag.src ? platform.tag.src(id) : null,\n attrs: {},\n });\n }\n\n if (tags.length === 0) return { tags, needsRuntime: false, warnings };\n\n if (ctx.consent !== true) {\n warnings.push(\n `ads: ${tags.map((t) => t.provider).join(', ')} ${tags.length === 1 ? 'sets' : 'set'} ` +\n 'advertising cookies, and `consent` is off — so they load for every visitor with no way ' +\n 'to decline. Set `consent: true`.',\n );\n } else {\n const missing = [...new Set(tags.map((t) => t.category))].filter(\n (c) => ctx.consentCategories && !ctx.consentCategories.includes(c),\n );\n if (missing.length)\n warnings.push(\n `ads: the tags demand the ${missing.join(', ')} consent categor${missing.length === 1 ? 'y' : 'ies'}, ` +\n `but the banner offers ${ctx.consentCategories?.join(', ') || 'nothing'}. Add ` +\n `${missing.join(', ')} to \\`consent.categories\\`, or the tags wait on a question the ` +\n 'banner never asks.',\n );\n }\n\n return { tags, needsRuntime: true, warnings };\n}\n","/**\n * Font LOADING — turning declarations into Astro's `fonts:` config.\n *\n * The design system NAMES fonts (`design-system.json`'s `fonts` block → `--font-<name>`\n * tokens) and loads none of them: a stack is a `font-family` list, with no `@font-face`,\n * no preload, and no metrics-matched fallback behind it. Something has to load them, and\n * the failure mode when nothing does is invisible — installing a `@fontsource*` package\n * and importing its CSS renders correctly while silently giving up subsetting, preload\n * and the `size-adjust`/`ascent-override` metrics that keep CLS down.\n *\n * So this module is the seam, in the same spirit as `vitopsHosting()`: declare the family\n * once and let the integration wire it, rather than hand-rolling the config.\n *\n * The declaration shape is `SiteFont` from the generator — deliberately not a new type.\n * The site config has carried a `fonts` array (provider / weights / subsets / preload)\n * since it was written; what was missing was anything that read it.\n */\nimport type { SiteFont } from '@getvitops/generator';\nimport { fontProviders } from 'astro/config';\n\n/**\n * Provider names, matching `fontProviders`' keys 1:1 (verified against astro@7.1.2).\n * The generator's `SiteFontSchema` enumerates the same set, so a mismatch is a\n * compile-time-invisible drift — `resolveFonts` throws on an unknown name rather than\n * quietly dropping the family and leaving the token resolving to a fallback stack.\n */\nconst PROVIDERS = new Set(Object.keys(fontProviders));\n\n/**\n * Providers a declaration can construct with no arguments.\n *\n * `adobe` is the one that can't: `fontProviders.adobe({ id })` takes a project API id,\n * and the point of that id being an argument is that it comes from the environment. A\n * JSON declaration has nowhere to put `process.env.ADOBE_ID` without either inlining the\n * key into a committed file or inventing an interpolation syntax, so Adobe families are\n * declared in `astro.config` directly — see the error thrown below.\n */\nconst NEEDS_OPTIONS = new Set(['adobe']);\n\n/**\n * What `<Head />` needs to emit `<Font />` for each family the integration registered.\n *\n * `preload` is passed to the component verbatim: Astro's `PreloadFilter` is\n * `boolean | Array<{weight?, style?, subset?}>`, and the declaration's per-face form is a\n * subset of that — so \"preload the 700 upright only\" survives instead of flattening to\n * \"preload everything\", which would preload the whole family.\n */\nexport interface HeadFont {\n cssVariable: string;\n preload: NonNullable<SiteFont['preload']> | false;\n}\n\nexport interface ResolvedFonts {\n /** Spread into `updateConfig({ fonts })`. Astro's own `FontFamily` shape. */\n fonts: Record<string, unknown>[];\n /** Passed through the virtual head module so `<Head />` can render `<Font />`. */\n head: HeadFont[];\n}\n\n/**\n * Map declarations onto Astro's `fonts:` entries.\n *\n * `preload` is dropped from the Astro entry on purpose — it is not part of the config\n * schema there, it is a prop on the `<Font />` component. It rides along in `head`\n * instead, which is what makes preload a property of the declaration rather than\n * something the consumer hand-picks in their layout.\n */\nexport function resolveFonts(decls: readonly SiteFont[]): ResolvedFonts {\n const fonts: Record<string, unknown>[] = [];\n const head: HeadFont[] = [];\n const seen = new Set<string>();\n\n for (const decl of decls) {\n if (!PROVIDERS.has(decl.provider))\n throw new Error(\n `[vitops] fonts: unknown provider \"${decl.provider}\" for \"${decl.name}\". ` +\n `Astro provides: ${[...PROVIDERS].sort().join(', ')}.`,\n );\n if (NEEDS_OPTIONS.has(decl.provider))\n throw new Error(\n `[vitops] fonts: provider \"${decl.provider}\" needs an API id from your environment ` +\n `(\"${decl.name}\"), and a JSON declaration has nowhere to hold one. Declare this family ` +\n `in astro.config instead — fonts: [{ provider: fontProviders.${decl.provider}` +\n `({ id: process.env.ADOBE_ID }), name: '${decl.name}', cssVariable: '${decl.cssVariable}' }] — ` +\n 'and render its <Font /> yourself; the design-system token can still point at it.',\n );\n if (!decl.cssVariable.startsWith('--'))\n throw new Error(\n `[vitops] fonts: cssVariable \"${decl.cssVariable}\" (${decl.name}) must start with \"--\".`,\n );\n // Two families on one variable is the documented failure of Astro's Fonts API:\n // it warns and the last one wins, so the first family silently never loads.\n if (seen.has(decl.cssVariable))\n throw new Error(\n `[vitops] fonts: two families both declare ${decl.cssVariable} ` +\n `(the second is \"${decl.name}\"). Astro resolves one family per variable — ` +\n `give them separate variables, or drop one.`,\n );\n seen.add(decl.cssVariable);\n\n const { provider, preload, ...rest } = decl;\n fonts.push({\n ...rest,\n provider: (fontProviders as unknown as Record<string, () => unknown>)[provider]!(),\n });\n head.push({ cssVariable: decl.cssVariable, preload: preload ?? false });\n }\n\n return { fonts, head };\n}\n","/**\n * Reconciles `site.locales`/`site.defaultLocale` against Astro's own `i18n`\n * config block.\n *\n * The two are a second, currently-disconnected locale list — EmDash reads\n * locales straight from `astro.config.mjs`'s `i18n` block (see AGENTS.md's\n * \"i18n is Astro-config-driven, not `Config`-driven\" note), while our own\n * SEO/legal code reads `site.locales`/`defaultLocale`. Nothing here merges\n * them into one source: `generate()`/`resolveConfig()` stay pure and\n * filesystem-only (see AGENTS.md's \"Config records facts\" framing — teaching\n * the generator to parse a consumer's `astro.config.mjs` would cost that),\n * so this only says when the two disagree, at the one layer that already\n * legitimately reads both — the Astro integration's `astro:config:setup`\n * hook, which has Astro's resolved `config.i18n` in hand alongside whatever\n * `site.json` `readSiteAds`/`readSiteConsentTracking` beside it already read.\n *\n * A **warning**, not the hard error `consentTrackingConflicts` throws for a\n * consent contradiction: plenty of legitimate sites configure `site.locales`\n * (for `<Seo>`/legal prose) with no Astro i18n routing at all, and the\n * reverse — i18n routing with no `site`/`css` option wired up here — is\n * equally legitimate. Either absence is silence, not a mismatch.\n */\n\n/** Astro's own `i18n` config shape, narrowed to what this needs. */\nexport interface AstroI18nLike {\n defaultLocale: string;\n locales: (string | { path: string; codes: string[] })[];\n}\n\nfunction codesOf(entry: string | { path: string; codes: string[] }): string[] {\n return typeof entry === 'string' ? [entry] : entry.codes;\n}\n\n/** Every BCP-47 code Astro's `i18n` block actually recognises. */\nexport function astroLocaleCodes(i18n: AstroI18nLike): Set<string> {\n return new Set(i18n.locales.flatMap(codesOf));\n}\n\n/**\n * Warning strings for a mismatch — `[]` when either side is absent (nothing\n * to reconcile) or the two agree.\n */\nexport function localeConsistencyWarnings(\n site: { locales: Record<string, unknown>; defaultLocale: string } | null | undefined,\n i18n: AstroI18nLike | undefined,\n): string[] {\n if (!site || !i18n) return [];\n const warnings: string[] = [];\n const astroCodes = astroLocaleCodes(i18n);\n const siteCodes = new Set(Object.keys(site.locales));\n const onlyInSite = [...siteCodes].filter((c) => !astroCodes.has(c));\n const onlyInAstro = [...astroCodes].filter((c) => !siteCodes.has(c));\n\n if (onlyInSite.length || onlyInAstro.length)\n warnings.push(\n `i18n: site.locales and astro.config.mjs's i18n.locales disagree` +\n (onlyInSite.length\n ? ` — site.locales has ${onlyInSite.map((c) => `\"${c}\"`).join(', ')} that astro's i18n doesn't`\n : '') +\n (onlyInAstro.length\n ? `${onlyInSite.length ? ', and' : ' —'} astro's i18n has ${onlyInAstro.map((c) => `\"${c}\"`).join(', ')} that site.locales doesn't`\n : '') +\n `. EmDash reads locales from astro.config.mjs directly, so a locale missing there never ` +\n `reaches its admin regardless of what site.locales says.`,\n );\n if (i18n.defaultLocale !== site.defaultLocale)\n warnings.push(\n `i18n: astro.config.mjs's i18n.defaultLocale (\"${i18n.defaultLocale}\") does not match ` +\n `site.defaultLocale (\"${site.defaultLocale}\") — SEO/legal prose renders in one locale by ` +\n `default while EmDash's own routing defaults to the other.`,\n );\n return warnings;\n}\n","/**\n * Resolves `site.seo` into the `GetvitopsSeoOptions` shape `<Seo />` consumes.\n *\n * The site config is the single source of site-level SEO defaults (ADR-0002); this is the one place\n * its `Localizable` values become plain strings for a build locale. `vitops({ seo })` stays as an\n * override merged over the result, so an app can still set one field in code.\n *\n * Pure — it reads a parsed config and returns JSON-serialisable data (the result is\n * `JSON.stringify`d into `virtual:getvitops/head`). `import type` only, for the same reason\n * `config-adapters.ts` does it: nothing here may drag the generator into a Worker bundle.\n */\nimport type { Config } from '@getvitops/generator';\nimport { t } from '@getvitops/utils/content';\nimport { resolveOrganizationName } from './routes/config-adapters.ts';\nimport type { GetvitopsSeoOptions } from './seo.ts';\n\nexport interface ResolveSeoDefaultsOptions {\n /** Build locale. Default `site.defaultLocale`; a value with no entry falls back to the default. */\n locale?: string;\n /** Which `site.environments` entry to read `robots` from. Default `'production'`. */\n env?: string;\n}\n\n/** Drop `undefined`/empty-string keys so the result carries only what the config actually states. */\nfunction compact<T extends Record<string, unknown>>(value: T): T {\n return Object.fromEntries(\n Object.entries(value).filter(([, v]) => v !== undefined && v !== ''),\n ) as T;\n}\n\nexport function resolveSeoDefaults(\n config: Config,\n options: ResolveSeoDefaultsOptions = {},\n): GetvitopsSeoOptions {\n const seo = config.site.seo;\n const defaultLocale = config.site.defaultLocale;\n const locale = options.locale ?? defaultLocale;\n const text = (value: Parameters<typeof t>[0]) => t(value, locale, defaultLocale) || undefined;\n\n // The same fact `vitops robots` and `search notify` read, so a staging page can't say `index`\n // while its robots.txt says `Disallow`.\n const robots = config.site.environments?.[options.env ?? 'production']?.robots ?? seo?.robots;\n\n const image = seo?.openGraph?.image;\n const openGraph = compact({\n type: seo?.openGraph?.type,\n locale: seo?.openGraph?.locale,\n localeAlternates: seo?.openGraph?.localeAlternates,\n image:\n image === undefined\n ? undefined\n : typeof image === 'string'\n ? { url: image }\n : compact({\n url: image.src,\n alt: image.alt,\n width: image.width,\n height: image.height,\n type: image.type,\n }),\n });\n const twitter = compact({\n card: seo?.twitter?.card,\n site: seo?.twitter?.site,\n creator: seo?.twitter?.creator,\n });\n\n return compact({\n siteName: text(seo?.siteName) ?? resolveOrganizationName(config, locale),\n titleTemplate: text(seo?.titleTemplate),\n defaultTitle: text(seo?.defaultTitle),\n defaultDescription: text(seo?.defaultDescription),\n robots,\n googleSiteVerification: seo?.googleSiteVerification,\n bingSiteVerification: seo?.bingSiteVerification,\n ...(Object.keys(openGraph).length ? { openGraph } : {}),\n ...(Object.keys(twitter).length ? { twitter } : {}),\n }) as GetvitopsSeoOptions;\n}\n\n/**\n * Merge `vitops({ seo })` over config-derived defaults. The two nested objects merge key by key, so\n * setting `openGraph.locale` in code doesn't discard the config's fallback image.\n */\nexport function mergeSeoDefaults(\n fromConfig: GetvitopsSeoOptions,\n override: GetvitopsSeoOptions | undefined,\n): GetvitopsSeoOptions {\n if (!override) return fromConfig;\n const merged: GetvitopsSeoOptions = { ...fromConfig, ...override };\n if (fromConfig.openGraph || override.openGraph)\n merged.openGraph = { ...fromConfig.openGraph, ...override.openGraph };\n if (fromConfig.twitter || override.twitter)\n merged.twitter = { ...fromConfig.twitter, ...override.twitter };\n return merged;\n}\n","/**\n * @getvitops/astro — the Astro integration. One entry wires the design system's\n * `<head>` contributions into a consumer site:\n * - favicons + PWA manifest (generated into public/),\n * - the web-component runtime bundles (copied into public/vitops/),\n * - (opt-in) the design-system CSS, generated + auto-injected (no manual import).\n * The `<Head />` component (shipped alongside) renders the tags, reading resolved\n * config from the `virtual:getvitops/head` module this integration provides.\n */\nimport { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, join, relative, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { LegalOutput, SiteFont, StylesheetFormat } from '@getvitops/generator';\nimport { isConfig, renderRobotsTxt, resolveConfig } from '@getvitops/generator';\nimport {\n type FaviconLink,\n collectIconRefs,\n faviconLinks,\n generateFavicons,\n generateIconInclude,\n resolveIcon,\n scanFiles,\n writeFaviconManifest,\n} from '@getvitops/utils';\n// Aliased: this module's own default export is `vitops` (the integration).\nimport vitopsPlugin from '@getvitops/vite';\n// Type only — the encoder itself is loaded by the Vite plugin, in the pass that\n// runs it, so a project without a `media` option never reaches ffmpeg.\nimport type { OutputKind as MediaOutputKind } from '@getvitops/utils/media';\nimport { REVIEWS_CACHE_PATH, readCache as readReviewsCache } from '@getvitops/utils/reviews';\nimport type { AstroIntegration } from 'astro';\nimport {\n consentCategories,\n type GetvitopsAnalyticsOptions,\n type GetvitopsConsentOptions,\n type OptionalConsentCategory,\n resolveAnalytics,\n} from './analytics.ts';\nimport { type GetvitopsAdsOptions, resolveAds } from './ads.ts';\nimport { type HeadFont, resolveFonts } from './fonts.ts';\nimport { localeConsistencyWarnings } from './i18n.ts';\nimport type { GetvitopsSeoOptions } from './seo.ts';\nimport { mergeSeoDefaults, resolveSeoDefaults } from './seo-defaults.ts';\nimport { TRACKING_ENDPOINT, type GetvitopsTrackingOptions, resolveTracking } from './tracking.ts';\n\nexport interface GetvitopsFaviconOptions {\n /** Source SVG or PNG. */\n source: string;\n lowResSource?: string;\n /** App name → enables `site.webmanifest`. */\n name?: string;\n /** PWA theme color (also `<meta name=\"theme-color\">`). */\n themeColor?: string;\n backgroundColor?: string;\n}\n\n/**\n * Video encoding. A sibling of `favicon`, not a widening of `css`.\n *\n * Mirrors `VitopsMediaOptions` in `@getvitops/vite`, which is where it runs — see\n * the `media` field on `GetvitopsOptions` for why.\n */\nexport interface GetvitopsMediaOptions {\n /** Directory of unprocessed video, walked recursively. Default: `'raw'`. */\n raw?: string;\n /** Where encoded outputs go. Default: `'src/assets/processed'`. */\n out?: string;\n /** Cap the output width, keeping aspect ratio. Default 1920; 0 disables. */\n maxWidth?: number;\n /** Quality on VP9's CRF scale (0–63, lower is better). Default 32. */\n crf?: number;\n /** Optional bitrate ceiling, e.g. `'2M'`. Absent means constant quality. */\n maxBitrate?: string;\n /** Keep the audio track. Default false — the common case is a muted loop. */\n audio?: boolean;\n /** Timestamp (seconds) the poster frame comes from. Default 0. */\n posterTime?: number;\n /** Which outputs to produce. Default: webm, mp4 and a poster. */\n outputs?: MediaOutputKind[];\n /** Cache file. Default `.vitops/media-manifest.json`. */\n manifest?: string;\n}\n\nexport interface GetvitopsCssOptions {\n /**\n * Path to the config (default `'design-system.json'`).\n *\n * May be a `design-system.json` **or** the larger site config that embeds one\n * (`company.json` / `site.json`) — they are told apart by shape. Pointing this\n * at a site config makes the top-level `site` option redundant and lets\n * `legal` and `fonts` default their own `input` to it, so the path is declared\n * once.\n */\n input?: string;\n /**\n * Which `designSystem.themes` entry to build, when `input` is a site config.\n * Default: the config's `defaultTheme`, else `default`.\n */\n theme?: string;\n /**\n * Output format (default 'tailwind'). Narrower than the generator's `Format`\n * on purpose: this option exists to produce a stylesheet to inject, and the\n * `design` format emits only `DESIGN.md`. Run that one via the CLI\n * (`vitops generate --format design --out .`).\n */\n format?: StylesheetFormat;\n /** Directory the generated CSS is written to (default 'src/styles'). */\n out?: string;\n /**\n * Auto-inject the generated stylesheet into every SSR page (default true).\n * Set false when other integrations add routes that must not inherit the\n * design system (e.g. EmDash's `/_emdash/admin`) — then import the generated\n * file (`<out>/tailwind.css` or `<out>/styles.css`) from your site layout so\n * only your own pages (and previews rendered through them) are styled.\n */\n inject?: boolean;\n /**\n * Also flip to dark when the visitor's OS asks and they have made no explicit\n * choice — `@media (prefers-color-scheme: dark)`.\n *\n * Off by default: turning it on flips the site dark for dark-OS visitors, which\n * is a visible change to an existing site. Turning it **on** is what makes\n * `<wc-color-scheme-toggle>`'s \"System\" position do anything (System removes the\n * theme attribute, so without this block it falls through to light), and it is\n * the only way a no-JS page gets the OS appearance at all.\n *\n * The site config's `designSystem.defaultColorScheme: \"system\"` says the same thing and is the\n * better home for it when you have one — this option wins if both are set, and\n * exists because requiring a whole `Config` to set one boolean would be out\n * of proportion.\n */\n systemColorScheme?: boolean;\n}\n\nexport interface GetvitopsSiteOptions {\n /** Path to the site config (JSON). */\n input: string;\n /** Environment whose A/B variant applies (default 'production'). */\n siteEnv?: string;\n}\n\nexport interface GetvitopsLegalOptions {\n /**\n * Path to the site config (JSON) the documents are rendered from. Optional\n * when the top-level `site` option is set — it defaults to that file, since\n * they are almost always the same one.\n */\n input?: string;\n /**\n * Where to write them (default 'src/content/legal'). The default is a content\n * collection: the documents are markdown, and a collection is what lets a page\n * render one through your own layout rather than the integration injecting a\n * route (which nothing in this package does).\n */\n out?: string;\n /** Output format (default 'md'). */\n format?: LegalOutput;\n /** Environment whose A/B variant applies (default 'production'). */\n siteEnv?: string;\n}\n\n/** `changefreq`, mirroring @astrojs/sitemap's `ChangeFreq`. */\nexport type GetvitopsSitemapChangeFreq =\n | 'always'\n | 'hourly'\n | 'daily'\n | 'weekly'\n | 'monthly'\n | 'yearly'\n | 'never';\n\n/** One sitemap entry, mirroring @astrojs/sitemap's `SitemapItem`. */\nexport interface GetvitopsSitemapEntry {\n url: string;\n lastmod?: string;\n changefreq?: GetvitopsSitemapChangeFreq;\n priority?: number;\n /** Alternate-language variants. `lang` is required, as upstream requires it. */\n links?: { lang: string; hreflang?: string; url: string }[];\n}\n\n/**\n * Forwarded verbatim to `@astrojs/sitemap`. Deliberately a hand-written subset of\n * its `SitemapOptions` rather than a re-export: aliasing would put an\n * `import … from '@astrojs/sitemap'` — and, transitively via `SitemapItem`, from\n * `sitemap` — into this package's published .d.mts, which cannot resolve for the\n * consumers who don't install the optional peer. Worse, `skipLibCheck` (on by\n * default via astro's base tsconfig) *suppresses* that error, silently degrading\n * the option to `any` instead of failing loudly.\n *\n * Need something not listed here (e.g. `chunks`)? Add `sitemap()` to your own\n * `integrations` array — getvitops detects it and leaves yours in charge.\n */\nexport interface GetvitopsSitemapOptions {\n /** Keep a page out of the sitemap. Receives the page's full absolute URL. */\n filter?: (page: string) => boolean;\n /** Absolute URLs of pages Astro doesn't build but the site serves. */\n customPages?: string[];\n /** Absolute URLs of externally-generated sitemaps to list in the index. */\n customSitemaps?: string[];\n /** Max entries per file before it splits (default 45000). */\n entryLimit?: number;\n /** Filename prefix (default 'sitemap' → `sitemap-index.xml`). */\n filenameBase?: string;\n changefreq?: GetvitopsSitemapChangeFreq;\n lastmod?: Date;\n priority?: number;\n /** Per-entry rewrite; return `undefined` to drop the entry. */\n serialize?: (\n item: GetvitopsSitemapEntry,\n ) => GetvitopsSitemapEntry | Promise<GetvitopsSitemapEntry | undefined> | undefined;\n /** Emit `<xhtml:link rel=\"alternate\">` for localised routes. */\n i18n?: { defaultLocale: string; locales: Record<string, string> };\n /** XSL stylesheet to prettify the XML (path relative to `site`, or absolute URL). */\n xslURL?: string;\n /** XML namespaces to exclude (all included by default). */\n namespaces?: { news?: boolean; xhtml?: boolean; image?: boolean; video?: boolean };\n}\n\nexport interface GetvitopsOptions {\n /**\n * Your site config — the one place to name it.\n *\n * A `Config` records site-level facts, several of which other options here\n * need: `designSystem.defaultColorScheme` decides whether the generated colour layer\n * carries a `prefers-color-scheme` block, `fonts` can supply the webfont\n * declarations, and `legal` renders from it. Set this and each of those reads\n * it, rather than repeating the path per feature. Every one can still be given\n * explicitly, which wins.\n *\n * Needs `css`, since the generation runs in the Vite plugin `css` registers.\n */\n site?: GetvitopsSiteOptions;\n favicon?: GetvitopsFaviconOptions;\n /**\n * Encode raw video into WebM + an MP4 fallback + a poster frame.\n *\n * Import the results like any other asset, so Vite content-hashes them:\n *\n * ```astro\n * import hero from '../assets/processed/hero.webm';\n * import poster from '../assets/processed/hero.jpg';\n * <video poster={poster.src} autoplay muted loop playsinline>\n * <source src={hero} type=\"video/webm\" />\n * </video>\n * ```\n *\n * Cached on source content and encode settings, so a rebuild costs a hash per\n * file rather than an encode. **Commit the outputs and\n * `.vitops/media-manifest.json`** — ffmpeg output is not reproducible across\n * versions, so re-encoding in CI churns the diff on every toolchain bump.\n *\n * Needs `ffmpeg` on PATH, and fails the build without it rather than skipping:\n * a page referencing a video that was never encoded is broken, not degraded.\n *\n * Needs `css`, since it runs in the Vite plugin `css` registers.\n */\n media?: GetvitopsMediaOptions;\n /** Copy + link the web-component bundles (default true). */\n webComponents?: boolean;\n /** Generate + auto-inject the design-system CSS. Off unless provided. */\n css?: GetvitopsCssOptions;\n /**\n * Ship the live theme editor (default false).\n *\n * Copies `editor.js` into `public/vitops/`, loads it from `<Head />`, and mirrors\n * the generated `design-manifest.json` next to it so `<wc-theme-editor>` resolves\n * its default `/vitops/design-manifest.json`. Requires `css` (the manifest is a\n * `css`-format output) — place the `<wc-theme-editor>` tag in your layout yourself.\n *\n * Editing is live on any deploy; **saving back** to `design-system.json` needs the\n * dev server, so on a static build that button simply isn't rendered.\n */\n editor?: boolean;\n /**\n * Ship what a host needs to embed `<wc-theme-editor>`, WITHOUT loading it from `<Head />`\n * (default false). Writes into `public/vitops/`: `design-manifest.json`, `editor.js`, a\n * standalone `editor.css`, and `design-system-editor.html`, a page that mounts the editor.\n *\n * For hosts outside the site's own pages, such as the EmDash admin\n * (`@getvitops/emdash`'s `designSystemEditor`, which iframes that page so the design\n * system never leaks into the admin's styles). Public pages don't pay for the bundle.\n * Requires `css`.\n */\n editorManifest?: boolean;\n /**\n * Render the site's legal documents (privacy policy, terms, cookie notice)\n * from a site config, and re-render when it changes. Off unless provided.\n *\n * Requires `css`, which is what registers the Vite plugin that runs the\n * generation — without it the documents would be written once and never\n * refresh. Not using `css`? Run `vitops legal` from the CLI instead; it is the\n * same renderer and works in any stack.\n */\n legal?: GetvitopsLegalOptions;\n /**\n * Load webfonts through Astro's Fonts API and emit `<Font />` for each from\n * `<Head />`. Off unless provided.\n *\n * This is the seam the design system deliberately lacks. `design-system.json`'s\n * `fonts` block holds **stacks only** — `--font-<name>` tokens and nothing else: no\n * `@font-face`, no preload, no metrics-matched fallback. Declaring the family here\n * makes the token resolvable *and* loaded, so the token keeps pointing at a\n * `cssVariable` rather than at a literal stack the browser has no file for:\n *\n * ```js\n * vitops({ fonts: [{ name: 'League Spartan', provider: 'fontsource',\n * cssVariable: '--font-league-spartan',\n * weights: ['100 900'], subsets: ['latin'], preload: true }] })\n * ```\n * ```jsonc\n * // design-system.json\n * \"fonts\": { \"display\": \"var(--font-league-spartan), sans-serif\" }\n * ```\n *\n * Pass a **string** instead to read the array from a site config's `fonts` block —\n * the same declarations, kept next to the rest of the site's facts. (`siteEnv`\n * picks the A/B variant, as in `legal`.) With the top-level `site` option set,\n * plain `true` does the same thing without repeating the path.\n *\n * Independent of `css`: the loading runs in `astro:config:setup`, not in the Vite\n * plugin. Both `<Head />` and the config entry are required — Astro's `fonts:`\n * config resolves the files, and `<Font />` is what puts the `@font-face` on the\n * page — so a declaration without `<Head />` in your layout loads nothing.\n */\n fonts?: boolean | string | SiteFont[] | GetvitopsFontsOptions;\n /**\n * Emit `sitemap-index.xml` + `sitemap-0.xml` via `@astrojs/sitemap`, and link it\n * from `<Head />`. Off unless provided; `true` uses its defaults.\n *\n * Needs `@astrojs/sitemap` (an optional peer — install it yourself) and the\n * `site` astro.config option, since a sitemap lists absolute URLs.\n *\n * Skipped with a warning when `emdash()` is registered — EmDash serves its own\n * database-driven `/sitemap.xml` — and deferred to when you already list\n * `@astrojs/sitemap` yourself. Note it enumerates **prerendered** routes only,\n * so on-demand pages of an `output: 'server'` site are not listed.\n */\n sitemap?: boolean | GetvitopsSitemapOptions;\n /**\n * Site-level defaults for `<Seo />` (`@getvitops/astro/Seo.astro`) — site name,\n * title template, Open Graph + Twitter defaults, verification tokens.\n *\n * These come from `site.seo` in the site config (with `organization.name` as the site\n * name); this option is an **override** merged over that, field by field, for a value\n * an app wants to set in code. Per-page props override both.\n *\n * Needs the `site` astro.config option for canonical and `og:url`; without it\n * `<Seo />` omits every absolute-URL tag rather than deriving one from the\n * request, and this warns at build time.\n *\n * On an EmDash site prefer `<EmDashHead>`, which covers the same tags from the\n * CMS — using both double-emits them.\n */\n seo?: GetvitopsSeoOptions;\n /**\n * Resolve semantic icon names centrally and, on a server build, derive which\n * icons to bundle by scanning your source. Off unless provided; `true` uses\n * the defaults below.\n *\n * Two separate jobs, and it is worth knowing which one you are buying:\n *\n * 1. **Naming.** `<Icon name=\"menu\" />` resolves through the semantic map, so\n * the icon set is a config value rather than something spelled out at every\n * call site. A name containing `:` passes through untouched — that is the\n * escape hatch for a set-specific glyph.\n * 2. **Bundle size.** astro-icon is zero-config on a static build, but under\n * `output: 'server'` it bundles EVERY icon in a set unless given an\n * `include` map. That is the list consumers end up hand-maintaining. Here it\n * is derived by scanning `scan` for icon references, merged with anything\n * you declare. On a static build no `include` is passed at all, because\n * there is nothing to trim.\n *\n * Names the scanner cannot read statically (`<Icon name={expr} />`) are\n * reported with file and line, not guessed at — declare them in `include`.\n */\n icons?: boolean | GetvitopsIconsOptions;\n /**\n * Analytics tags for `<Analytics />` — Google Analytics, Microsoft Clarity,\n * Matomo, Plausible. Off unless provided.\n *\n * Every tag loads off the critical path (`strategy`, default `'idle'`: after\n * `load`, on an idle callback) and, when it sets cookies, only after consent.\n *\n * Which consent category a provider needs is derived from whether it sets\n * cookies, not declared — so configuring a cookie-setting provider without\n * `consent` warns rather than silently tracking everyone.\n */\n analytics?: GetvitopsAnalyticsOptions;\n /**\n * The consent gate: `@getvitops/core/consent` + `<CookieConsent />`. Off unless\n * provided; `true` uses its defaults.\n *\n * A sibling of `analytics`, not part of it. The gate is general — anything\n * marked `data-consent=\"<category>\"` waits on the same choice, so A/B\n * assignment, personalisation and third-party embeds use it too, and a site can\n * enable it with no analytics configured at all.\n *\n * Pair it with `legal` so the cookie notice describes the categories this\n * actually offers.\n */\n consent?: boolean | GetvitopsConsentOptions;\n /**\n * Ad-click attribution for `<Tracking />` — capture click IDs and UTMs from the\n * landing URL into the `_ac` cookie, so a conversion can be attributed to the\n * ad that produced it. Off unless provided.\n *\n * `_ac` is a 90-day identifier, so writing it waits on consent (`marketing` by\n * default) — and **asking for that permission is what raises the banner**, on\n * the arrival that carried a click ID and on no other. A visitor who arrives\n * organically has nothing to attribute and is never asked.\n *\n * Read the cookie back with `parseTrackingCookie` from\n * `@getvitops/utils/tracking`, and turn a conversion into a notification with\n * `@getvitops/utils/notify`.\n */\n tracking?: boolean | GetvitopsTrackingOptions;\n}\n\nexport interface GetvitopsFontsOptions {\n /** Path to a site config (JSON) whose `fonts` array holds the declarations. */\n input?: string;\n /** Declarations written inline; merged after anything `input` supplies. */\n families?: SiteFont[];\n /** Environment whose A/B variant applies to `input` (default 'production'). */\n siteEnv?: string;\n}\n\nexport interface GetvitopsIconsOptions {\n /** Icon set for UI chrome. Default `'fa7-solid'`. */\n ui?: string;\n /** Icon set for brand marks. Default `'simple-icons'`. */\n brand?: string;\n /**\n * Weight for suffix-weighted sets like Phosphor (`'bold'`, `'fill'`, …).\n * Ignored by sets that split weights across collections, e.g. Font Awesome.\n */\n weight?: string;\n /**\n * Icons to bundle, in `generateIconInclude`'s shape:\n * `{ semantic: ['menu'], 'simple-icons': ['zoho'] }`.\n *\n * Merged with what the scan finds. Declared names that don't resolve **throw**\n * (a config error); scanned names that don't resolve only **warn** (a source\n * typo shouldn't kill the dev server).\n */\n include?: Parameters<typeof generateIconInclude>[0];\n /**\n * How `<Icon />` draws the glyph.\n *\n * `'inline'` (default) reads it from the `@iconify-json/*` collection and\n * inlines the `<svg>` — no runtime request, no icon-integration peer.\n * `'sprite'` emits `<use href=\"…/icons.svg#id\">` instead, for pages served\n * where the sprite is already on the origin.\n *\n * Note this is separate from `register`: whether astro-icon is registered for\n * YOUR OWN `<Icon>` usage is a different question from how ours renders.\n */\n engine?: 'inline' | 'sprite';\n /**\n * Register the icon integration for you (default true). Set false when you\n * already list `icon()` in `integrations` yourself — the scan, the sprite and\n * the resolver still run either way.\n */\n register?: boolean;\n /** Directories to scan, relative to the project root. Default `['src']`; `false` disables. */\n scan?: string[] | false;\n /** Public href of the sprite, for the `'sprite'` engine. Default `'/vitops/icons.svg'`. */\n sprite?: string;\n /** Aliases applied before the semantic map — legacy names, or values stored in CMS content. */\n overrides?: Record<string, string>;\n}\n\n/**\n * What `<Tracking />` emits — pulled out to its own named type (rather than\n * inline in `HeadData`, which `head.test.ts`'s regex-based drift guard reads\n * line-by-line) purely so the formatter's line-wrapping doesn't turn each of\n * its fields into a false top-level `HeadData` key match.\n */\ninterface HeadTrackingData {\n enabled: boolean;\n category: string;\n cookies: string[];\n /** First enabled `site.experiments` entry's cookie name — see tracking.ts. */\n abVariantCookie: string | null;\n}\n\n/**\n * Serialised into the virtual module with `JSON.stringify`, so every field here\n * must be JSON-representable — no functions. (That is why the sitemap's *href*\n * lives here and its options, which carry `filter`/`serialize`, do not.)\n */\ninterface HeadData {\n favicons: boolean;\n faviconLinks: FaviconLink[];\n themeColor: string | null;\n webComponents: boolean;\n wcBase: string;\n editor: boolean;\n /** `<link rel=\"sitemap\">` target, or null when no sitemap was registered. */\n sitemap: string | null;\n /**\n * Families registered by `vitops({ fonts })`, for `<Font />`. Only ours: a\n * `cssVariable` Astro cannot resolve makes `<Font />` throw, so this must never\n * include a family declared elsewhere (a user-level `fonts:`, or EmDash's).\n */\n fonts: HeadFont[];\n /** `config.site`, so `<Seo />` has a canonical base even without `Astro.site`. */\n site: string | null;\n /** Site-level `<Seo />` defaults. Function-free, hence serialisable. */\n seo: GetvitopsSeoOptions;\n /** `<Analytics />` providers. IDs and flags only — function-free. */\n analytics: GetvitopsAnalyticsOptions;\n /** Is the consent gate active? Decides whether tags are gated at all. */\n consent: boolean;\n /** Categories `<CookieConsent />` offers unless a page overrides them. */\n consentCategories: OptionalConsentCategory[];\n /** Cookie-notice URL for the banner, or null. */\n consentPolicyUrl: string | null;\n /** Does `<Head />` need to load `consent.js`? (gate enabled, or a tag needs scheduling) */\n consentRuntime: boolean;\n /**\n * What `<Tracking />` emits, or null when tracking is off. Resolved here rather\n * than in the component because its warnings belong beside the others, and the\n * offered consent categories it checks against are known only at this point.\n */\n /** `abVariantCookie` is the first enabled `site.experiments` entry's cookie name — see tracking.ts. */\n tracking: HeadTrackingData | null;\n /** Enabled `site.experiments`, keyed the same way, each resolved to its cookie name — for `<Analytics />`'s reporting hook. */\n experiments: Record<string, { cookieName: string }>;\n /** `site.analytics.matomo.abVariantDimensions` — passed through for the same reporting hook. */\n matomoAbVariantDimensions: Record<string, number>;\n /**\n * What `<Ads />` emits: the `site.ads` properties plus whether this environment\n * fires them at all. Read from the site config here rather than in the component\n * because that is where the config is already parsed, and because the offered\n * consent categories must know a `marketing` demand exists before the banner is\n * built.\n */\n ads: { enabled: boolean; properties: GetvitopsAdsOptions } | null;\n /**\n * `site.security.turnstile.siteKey` (public), for form blocks that render the\n * Turnstile widget. Null when none is configured — a form then renders with no\n * widget and no third-party reference at all.\n */\n turnstileSiteKey: string | null;\n}\n\n/**\n * Which providers `vitops({ analytics })` configures that the site config's\n * own `analytics` block does not.\n *\n * The two are separate surfaces on purpose — the integration must not import\n * `Config` — but they describe the same site, and a disagreement between them\n * is a compliance defect rather than a style issue: `vitops legal` derives the\n * cookie notice from the site config, so a provider missing there is a tag the\n * site runs and its own notice never mentions. That is precisely what the\n * generator's processor table exists to prevent.\n *\n * Read is best-effort — the `legal` step already reports an unreadable config,\n * and a second copy of that error helps nobody.\n */\nconst SITE_CONFIG_KEYS: Record<string, string> = {\n googleAnalytics: 'googleAnalyticsId',\n clarity: 'clarityId',\n matomo: 'matomo',\n plausible: 'plausibleDomain',\n};\n\nfunction undisclosedProviders(configPath: string, analytics: GetvitopsAnalyticsOptions): string[] {\n let declared: Record<string, unknown>;\n try {\n // `site.analytics`, matching the three-section config. Reading the wrong path\n // here does not fail quietly in the harmless direction: an empty `declared`\n // makes *every* configured provider look undisclosed, so the warning would\n // fire on correct configs and stop being worth reading.\n const parsed = JSON.parse(readFileSync(configPath, 'utf8')) as {\n site?: { analytics?: unknown };\n };\n declared = (parsed.site?.analytics ?? {}) as Record<string, unknown>;\n } catch {\n return [];\n }\n return Object.keys(SITE_CONFIG_KEYS).filter(\n (key) =>\n analytics[key as keyof GetvitopsAnalyticsOptions] !== undefined &&\n !declared[SITE_CONFIG_KEYS[key] as string],\n );\n}\n\n/**\n * Read `site.locales`/`site.defaultLocale` out of the site config, for\n * `localeConsistencyWarnings` to check against Astro's own `i18n` block.\n *\n * Best-effort, matching `readSiteAds`'s shape exactly: a malformed or missing\n * config is silence here, since every other consumer of this file already\n * validates it loudly elsewhere in the same hook.\n */\nfunction readSiteLocales(\n configPath: string,\n siteEnv: string,\n): { locales: Record<string, unknown>; defaultLocale: string } | null {\n try {\n const cfg = resolveConfig(JSON.parse(readFileSync(configPath, 'utf8')), siteEnv);\n return { locales: cfg.site.locales, defaultLocale: cfg.site.defaultLocale };\n } catch {\n return null;\n }\n}\n\n/**\n * Resolve `site.seo` (plus `organization.name` as the site-name fallback) out of the site config.\n *\n * Best-effort, like `readSiteLocales`: a malformed or missing config is silence here, since every\n * other consumer of the file validates it loudly in the same hook. An empty object means \"the\n * config states no SEO defaults\", which is also what no site config at all means.\n */\nfunction readSiteSeo(configPath: string, siteEnv: string): GetvitopsSeoOptions {\n try {\n return resolveSeoDefaults(\n resolveConfig(JSON.parse(readFileSync(configPath, 'utf8')), siteEnv),\n {\n env: siteEnv,\n },\n );\n } catch {\n return {};\n }\n}\n\n/**\n * Read `site.ads` and this environment's ads switch out of the site config.\n *\n * Best-effort, like `undisclosedProviders`: every other consumer of this file\n * validates it loudly elsewhere in the same hook, and a second copy of that error\n * helps nobody. `resolveConfig` is used rather than a bare `JSON.parse` so an A/B\n * variant's `overrides` are applied first — a variant that changes which pixels\n * run is exactly the kind of thing overrides exist for.\n *\n * The environment switch cascades `ads` → `analytics` → true. An environment that\n * has already said \"no analytics here\" has said the interesting half of it; making\n * a consumer repeat themselves to stop conversion pixels would mean a preview\n * deploy fires them by default, which is the wrong way for this to fail.\n */\nfunction readSiteAds(\n configPath: string,\n siteEnv: string,\n): { enabled: boolean; properties: GetvitopsAdsOptions } | null {\n try {\n const cfg = resolveConfig(JSON.parse(readFileSync(configPath, 'utf8')), siteEnv);\n const properties = cfg.site.ads;\n if (!properties || Object.keys(properties).length === 0) return null;\n const env = cfg.site.environments?.[siteEnv];\n return {\n enabled: env?.ads ?? env?.analytics ?? true,\n properties: properties as GetvitopsAdsOptions,\n };\n } catch {\n return null;\n }\n}\n\n/** Read `site.security.turnstile.siteKey`. Best-effort, like `readSiteAds`. */\nfunction readSiteTurnstileKey(configPath: string, siteEnv: string): string | null {\n try {\n const cfg = resolveConfig(JSON.parse(readFileSync(configPath, 'utf8')), siteEnv);\n return cfg.site.security?.turnstile?.siteKey || null;\n } catch {\n return null;\n }\n}\n\n/**\n * Read `site.experiments` + `site.analytics.matomo.abVariantDimensions` out of\n * the site config, resolving each experiment's cookie name (default\n * `_ab_<key>`, matching `vitopsAbTesting()`'s own default) so `<Analytics />`\n * doesn't need to know the fallback rule itself.\n *\n * Mirrors `readSiteAds`'s shape: best-effort, only enabled experiments\n * (a disabled one has no cookie to read and shouldn't cost every provider's\n * bootstrap a no-op check), `null` when nothing's configured.\n */\nfunction readSiteExperiments(\n configPath: string,\n siteEnv: string,\n): {\n experiments: Record<string, { cookieName: string }>;\n matomoAbVariantDimensions: Record<string, number>;\n /** The first enabled experiment's cookie name — see tracking.ts's own docblock for why only one. */\n primaryCookie: string | null;\n} | null {\n try {\n const cfg = resolveConfig(JSON.parse(readFileSync(configPath, 'utf8')), siteEnv);\n const configured = cfg.site.experiments;\n if (!configured || Object.keys(configured).length === 0) return null;\n const experiments: Record<string, { cookieName: string }> = {};\n let primaryCookie: string | null = null;\n for (const [key, experiment] of Object.entries(configured)) {\n if (!experiment.enabled) continue;\n const cookieName = experiment.cookieName ?? `_ab_${key}`;\n experiments[key] = { cookieName };\n primaryCookie ??= cookieName;\n }\n if (Object.keys(experiments).length === 0) return null;\n return {\n experiments,\n matomoAbVariantDimensions: cfg.site.analytics?.matomo?.abVariantDimensions ?? {},\n primaryCookie,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Is `site.crm` configured and not explicitly disabled?\n *\n * Mirrors `readSiteAds`'s \"read the config, decide nothing else\" shape — this\n * only answers the one question `hasConversionRoute`'s warning needs, not the\n * whole `CrmConfig`, which the consumer's own route file builds from the same\n * config however it likes.\n */\nfunction siteCrmEnabled(configPath: string, siteEnv: string): boolean {\n try {\n const cfg = resolveConfig(JSON.parse(readFileSync(configPath, 'utf8')), siteEnv);\n return Boolean(cfg.site.crm && cfg.site.crm.enabled !== false);\n } catch {\n return false;\n }\n}\n\n/**\n * The consent + tracking facts a site config already states.\n *\n * These were the one pair the integration did not read. `site.tracking` and\n * `site.legal.cookieConsent` drive the **generated cookie notice**, while\n * `vitops({ consent, tracking })` drove the **runtime** — two hand-synced\n * declarations of the same fact with nothing checking them against each other.\n * The failure that produces is precisely the one the rest of the toolchain is\n * fastidious about avoiding: a site can ship a cookie notice naming categories\n * its banner never offers, or promising attribution that no capture script\n * implements, and the build is clean. That is a compliance defect, not a\n * documentation gap.\n *\n * Read as DEFAULTS, the same as `css` / `fonts` / `favicon` / `ads` — the\n * config is where a site's facts live, and an integration option is how you\n * override one. A genuine contradiction is an error rather than a warning; see\n * `consentTrackingConflicts`.\n */\nfunction readSiteConsentTracking(\n configPath: string,\n siteEnv: string,\n): { tracking?: GetvitopsTrackingOptions; consentEnabled?: boolean; categories?: string[] } | null {\n try {\n const cfg = resolveConfig(JSON.parse(readFileSync(configPath, 'utf8')), siteEnv);\n const t = cfg.site.tracking;\n const cc = cfg.site.legal?.cookieConsent;\n if (!t && !cc) return null;\n return {\n ...(t ? { tracking: t as GetvitopsTrackingOptions } : {}),\n ...(cc ? { consentEnabled: cc.enabled } : {}),\n ...(cc?.categories ? { categories: cc.categories } : {}),\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Contradictions between the config and an explicit integration option.\n *\n * Absent-vs-present is a default and resolves silently. `true` vs `false` on the\n * same fact is not resolvable — one of the two documents is wrong, and picking\n * either quietly is how a notice and a banner get out of step. The message names\n * both sides so the fix does not require guessing which one won.\n */\nexport function consentTrackingConflicts(\n site: { tracking?: { enabled?: boolean | undefined } | undefined; consentEnabled?: boolean },\n opts: { consent?: unknown; tracking?: unknown },\n configPath: string,\n): string[] {\n const out: string[] = [];\n const optConsent = opts.consent === undefined ? undefined : !!opts.consent;\n if (\n site.consentEnabled !== undefined &&\n optConsent !== undefined &&\n site.consentEnabled !== optConsent\n )\n out.push(\n `consent: ${configPath} sets site.legal.cookieConsent.enabled: ${site.consentEnabled}, ` +\n `but vitops({ consent: ${optConsent} }) says the opposite. The generated cookie notice ` +\n `follows the config and the banner follows the option, so one of them would describe a ` +\n `site that does not exist. Set them the same, or drop the option and let the config decide.`,\n );\n const optTracking =\n opts.tracking === undefined\n ? undefined\n : opts.tracking === true\n ? true\n : opts.tracking === false\n ? false\n : ((opts.tracking as { enabled?: boolean }).enabled ?? true);\n if (\n site.tracking?.enabled !== undefined &&\n optTracking !== undefined &&\n site.tracking.enabled !== optTracking\n )\n out.push(\n `tracking: ${configPath} sets site.tracking.enabled: ${site.tracking.enabled}, but ` +\n `vitops({ tracking: ${optTracking} }) says the opposite. The cookie notice discloses \\`_ac\\` ` +\n `from the config while the capture script ships from the option — so the site would either ` +\n `disclose a cookie it never sets, or set one it never discloses.`,\n );\n return out;\n}\n\n/**\n * Does a route answer `TRACKING_ENDPOINT`?\n *\n * Checked by filename rather than through Astro's route manifest, because this\n * runs in `astro:config:setup` — before routes are collected. The extension set\n * covers the ways a consumer can author the same endpoint; a false negative\n * costs a warning, a false positive costs the whole point of the check, so this\n * errs toward looking in more places.\n */\nfunction hasConversionRoute(root: string): boolean {\n const base = resolve(root, 'src/pages', TRACKING_ENDPOINT.replace(/^\\//, ''));\n return ['.ts', '.js', '.mjs', '.astro'].some(\n (ext) => existsSync(`${base}${ext}`) || existsSync(join(base, `index${ext}`)),\n );\n}\n\nconst VIRTUAL_ID = 'virtual:getvitops/head';\nconst RESOLVED_ID = `\\0${VIRTUAL_ID}`;\n\nfunction virtualHeadPlugin(data: HeadData) {\n return {\n name: '@getvitops/astro:virtual-head',\n resolveId(id: string) {\n return id === VIRTUAL_ID ? RESOLVED_ID : null;\n },\n load(id: string) {\n return id === RESOLVED_ID ? `export default ${JSON.stringify(data)};` : null;\n },\n };\n}\n\n/**\n * What `<Icon />` needs to resolve a name at render time.\n *\n * JSON only, same rule as `HeadData` — which is precisely why there is no\n * `resolver` field. `resolveIcon` is a pure function in `@getvitops/utils` that\n * the component imports statically; what varies per site is the *data* it takes.\n */\ninterface IconsData {\n engine: 'inline' | 'sprite' | 'none';\n /**\n * Absolute project root, for resolving `@iconify-json/*`.\n *\n * Explicit rather than `process.cwd()`: the collections are a dependency of\n * the SITE, and the cwd is wherever the process happened to start — a task\n * runner invoking astro from a monorepo root resolves nothing and every icon\n * renders as an empty box, with no error to explain it.\n */\n root: string;\n ui: string;\n brand: string;\n weight: string | null;\n overrides: Record<string, string>;\n sprite: string | null;\n}\n\nconst ICONS_ID = 'virtual:getvitops/icons';\nconst RESOLVED_ICONS_ID = `\\0${ICONS_ID}`;\n\nfunction virtualIconsPlugin(data: IconsData) {\n return {\n name: '@getvitops/astro:virtual-icons',\n resolveId(id: string) {\n return id === ICONS_ID ? RESOLVED_ICONS_ID : null;\n },\n load(id: string) {\n return id === RESOLVED_ICONS_ID ? `export default ${JSON.stringify(data)};` : null;\n },\n };\n}\n\nconst REVIEWS_ID = 'virtual:getvitops/reviews';\nconst RESOLVED_REVIEWS_ID = `\\0${REVIEWS_ID}`;\n\n/**\n * The committed `.vitops/reviews.json` cache as a build-time import, for server-rendered\n * components that cannot read the filesystem (the EmDash `vitops.reviews` block runs in a\n * Worker — and, with pages prerendered, inside the Cloudflare adapter's local workerd).\n *\n * **An absent or unusable file serves an EMPTY cache, never an error.** A fresh scaffold has\n * no `.vitops/reviews.json` (nothing has run `vitops reviews` yet) and a build must not fail\n * for it; the block renders an editor placeholder instead. `readCache` already treats a\n * corrupt or wrong-version file as absent, which is the same rule — the failure is silent\n * here only because the consumer of this module reports it.\n *\n * Read in `load`, not at plugin construction, and the file is registered as a watch file, so\n * a dev server picks up a re-run of `vitops reviews` without a restart.\n */\nfunction virtualReviewsPlugin(root: string) {\n const path = resolve(root, REVIEWS_CACHE_PATH);\n return {\n name: '@getvitops/astro:virtual-reviews',\n resolveId(id: string) {\n return id === REVIEWS_ID ? RESOLVED_REVIEWS_ID : null;\n },\n load(this: { addWatchFile?: (id: string) => void } | void, id: string) {\n if (id !== RESOLVED_REVIEWS_ID) return null;\n this?.addWatchFile?.(path);\n const cache = readReviewsCache(path) ?? { version: 1, locations: {} };\n return `export default ${JSON.stringify(cache)};`;\n },\n };\n}\n\n/** Defaults live here so the component and the integration can't drift. */\nconst ICON_DEFAULTS = {\n ui: 'fa7-solid',\n brand: 'simple-icons',\n sprite: '/vitops/icons.svg',\n scan: ['src'],\n} as const;\n\n/**\n * Probe an optional icon integration so it can be registered for the consumer's\n * OWN `<Icon>` usage. Nothing here decides how our `<Icon />` renders — that is\n * `engine`, and it never needs a peer.\n */\n/**\n * Indirection so the bundler can't resolve the specifier statically. See the\n * import site in the `tailwind` branch for why that matters.\n */\nconst TAILWIND_VITE = '@tailwindcss/vite';\n\nasync function probeIconPackage(name: 'astro-icon' | 'astro-iconset'): Promise<boolean> {\n try {\n await import(/* @vite-ignore */ name);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Does the file at `path` hold a site config rather than a bare design system?\n *\n * Swallows a missing or unparseable file and answers `false`: this only decides\n * whether the other options may default their paths to this one, and the real\n * read happens in the Vite plugin, which reports a parse error properly. Failing\n * the build here would turn \"your JSON has a trailing comma\" into an error about\n * an option the consumer never set.\n */\nfunction readsAsConfig(path: string): boolean {\n try {\n return isConfig(JSON.parse(readFileSync(path, 'utf8')));\n } catch {\n return false;\n }\n}\n\n/** Whether the site config sets `site.icons.sprite` — the fact that makes the generator emit icons.svg. */\nfunction siteWantsSprite(root: string, siteInput: string | undefined, siteEnv?: string): boolean {\n if (!siteInput) return false;\n try {\n const cfg = resolveConfig(\n JSON.parse(readFileSync(resolve(root, siteInput), 'utf8')),\n siteEnv ?? 'production',\n );\n return Boolean((cfg.site.icons as { sprite?: boolean } | undefined)?.sprite);\n } catch {\n return false;\n }\n}\n\n/**\n * A standalone page hosting `<wc-theme-editor>`, for a host that cannot load the\n * framework CSS itself (the EmDash admin embeds this in an iframe, so the design\n * system never touches the admin's own styles). `?save=<url>` supplies the save\n * endpoint, which is the host's to name, not this integration's.\n */\nfunction writeEditorHost(dest: string): void {\n mkdirSync(dest, { recursive: true });\n writeFileSync(\n join(dest, 'design-system-editor.html'),\n `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<meta name=\"robots\" content=\"noindex\">\n<title>Design system editor</title>\n<link rel=\"stylesheet\" href=\"/vitops/editor.css\">\n</head>\n<body>\n<script type=\"module\">\n await import('/vitops/editor.js');\n const el = document.createElement('wc-theme-editor');\n el.setAttribute('manifest', '/vitops/design-manifest.json');\n const save = new URLSearchParams(location.search).get('save');\n if (save && save.startsWith('/')) el.setAttribute('save-endpoint', save);\n document.body.append(el);\n</script>\n</body>\n</html>\n`,\n );\n}\n\nexport default function vitops(opts: GetvitopsOptions = {}): AstroIntegration {\n return {\n name: '@getvitops/astro',\n hooks: {\n 'astro:config:setup': async ({ config, updateConfig, injectScript, logger }) => {\n const root = fileURLToPath(config.root);\n const publicDir = fileURLToPath(config.publicDir);\n const webComponents = opts.webComponents !== false;\n const editor = opts.editor === true;\n if ((editor || opts.editorManifest) && !opts.css)\n logger.warn(\n 'editor/editorManifest needs a `css` config — it reads design-manifest.json.',\n );\n if (opts.legal && !opts.css)\n logger.warn(\n 'legal: needs a `css` config — the generation runs in the Vite plugin that `css` ' +\n 'registers. Run `npx vitops legal` instead if you do not use `css`.',\n );\n if (opts.media && !opts.css)\n logger.warn(\n 'media: needs a `css` config — the encoding runs in the Vite plugin that `css` ' +\n 'registers. Run `npx vitops media` instead if you do not use `css`.',\n );\n // Where the site config is, if there is one. `css.input` counts when it\n // is itself a site config — that is the common setup once a consumer\n // keeps their tokens inside `company.json`, and making every option\n // repeat the path is exactly the friction this is meant to remove.\n // The effective path, default included, so the answer matches what the\n // Vite plugin will actually read rather than only covering the case\n // where the option was written out.\n const cssInput = opts.css ? (opts.css.input ?? 'design-system.json') : undefined;\n const siteInput =\n opts.site?.input ??\n (cssInput != null && readsAsConfig(resolve(root, cssInput)) ? cssInput : undefined);\n if (opts.site && !opts.css)\n logger.warn(\n 'site: needs a `css` config — it is read during generation, which runs in the Vite ' +\n 'plugin that `css` registers. Without it the site config changes nothing here.',\n );\n // `legal` only needs its own `input` when it points somewhere other than\n // the site config; the two are almost always the same file.\n const legalInput = opts.legal?.input ?? siteInput;\n if (opts.legal && !legalInput)\n throw new Error(\n '[vitops] legal: needs `legal.input` (a site config path), the top-level `site` ' +\n 'option, or a `css.input` that is itself a site config.',\n );\n // See i18n.ts's own docblock for why this is a warning, not the hard\n // error a consent contradiction gets: either side can legitimately be\n // configured without the other.\n if (siteInput) {\n const siteLocales = readSiteLocales(\n resolve(root, siteInput),\n opts.site?.siteEnv ?? 'production',\n );\n for (const warning of localeConsistencyWarnings(siteLocales, config.i18n))\n logger.warn(warning);\n }\n // `site.seo` is the source; `vitops({ seo })` overrides it field by field (ADR-0002).\n const seoDefaults = mergeSeoDefaults(\n siteInput\n ? readSiteSeo(resolve(root, siteInput), opts.site?.siteEnv ?? 'production')\n : {},\n opts.seo,\n );\n // A config that only supplies the organization name (so a site name) is not \"seo\n // configured\"; warn only when the site actually asked for <Seo /> defaults.\n if (opts.seo || Object.keys(seoDefaults).some((k) => k !== 'siteName')) {\n // Warn once here rather than letting every page silently drop its\n // canonical — a missing absolute URL is invisible in the output.\n if (!config.site)\n logger.warn(\n 'seo: needs the `site` astro.config option (your deployed URL) — without it <Seo /> ' +\n 'omits canonical, og:url and any relative og:image rather than guessing an origin.',\n );\n if (config.integrations.some((i) => i.name === 'emdash'))\n logger.warn(\n 'seo: emdash() is registered, and its <EmDashHead> emits the same title/description/' +\n 'canonical/og tags from the CMS. Use one or the other — rendering both duplicates ' +\n 'every tag.',\n );\n }\n // Analytics + consent are resolved here rather than in the component:\n // resolveAnalytics is pure, its warnings belong beside the others, and\n // step 2 needs to know whether the consent bundle has to be copied.\n //\n // The site config supplies defaults for both, and a genuine\n // contradiction fails the build — see `readSiteConsentTracking`.\n const siteConsent = siteInput\n ? readSiteConsentTracking(resolve(root, siteInput), opts.site?.siteEnv ?? 'production')\n : null;\n if (siteConsent) {\n const conflicts = consentTrackingConflicts(siteConsent, opts, siteInput as string);\n if (conflicts.length)\n throw new Error(\n `vitops: the site config and the integration options disagree about consent.\\n\\n` +\n conflicts.map((c) => ` • ${c}`).join('\\n\\n'),\n );\n }\n const consentEnabled =\n opts.consent !== undefined ? !!opts.consent : !!siteConsent?.consentEnabled;\n const consentOpts: GetvitopsConsentOptions =\n typeof opts.consent === 'object'\n ? opts.consent\n : // A `categories` list the notice already states is the banner's\n // row set: the two must agree, and the notice is the document a\n // regulator reads.\n siteConsent?.categories\n ? ({ categories: siteConsent.categories } as GetvitopsConsentOptions)\n : {};\n const analytics = resolveAnalytics(opts.analytics, { consent: consentEnabled });\n // What the banner *can* ask, not what it will. Since `<wc-consent>` reveals\n // only the rows something has demanded and the visitor hasn't answered, an\n // unused row costs a hidden `<label>` and nothing else — so the default is\n // deliberately wider than what we can detect. `analytics` covers\n // consumer-authored `data-consent` markup (an A/B split, an embed) that no\n // build-time scan can see; `preferences` covers core's own\n // `<wc-color-scheme-toggle>`, which demands it when a scheme is picked.\n //\n // This used to be a guess standing in for a fact — falling back to\n // `['analytics']` and then showing that banner to everyone. The runtime now\n // has the fact, so this only has to be generous enough not to be in its way.\n // `marketing` is the one that isn't free: it is the category a visitor is\n // most likely to read as \"this site advertises at me\", so it is offered\n // only when something configured actually needs it — a tag that says so,\n // or attribution capture, which demands it at runtime the moment someone\n // arrives on an ad click. This is the build-time half of that pair: the\n // banner must have a row for the category the capture script will ask for.\n const trackingOpts: GetvitopsTrackingOptions | undefined =\n opts.tracking === true\n ? { enabled: true }\n : opts.tracking ||\n // `site.tracking` is what the cookie notice already discloses `_ac`\n // from, so reading it here is what makes the two halves agree by\n // default instead of by hand.\n (siteConsent?.tracking?.enabled ? siteConsent.tracking : undefined);\n // Ad properties come from the site config, not an integration option — the\n // ad account is a fact about the site, and `vitops ads setup`, `ads lint`\n // and the cookie notice read the same block. Resolved before the categories\n // are chosen, because a pixel is a `marketing` demand and the banner must\n // have a row for it.\n const adsConfig = siteInput\n ? readSiteAds(resolve(root, siteInput), opts.site?.siteEnv ?? 'production')\n : null;\n const turnstileSiteKey = siteInput\n ? readSiteTurnstileKey(resolve(root, siteInput), opts.site?.siteEnv ?? 'production')\n : null;\n const experimentsConfig = siteInput\n ? readSiteExperiments(resolve(root, siteInput), opts.site?.siteEnv ?? 'production')\n : null;\n const detectedCategories = consentCategories([\n ...analytics.tags,\n ...resolveAds(adsConfig?.properties, { consent: consentEnabled, enabled: true }).tags,\n ]);\n const defaultCategories: OptionalConsentCategory[] = ['analytics', 'preferences'];\n const trackingCategory = trackingOpts?.enabled\n ? (trackingOpts.category ?? 'marketing')\n : null;\n if (detectedCategories.includes('marketing') || trackingCategory === 'marketing')\n defaultCategories.push('marketing');\n const offeredCategories: OptionalConsentCategory[] =\n consentOpts.categories ?? defaultCategories;\n for (const warning of analytics.warnings) logger.warn(warning);\n\n const tracking = resolveTracking(trackingOpts, {\n consent: consentEnabled,\n consentCategories: offeredCategories,\n });\n for (const warning of tracking.warnings) logger.warn(warning);\n // The capture script beacons `tel:` conversions at a fixed path, but the\n // route that answers it is the consumer's to write. Nothing connected\n // the two: a missing or differently-named route means every beacon 404s\n // and conversions vanish with no error on either side. The route file is\n // a build-time fact, so this is checkable.\n if (tracking.enabled && !hasConversionRoute(root)) {\n logger.warn(\n `tracking is on, so the capture script beacons \\`tel:\\` conversions to ` +\n `${TRACKING_ENDPOINT}, but no route answers that path. Add ` +\n `\\`src/pages/api/track.ts\\` exporting \\`createConversionRoute(...)\\` from ` +\n `\\`@getvitops/astro/routes\\` — otherwise every call conversion is silently lost.`,\n );\n }\n // The same \"config asserts a fact nothing implements\" gap Turnstile hit:\n // `site.crm` names a provider, but nothing writes to it unless a route\n // passes the `crm` option to `createConversionRoute()`.\n if (\n siteInput &&\n siteCrmEnabled(resolve(root, siteInput), opts.site?.siteEnv ?? 'production') &&\n !hasConversionRoute(root)\n ) {\n logger.warn(\n `site.crm is configured, but no route answers ${TRACKING_ENDPOINT}. Add ` +\n `\\`src/pages/api/track.ts\\` exporting \\`createConversionRoute({ crm: {...}, ... })\\` ` +\n `from \\`@getvitops/astro/routes\\` — otherwise submissions never reach the CRM.`,\n );\n }\n\n // Resolved a second time, now against the categories the banner will\n // actually offer and this environment's switch, so its warnings are about\n // the build that ships rather than the probe above.\n const ads = resolveAds(adsConfig?.properties, {\n consent: consentEnabled,\n consentCategories: offeredCategories,\n enabled: adsConfig?.enabled ?? true,\n });\n for (const warning of ads.warnings) logger.warn(warning);\n\n // `legalInput`, not `opts.legal.input` — the path is now a cascade\n // (explicit legal input → `site.input` → a `css.input` that reads as a\n // site config), so a consumer who declares `legal: {}` and lets it fall\n // through still gets the disclosure check rather than skipping it.\n if (opts.analytics && legalInput) {\n const undisclosed = undisclosedProviders(resolve(root, legalInput), opts.analytics);\n if (undisclosed.length)\n logger.warn(\n `analytics: ${undisclosed.join(', ')} ${undisclosed.length === 1 ? 'is' : 'are'} ` +\n \"configured here but absent from your site config's `analytics` block, so the \" +\n 'cookie notice `legal` generates will not disclose ' +\n `${undisclosed.length === 1 ? 'it' : 'them'}. Add ` +\n `${undisclosed.map((k) => SITE_CONFIG_KEYS[k]).join(', ')} to ${legalInput}.`,\n );\n }\n\n const hasManifest = !!(opts.favicon?.name && opts.favicon?.themeColor);\n let hasSvg = false;\n\n // 1. Favicons + manifest → public/\n if (opts.favicon) {\n const source = resolve(root, opts.favicon.source);\n hasSvg = source.endsWith('.svg');\n await generateFavicons({\n source,\n outputDir: publicDir,\n ...(opts.favicon.lowResSource\n ? { lowResSource: resolve(root, opts.favicon.lowResSource) }\n : {}),\n // Also the manifest's `background_color`. The maskable outputs are\n // composited onto it, so the raster and the manifest agree.\n ...(opts.favicon.backgroundColor\n ? { backgroundColor: opts.favicon.backgroundColor }\n : {}),\n });\n if (opts.favicon.name && opts.favicon.themeColor) {\n await writeFaviconManifest(publicDir, {\n name: opts.favicon.name,\n themeColor: opts.favicon.themeColor,\n ...(opts.favicon.backgroundColor\n ? { backgroundColor: opts.favicon.backgroundColor }\n : {}),\n });\n }\n logger.info('generated favicons + manifest → public/');\n }\n\n // 1b. Webfonts → Astro's `fonts:` config (+ <Font /> data for <Head />)\n let headFonts: HeadFont[] = [];\n // `site.fonts` is a declaration of intent, so leaving the option unset must\n // not silently drop it: no @font-face, no preload and no warning was the\n // failure. An explicit `fonts: false` still opts out. A config that fails to\n // resolve is left to the css pass to report, with its field paths.\n let fontsOpt = opts.fonts;\n if (fontsOpt === undefined && siteInput) {\n try {\n const cfg = resolveConfig(\n JSON.parse(readFileSync(resolve(root, siteInput), 'utf8')),\n opts.site?.siteEnv ?? 'production',\n );\n if ((cfg.site.fonts ?? []).length > 0) {\n fontsOpt = true;\n logger.info(\n `fonts: ${siteInput} declares site.fonts, so webfonts are on (pass fonts: false to opt out)`,\n );\n }\n } catch {\n // reported by the css pass\n }\n }\n if (fontsOpt) {\n const given: GetvitopsFontsOptions = Array.isArray(fontsOpt)\n ? { families: fontsOpt }\n : typeof fontsOpt === 'string'\n ? { input: fontsOpt }\n : fontsOpt === true\n ? {}\n : fontsOpt;\n // `fonts: true` (or an options object with no `input`) reads the site\n // config's own `fonts` array — one place to declare a family, next to\n // the rest of the site's facts. That config is wherever it is: the\n // `site` option, or `css.input` when it is one.\n const o: GetvitopsFontsOptions = {\n ...given,\n ...(given.input == null && given.families == null && siteInput\n ? { input: siteInput, ...(opts.site?.siteEnv ? { siteEnv: opts.site.siteEnv } : {}) }\n : {}),\n };\n const declared: SiteFont[] = [];\n if (o.input) {\n // resolveConfig validates and throws with field paths, so a typo'd\n // provider fails the build here rather than resolving to a fallback stack.\n const cfg = resolveConfig(\n JSON.parse(readFileSync(resolve(root, o.input), 'utf8')),\n o.siteEnv ?? 'production',\n );\n declared.push(...(cfg.site.fonts ?? []));\n }\n declared.push(...(o.families ?? []));\n\n if (declared.length === 0) {\n logger.warn(\n 'fonts: nothing declared — no webfonts will load. Add families, or drop the option.',\n );\n } else {\n // A user-level `fonts:` is already on `config` here; another integration's\n // is not (its updateConfig may not have run yet). Astro concatenates the\n // arrays, and two entries on one variable means the last wins silently —\n // so flag the half we can actually see.\n const existing = new Set(\n ((config as { fonts?: { cssVariable?: string }[] }).fonts ?? []).map(\n (f) => f.cssVariable,\n ),\n );\n const { fonts, head } = resolveFonts(declared);\n for (const f of head)\n if (existing.has(f.cssVariable))\n logger.warn(\n `fonts: ${f.cssVariable} is already declared in your astro.config \\`fonts\\` ` +\n 'array. Astro resolves one family per variable, so one of the two will be ' +\n 'dropped — remove it from one place.',\n );\n updateConfig({ fonts });\n headFonts = head;\n const preloaded = head.filter((f) => f.preload !== false).length;\n logger.info(\n `fonts: ${fonts.length} famil${fonts.length === 1 ? 'y' : 'ies'} registered ` +\n `via Astro's Fonts API (${preloaded} preloaded) — <Head /> emits <Font /> for each`,\n );\n }\n }\n\n // 2. Web-component bundles → public/vitops/\n //\n // `consent.js` is copied independently of `webComponents`: the gate is\n // what decides whether third-party tags run, so a site that turned the\n // element runtime off and still asked for consent must not silently lose\n // it — that failure mode is tags loading for everyone.\n const consentRuntime = consentEnabled || analytics.needsRuntime;\n if (webComponents || consentRuntime) {\n const corePkg = fileURLToPath(import.meta.resolve('@getvitops/core/package.json'));\n const coreDist = join(dirname(corePkg), 'dist');\n const dest = join(publicDir, 'vitops');\n if (existsSync(coreDist)) {\n const bundles = webComponents\n ? ['polyfills.js', 'deferred.js', 'elements.js', 'polyfills']\n : [];\n if (consentRuntime) bundles.push('consent.js');\n if (editor || opts.editorManifest) bundles.push('editor.js');\n for (const f of bundles) {\n const src = join(coreDist, f);\n if (existsSync(src)) cpSync(src, join(dest, f), { recursive: true });\n }\n if (opts.editorManifest) writeEditorHost(dest);\n logger.info('copied web-component bundles → public/vitops/');\n } else {\n logger.warn(\n `@getvitops/core dist not found (${coreDist}); run build:core. Skipping WC copy.`,\n );\n }\n }\n\n // 3. Sitemap — opt-in, delegated to the official @astrojs/sitemap.\n //\n // Registered through updateConfig rather than imported at module scope,\n // because the package is an optional peer. Astro core has no\n // addIntegration() (that is Starlight's plugin API), but appending here is\n // supported: runHookConfigSetup re-reads `integrations.length` each\n // iteration and assigns the merged array back onto the settings, and\n // @astrojs/sitemap declares no `astro:config:setup` hook at all — only\n // config:done / routes:resolved / build:done, which all run afterwards.\n //\n // Runs before the virtual head module so <Head /> can link the result.\n let sitemapHref: string | null = null;\n if (opts.sitemap) {\n const registered = config.integrations.map((i) => i.name);\n if (registered.includes('@astrojs/sitemap')) {\n logger.info(\n 'sitemap: @astrojs/sitemap is already in your integrations — leaving yours in charge.',\n );\n } else if (registered.includes('emdash')) {\n logger.warn(\n 'sitemap: emdash() already serves /sitemap.xml from the database, which also covers ' +\n 'on-demand pages a static sitemap cannot. Skipping — drop the `sitemap` option, or ' +\n 'add @astrojs/sitemap to `integrations` yourself to run both.',\n );\n } else if (!config.site) {\n logger.warn(\n 'sitemap: needs the `site` astro.config option (your deployed URL) — a sitemap lists ' +\n 'absolute URLs. Skipping.',\n );\n } else {\n // Optional peer, loaded the same way as @tailwindcss/vite below.\n //\n // The import is cast because the option surface here is this package's\n // narrowed mirror, not @astrojs/sitemap's own `SitemapOptions`: the two\n // disagree on `serialize`'s entry type (upstream's `changefreq` is the\n // `EnumChangefreq` enum, ours the string union a consumer actually\n // wants to write). The shapes are identical at runtime — the value is\n // forwarded verbatim. Cast the module rather than widening the local to\n // `(o?: unknown)`, which is the wrong variance: a target accepting\n // `unknown` promises to accept *any* argument, which upstream doesn't.\n type SitemapFactory = (o?: GetvitopsSitemapOptions) => AstroIntegration;\n let sitemap: SitemapFactory;\n try {\n ({ default: sitemap } = (await import('@astrojs/sitemap')) as unknown as {\n default: SitemapFactory;\n });\n } catch {\n throw new Error(\n '[vitops] sitemap requires `@astrojs/sitemap` in your devDependencies — ' +\n 'install it, or drop the `sitemap` option.',\n );\n }\n if (config.output === 'server')\n logger.warn(\n \"sitemap: output: 'server' — @astrojs/sitemap lists prerendered routes only, so \" +\n 'on-demand pages are left out. Add `export const prerender = true` to the pages ' +\n 'you want indexed, or list them in `sitemap.customPages`.',\n );\n const sitemapOpts = opts.sitemap === true ? undefined : opts.sitemap;\n updateConfig({ integrations: [sitemap(sitemapOpts)] });\n sitemapHref = `/${sitemapOpts?.filenameBase ?? 'sitemap'}-index.xml`;\n logger.info(`sitemap registered → ${sitemapHref.slice(1)}`);\n }\n }\n\n // 3b. The IndexNow key file.\n //\n // Derived from the site config rather than an option of its own: the key\n // is already declared there for `vitops search notify`, and a second place to\n // write it is a second place for it to disagree. Nothing else is needed\n // to opt in — a config with `seo.indexing.indexNow.key` has, by saying so,\n // asked for the file that makes the key usable.\n //\n // Not a secret. IndexNow verifies host ownership by fetching this file\n // back, so publishing it *is* the mechanism.\n if (siteInput) {\n try {\n const cfg = resolveConfig(\n JSON.parse(readFileSync(resolve(root, siteInput), 'utf8')),\n opts.site?.siteEnv ?? 'production',\n );\n const key = cfg.site.seo?.indexing?.indexNow?.key;\n if (key) {\n mkdirSync(publicDir, { recursive: true });\n writeFileSync(join(publicDir, `${key}.txt`), `${key}\\n`);\n logger.info(`IndexNow key file → public/${key}.txt`);\n }\n\n // 3c. robots.txt — the same environment.robots fact search notify's\n // noindex gate already reads. Unlike the key file, this is written\n // only if the consumer has not already placed one: today's\n // `RobotsSchema` is a single directive string, with no way to\n // express path-level rules (`Disallow: /admin`), so a hand-authored\n // file may carry more than the config can express and must not be\n // silently overwritten every build.\n const robotsPath = join(publicDir, 'robots.txt');\n if (!existsSync(robotsPath)) {\n mkdirSync(publicDir, { recursive: true });\n writeFileSync(\n robotsPath,\n renderRobotsTxt(cfg, { env: opts.site?.siteEnv ?? 'production' }),\n );\n logger.info('robots.txt → public/robots.txt');\n }\n } catch (err) {\n // Never fatal. Every other consumer of this config validates it\n // loudly elsewhere in this hook; failing the build a second time,\n // here, would just bury the first message.\n logger.warn(`IndexNow key file / robots.txt skipped: ${(err as Error).message}`);\n }\n }\n\n // 4. Icons — resolve names centrally, and derive the bundle on a server build.\n //\n // Everything here has to finish BEFORE the updateConfig that appends the\n // icon integration, because an appended integration's own\n // astro:config:setup runs after this hook returns — it cannot be handed\n // options computed later. That is why the scan is synchronous rather than\n // deferred to the Vite plugin's buildStart, which runs far too late.\n let iconsData: IconsData | null = null;\n if (opts.icons) {\n const o: GetvitopsIconsOptions = opts.icons === true ? {} : opts.icons;\n const ui = o.ui ?? ICON_DEFAULTS.ui;\n const brand = o.brand ?? ICON_DEFAULTS.brand;\n const spriteHref = o.sprite ?? ICON_DEFAULTS.sprite;\n\n // Declared icons first. An unresolvable name here THROWS — it's a\n // config error, and generateIconInclude has always been loud about it.\n // `exactOptionalPropertyTypes` is on, so an optional key has to be\n // absent rather than explicitly undefined.\n const weightOpt = o.weight ? { weight: o.weight } : {};\n const include: Record<string, string[]> = o.include\n ? generateIconInclude({ ui, brand, ...weightOpt, ...o.include })\n : {};\n const add = (prefix: string, name: string) => {\n const list = (include[prefix] ??= []);\n if (!list.includes(name)) list.push(name);\n };\n\n // Then whatever the source actually references.\n const scanDirs = o.scan === false ? [] : (o.scan ?? [...ICON_DEFAULTS.scan]);\n const unresolved: string[] = [];\n const dynamic: { file: string; line: number; expr: string }[] = [];\n for (const dir of scanDirs) {\n const abs = resolve(root, dir);\n if (!existsSync(abs)) continue;\n const found = collectIconRefs(scanFiles(abs));\n dynamic.push(\n ...found.dynamic.map((d) => ({\n file: relative(root, d.file),\n line: d.line,\n expr: d.expr,\n })),\n );\n for (const name of found.names) {\n // Already qualified — group it under its own prefix verbatim.\n const colon = name.indexOf(':');\n if (colon > 0) {\n add(name.slice(0, colon), name.slice(colon + 1));\n continue;\n }\n // A scanned name that doesn't resolve only WARNS. It is far more\n // likely a local SVG (astro-icon reads src/icons/*.svg by bare\n // name) or a sprite id than a mistake, and a dev server that dies\n // on a typo in a template is worse than one that tells you.\n try {\n const q = resolveIcon(name, ui, weightOpt);\n add(q.slice(0, q.indexOf(':')), q.slice(q.indexOf(':') + 1));\n } catch {\n unresolved.push(name);\n }\n }\n }\n\n // `include` only matters where a bundle is at stake. astro-icon is\n // zero-config on a static build; passing a list there would trim\n // nothing and risk dropping a glyph the scan couldn't see.\n const wantsInclude = config.output !== 'static';\n if (dynamic.length && wantsInclude) {\n logger.warn(\n `icons: ${dynamic.length} icon name(s) are computed at runtime and cannot be ` +\n `bundled automatically:\\n` +\n dynamic.map((d) => ` ${d.file}:${d.line} ${d.expr}`).join('\\n') +\n `\\n Declare them in \\`icons.include\\` (or set \\`icons.scan: false\\` to silence this).`,\n );\n }\n if (unresolved.length)\n logger.warn(\n `icons: ${unresolved.length} name(s) are not in the semantic map for '${ui}': ` +\n `${unresolved.join(', ')}. Left as-is — fine for a local src/icons/*.svg or a ` +\n `sprite id, a typo otherwise.`,\n );\n\n const engine: IconsData['engine'] = o.engine ?? 'inline';\n if (o.sprite && !opts.css)\n logger.warn(\n 'icons: the sprite is written by the same pass that generates the CSS, so ' +\n '`sprite` without `css` produces no file. Add `css`, or run `vitops icons --sprite`.',\n );\n\n const registered = config.integrations.map((i) => i.name);\n const already = registered.find((n) => n === 'astro-icon' || n === 'astro-iconset');\n if (o.register === false) {\n logger.info('icons: register: false — resolving names only, not registering anything.');\n } else if (already) {\n // Only the registration is skipped. The resolver, the scan and the\n // sprite are not astro-icon's job, so they still run.\n logger.info(\n `icons: ${already} is already in your integrations — leaving yours in charge.`,\n );\n } else {\n // Registered for the consumer's OWN `<Icon>` (astro-icon's), and to\n // hand it the derived include. Independent of how ours renders —\n // `<Icon />` inlines from the collection and needs no integration —\n // so a missing package is silence, not an error.\n const pkg = (await probeIconPackage('astro-icon'))\n ? 'astro-icon'\n : (await probeIconPackage('astro-iconset'))\n ? 'astro-iconset'\n : null;\n if (pkg) {\n type IconFactory = (o?: Record<string, unknown>) => AstroIntegration;\n const { default: icon } = (await import(/* @vite-ignore */ pkg)) as unknown as {\n default: IconFactory;\n };\n updateConfig({ integrations: [icon(wantsInclude ? { include } : {})] });\n logger.info(\n wantsInclude\n ? `icons: ${pkg} registered — bundling ${Object.values(include).flat().length} icon(s)`\n : `icons: ${pkg} registered (output: 'static' — no include needed)`,\n );\n }\n }\n\n iconsData = {\n engine,\n root,\n ui,\n brand,\n weight: o.weight ?? null,\n overrides: o.overrides ?? {},\n sprite: engine === 'sprite' || o.sprite ? spriteHref : null,\n };\n }\n\n // 5. Virtual head module for <Head/>\n updateConfig({\n vite: {\n // `<Icon>` imports `@getvitops/generator/icons-sprite`, which imports `@iconify/utils`.\n // On the Cloudflare adapter `astro dev` evaluates the worker bundle in workerd, where\n // an un-prebundled CommonJS dependency fails with \"module is not defined\" on EVERY\n // route that reaches it (the build is unaffected: Rollup handles the interop). The\n // dev optimizer only discovers dependencies a site's own sources import, so one\n // reached through a package's `.astro` file, e.g. a plugin block that renders\n // `<ReviewCard>`, is never pre-bundled. Naming it here, relative to this package so\n // it resolves under pnpm's strict layout, fixes dev for every site using `<Icon>`.\n ssr: {\n optimizeDeps: { include: ['@getvitops/astro > @getvitops/generator/icons-sprite'] },\n },\n plugins: [\n virtualReviewsPlugin(root),\n virtualIconsPlugin(\n iconsData ?? {\n engine: 'none',\n root,\n ui: ICON_DEFAULTS.ui,\n brand: ICON_DEFAULTS.brand,\n weight: null,\n overrides: {},\n sprite: null,\n },\n ),\n virtualHeadPlugin({\n favicons: !!opts.favicon,\n faviconLinks: opts.favicon ? faviconLinks({ hasSvg, manifest: hasManifest }) : [],\n themeColor: opts.favicon?.themeColor ?? null,\n webComponents,\n wcBase: '/vitops',\n editor,\n sitemap: sitemapHref,\n fonts: headFonts,\n site: config.site ?? null,\n seo: seoDefaults,\n analytics: opts.analytics ?? {},\n consent: consentEnabled,\n consentCategories: offeredCategories,\n consentPolicyUrl: consentOpts.policyUrl ?? null,\n consentRuntime,\n tracking: tracking.enabled\n ? {\n enabled: true,\n category: tracking.category,\n cookies: tracking.cookies,\n abVariantCookie: experimentsConfig?.primaryCookie ?? null,\n }\n : null,\n experiments: experimentsConfig?.experiments ?? {},\n matomoAbVariantDimensions: experimentsConfig?.matomoAbVariantDimensions ?? {},\n ads: adsConfig,\n turnstileSiteKey,\n }),\n ],\n },\n });\n\n // 6. CSS — generate + compile + auto-inject (consumer imports nothing).\n if (opts.css) {\n const format: StylesheetFormat = opts.css.format ?? 'tailwind';\n // Typed out above, but a plain-JS config reaches here unchecked and\n // would otherwise fail late with an unresolvable `styles.css` import.\n if ((format as string) === 'design')\n throw new Error(\n \"[vitops] css.format: 'design' emits only DESIGN.md, not a stylesheet. \" +\n 'Run `npx vitops generate --format design --out .` for that file, and set ' +\n \"css.format to 'tailwind' | 'css' | 'bricks' here.\",\n );\n const out = opts.css.out ?? 'src/styles';\n const plugins = [\n vitopsPlugin({\n input: opts.css.input ?? 'design-system.json',\n ...(opts.css.theme != null ? { theme: opts.css.theme } : {}),\n format,\n out,\n // Mirrored inside the generate pass so the served copy can never be\n // missing on a cold start or stale after a config edit.\n ...(editor || opts.editorManifest ? { editorManifestDir: publicDir } : {}),\n // Same mirroring rule as the manifest: the bytes are produced by\n // this pass, so the copy belongs inside it rather than in the hook.\n //\n // Also when the SITE CONFIG asks for the sprite (`site.icons.sprite`):\n // that alone makes the generator write icons.svg, but only the `icons`\n // option used to mirror it, so the file was generated and never\n // served — `/vitops/icons.svg` fell through to the catch-all.\n ...(iconsData?.sprite || siteWantsSprite(root, siteInput, opts.site?.siteEnv)\n ? { spriteDir: publicDir }\n : {}),\n // Same reasoning: inside the pass, so the documents track the site\n // config in dev instead of going stale after the first build.\n ...(opts.legal ? { legal: { ...opts.legal, input: legalInput as string } } : {}),\n // Byte-producing, so it belongs in the pass rather than in this hook.\n // Its own cache makes a rebuild cheap; nothing here needs to gate it.\n ...(opts.media ? { media: opts.media } : {}),\n // Read during generation for the site-level facts the stylesheet\n // depends on — currently `designSystem.defaultColorScheme`, which decides\n // whether the colour layer carries a prefers-color-scheme block.\n ...(opts.site ? { site: opts.site } : {}),\n ...(opts.css.systemColorScheme != null\n ? { systemColorScheme: opts.css.systemColorScheme }\n : {}),\n }),\n ];\n // Tailwind is an optional peer — only the `tailwind` format needs it,\n // so it's loaded here rather than imported at module scope. (The\n // plugin is typed against a newer vite than @getvitops/vite's peer;\n // the Plugin shapes are structurally compatible at runtime — Astro\n // forwards them verbatim — so bridge the two type identities.)\n //\n // The specifier goes through a constant, with `@vite-ignore`, for the\n // same reason `probeIconPackage` does it: a statically analysable\n // `import('@tailwindcss/vite')` makes the bundler FOLLOW the module\n // even on a `css`-format site that never reaches this branch, and the\n // chain ends at @tailwindcss/oxide's native `.node` binding, which it\n // cannot parse. That failed the whole build with an opaque\n // \"stream did not contain valid UTF-8\" — for a site not using Tailwind\n // at all. `optional peer` has to mean optional to the bundler too.\n if (format === 'tailwind') {\n let tailwindcss: () => unknown[];\n try {\n ({ default: tailwindcss } = (await import(\n /* @vite-ignore */ TAILWIND_VITE\n )) as unknown as { default: () => unknown[] });\n } catch {\n throw new Error(\n \"[vitops] css.format: 'tailwind' requires `tailwindcss` and `@tailwindcss/vite` \" +\n 'in your devDependencies — install them, or switch to css.format: ' +\n \"'css' for the standalone bundle.\",\n );\n }\n plugins.push(...(tailwindcss() as unknown as typeof plugins));\n }\n // `never`: with Astro 7.4's `DeepPartial<AstroConfig>`, comparing the plugin array against\n // Vite's `PluginOption` blows TypeScript's instantiation depth (TS2321). The array is\n // built from real Vite plugins above, so the cast loses nothing a check would catch.\n updateConfig({ vite: { plugins: plugins as never } });\n const cssFile = format === 'tailwind' ? 'tailwind.css' : 'styles.css';\n if (opts.css.inject !== false) {\n const cssPath = resolve(root, out, cssFile);\n injectScript('page-ssr', `import ${JSON.stringify(cssPath)};`);\n logger.info(`design-system CSS (${format}) auto-injected from ${out}/${cssFile}`);\n } else {\n logger.info(\n `design-system CSS (${format}) generated at ${out}/${cssFile} (inject: false — import it from your layout)`,\n );\n }\n }\n },\n },\n };\n}\n","/**\n * The inline scripts `<Head />` emits, as plain strings.\n *\n * They live here rather than in `Head.astro`'s frontmatter for one reason: a\n * Content-Security-Policy that doesn't allow `'unsafe-inline'` has to name each\n * inline script by SHA-256, and the hash is of the exact emitted bytes. One\n * constant imported by both the component (which emits it) and `csp.ts` (which\n * hashes it) makes \"the hash matches what the page serves\" true by construction\n * rather than by a test remembering to compare two copies. `csp.test.ts` and the\n * container render test still assert it against real rendered HTML.\n *\n * No imports and no Node builtins: `Head.astro` runs in a Worker for any page\n * that is rendered on demand.\n */\n\n/**\n * Applies the remembered colour scheme before first paint.\n *\n * The key MUST match `WCColorSchemeToggle.STORAGE_KEY` in @getvitops/core;\n * `head.test.ts` asserts they stay in step. try/catch because localStorage\n * throws in private/partitioned contexts.\n */\nexport const THEME_SCRIPT =\n `try{var s=localStorage.getItem('vitops-color-scheme');` +\n `if(s==='light'||s==='dark'){document.documentElement.dataset.theme=s;` +\n `document.documentElement.style.colorScheme=s+' only'}}catch(e){}`;\n\n/**\n * The consent gate's build-time signal: \"something on this page may need\n * permission, and the real gate hasn't loaded yet.\"\n *\n * `consent.js` and `elements.js` are both deferred modules with no ordering\n * guarantee between them, so `<wc-color-scheme-toggle>` can upgrade and be clicked\n * before the gate exists. Without this, a caller probing for\n * `window.vitopsConsent` in that window would see nothing, correctly conclude\n * that sites without the gate should just store — and store. The stub closes the\n * window by making the gate's *existence* known synchronously in `<head>`, which\n * is the only place that beats every deferred bundle.\n *\n * It answers `false` to everything, because nothing is granted until the store\n * has been read, and queues what it was asked so `consent/runtime.ts` can replay\n * it on evaluation. `q` entries are `[category]` for `require()` and\n * `[category, resolve]` for `request()`. Same shape as the `dataLayer`/`gtag`\n * stub in `analytics.ts` and for the same reason.\n *\n * Emitted only when the gate is on: its absence is what tells a caller the site\n * has no gate at all.\n */\nexport const CONSENT_STUB =\n `window.vitopsConsent=window.vitopsConsent||{q:[],` +\n `require:function(c){this.q.push([c]);return false},` +\n `request:function(c){var s=this;return new Promise(function(r){s.q.push([c,r])})},` +\n `granted:function(){return false},needed:function(){return false}};`;\n","/**\n * Content-Security-Policy helpers for the inline scripts `<Head />` emits.\n *\n * `<Head />` writes up to two inline `<script>`s into every page: the pre-paint\n * colour-scheme bootstrap and the consent-gate stub. A policy without\n * `'unsafe-inline'` blocks both unless each is named by hash, and both are\n * constants (`inline-scripts.ts`), so their hashes are fixed per package version\n * and can sit in a static `_headers` file — which is exactly what the scaffolded\n * templates do, in `Content-Security-Policy-Report-Only` mode.\n *\n * Why hashes and not nonces: a nonce is per response, so it needs the page to be\n * rendered by a Worker on every request. The templates' pages are prerendered\n * static assets, which have no request to attach a nonce to. Hashes also survive\n * being cached.\n *\n * What this does NOT cover, and the policy says so rather than hiding it: scripts\n * the page gets from elsewhere (EmDash's own head tags, a server-island loader,\n * an editor's HTML block, the consent gate activating a third-party tag). Those\n * are what Report-Only is for — see the template's AGENTS.md.\n *\n * Node-only (`node:crypto`), so it is exported from the package root — a build\n * time import — and never from a Worker entry.\n */\nimport { createHash } from 'node:crypto';\nimport { CONSENT_STUB, THEME_SCRIPT } from './inline-scripts.ts';\n\n/** The CSP source expression for one inline script: `'sha256-<base64>'`. */\nexport function cspScriptHash(script: string): string {\n return `'sha256-${createHash('sha256').update(script, 'utf8').digest('base64')}'`;\n}\n\nexport interface InlineScriptOptions {\n /** `<Head />` emits the colour-scheme bootstrap (its `webComponents` prop). Default true. */\n webComponents?: boolean;\n /** `<Head />` emits the consent-gate stub (a site with the gate on). Default true. */\n consent?: boolean;\n}\n\n/**\n * CSP hash sources for every inline script `<Head />` can emit.\n *\n * Defaults to ALL of them: listing a hash for a script the page doesn't emit is\n * harmless, while omitting one a page does emit is a violation.\n */\nexport function inlineScriptHashes(opts: InlineScriptOptions = {}): string[] {\n const out: string[] = [];\n if (opts.webComponents !== false) out.push(cspScriptHash(THEME_SCRIPT));\n if (opts.consent !== false) out.push(cspScriptHash(CONSENT_STUB));\n return out;\n}\n\nexport interface ContentSecurityPolicyOptions {\n /** Extra `script-src` sources, e.g. a hash for your own inline script or an origin. */\n scriptSrc?: string[];\n /** Origins for `connect-src` beyond `'self'` (analytics endpoints, a form API). */\n connectSrc?: string[];\n /** Origins for `img-src` beyond `'self'`, `data:` and `https:`. */\n imgSrc?: string[];\n /** Origins for `frame-src` (embeds). Default none. */\n frameSrc?: string[];\n /** A `report-uri` endpoint. Omit to rely on the browser console. */\n reportUri?: string;\n /** Which `<Head />` inline scripts to allow. Default all. */\n inline?: InlineScriptOptions;\n}\n\n/**\n * A strict starting policy for a prerendered site.\n *\n * `script-src` is `'self'` plus the `<Head />` hashes — no `'unsafe-inline'`.\n * `style-src` keeps `'unsafe-inline'` because Astro inlines scoped `<style>` and\n * components set `style=\"\"` attributes; a style injection is far less dangerous\n * than a script one, and hashing every stylesheet would break on any edit.\n * `img-src` allows `https:` because editors paste images from anywhere.\n */\nexport function contentSecurityPolicy(opts: ContentSecurityPolicyOptions = {}): string {\n const list = (name: string, ...sources: (string[] | undefined)[]) =>\n `${name} ${sources.flatMap((s) => s ?? []).join(' ')}`.trim();\n const directives = [\n \"default-src 'self'\",\n list('script-src', [\"'self'\"], inlineScriptHashes(opts.inline), opts.scriptSrc),\n \"style-src 'self' 'unsafe-inline'\",\n list('img-src', [\"'self'\", 'data:', 'https:'], opts.imgSrc),\n \"font-src 'self'\",\n list('connect-src', [\"'self'\"], opts.connectSrc),\n list('frame-src', opts.frameSrc?.length ? opts.frameSrc : [\"'none'\"]),\n \"object-src 'none'\",\n \"base-uri 'self'\",\n \"form-action 'self'\",\n \"frame-ancestors 'self'\",\n ];\n if (opts.reportUri) directives.push(`report-uri ${opts.reportUri}`);\n return directives.join('; ');\n}\n","/**\n * Real per-page `<lastmod>` for the sitemap, derived from git.\n *\n * This is the one lever that reliably moves Google. It is also load-bearing for\n * `vitops search notify`, which decides what to submit by diffing lastmod against the\n * previous build — with no lastmod, that diff can see pages appear and disappear\n * but never see one *change*, so an edited page is never resubmitted.\n *\n * A plain exported helper rather than magic inside the `sitemap` option, because\n * it carries a caveat the caller needs to see: it shells out to `git`, so it\n * returns nothing useful from a shallow CI clone (`fetch-depth: 1` — the default\n * in actions/checkout) or from a tarball with no history. In that case it warns\n * and emits no dates at all.\n *\n * import vitops, { gitLastmod } from '@getvitops/astro';\n * export default defineConfig({\n * site: 'https://acme.ca',\n * integrations: [vitops({ sitemap: { serialize: await gitLastmod() } })],\n * });\n *\n * **No date beats a wrong date.** An unmatched URL is left alone rather than\n * stamped with the build time: Google weighs lastmod only while it stays\n * consistent with what actually changed, and a site that stamps every page on\n * every deploy teaches it to stop believing the field — site-wide, not just for\n * the pages that were wrong.\n */\nimport { execFile } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { promisify } from 'node:util';\nimport type { GetvitopsSitemapEntry } from './integration.ts';\n\nconst exec = promisify(execFile);\n\n/** Route-bearing sources: file-based pages, and the collections they render. */\nconst DEFAULT_DIRS = ['src/pages', 'src/content', 'src/data'];\n\nconst PAGE_EXT = /\\.(astro|md|mdx|markdown|html)$/;\nconst CONTENT_EXT = /\\.(md|mdx|markdown|json|yaml|yml)$/;\n\nexport interface GitLastmodOptions {\n /** Directories to read history from (default `src/pages`, `src/content`, `src/data`). */\n dirs?: string[];\n /** Repository root (default `process.cwd()`). */\n cwd?: string;\n /** Called with a human-readable reason when no dates could be derived. */\n onWarn?: (message: string) => void;\n}\n\n/**\n * `src/pages/blog/index.astro` → `/blog`; `src/pages/index.astro` → `/`.\n *\n * Dynamic routes return `undefined`: one source file backs many URLs, and its\n * commit date describes the template rather than any of them. Stamping every\n * generated page with the template's date is exactly the \"wrong date\" case above.\n */\nexport function routeFromPage(file: string): string | undefined {\n const m = /(?:^|\\/)src\\/pages\\/(.+)$/.exec(file);\n if (!m) return undefined;\n let rest = m[1]!;\n if (!PAGE_EXT.test(rest)) return undefined;\n if (rest.includes('[')) return undefined; // dynamic route\n rest = rest.replace(PAGE_EXT, '');\n if (rest === 'index') return '/';\n rest = rest.replace(/\\/index$/, '');\n return `/${rest}`;\n}\n\n/** The slug a content entry is most likely published under. */\nexport function slugFromContent(file: string): string | undefined {\n const m = /(?:^|\\/)src\\/(?:content|data)\\/(.+)$/.exec(file);\n if (!m) return undefined;\n const rest = m[1]!;\n if (!CONTENT_EXT.test(rest)) return undefined;\n const base = rest.split('/').pop()!.replace(CONTENT_EXT, '');\n // `index` names the collection's directory, not a slug of its own.\n return base === 'index' ? undefined : base;\n}\n\nconst normalise = (pathname: string): string => {\n const p = pathname.replace(/\\/+$/, '');\n return p === '' ? '/' : p;\n};\n\n/**\n * Most recent commit date per file, from one `git log` pass.\n *\n * One process rather than one per file: a `git log -1` per page is fine for\n * twenty pages and unusable for two thousand. `--name-only` interleaves each\n * commit's date with its files, and because `git log` is newest-first the first\n * time a path appears is its latest change.\n */\nasync function commitDates(dirs: string[], cwd: string): Promise<Map<string, string>> {\n const dates = new Map<string, string>();\n const { stdout } = await exec(\n 'git',\n ['log', '--date-order', '--pretty=format:%x00%cI', '--name-only', '--', ...dirs],\n { cwd, maxBuffer: 64 * 1024 * 1024 },\n );\n\n let current: string | undefined;\n for (const line of stdout.split('\\n')) {\n if (line.startsWith('\\0')) {\n current = line.slice(1).trim();\n continue;\n }\n const file = line.trim();\n if (!file || !current) continue;\n if (!dates.has(file)) dates.set(file, current);\n }\n return dates;\n}\n\n/**\n * Build a `serialize` function that stamps `lastmod` from git history.\n *\n * Resolution order per URL: an exact file-based route first, then a unique\n * content slug. \"Unique\" is doing real work — a slug appearing in two collections\n * is dropped rather than guessed at, since picking either one would be a coin\n * flip presented as a fact.\n */\nexport async function gitLastmod(\n options: GitLastmodOptions = {},\n): Promise<(item: GetvitopsSitemapEntry) => GetvitopsSitemapEntry> {\n const cwd = options.cwd ?? process.cwd();\n const warn = options.onWarn ?? ((m: string) => console.warn(`[vitops] gitLastmod: ${m}`));\n const dirs = (options.dirs ?? DEFAULT_DIRS).filter((d) => existsSync(`${cwd}/${d}`));\n\n const identity = (item: GetvitopsSitemapEntry) => item;\n\n if (dirs.length === 0) {\n warn(`none of ${(options.dirs ?? DEFAULT_DIRS).join(', ')} exist — no dates emitted`);\n return identity;\n }\n\n let dates: Map<string, string>;\n try {\n dates = await commitDates(dirs, cwd);\n } catch (err) {\n warn(`git log failed (${(err as Error).message}) — no dates emitted`);\n return identity;\n }\n\n if (dates.size === 0) {\n // The shallow-clone case, and the one most likely to go unnoticed: the build\n // succeeds, the sitemap is valid, and every lastmod is simply missing.\n warn(\n 'git history has no commits touching your source — a shallow clone? Set fetch-depth: 0 in your checkout step. No dates emitted.',\n );\n return identity;\n }\n\n const byRoute = new Map<string, string>();\n const bySlug = new Map<string, string>();\n const ambiguous = new Set<string>();\n\n for (const [file, date] of dates) {\n const route = routeFromPage(file);\n if (route) {\n byRoute.set(route, date);\n continue;\n }\n const slug = slugFromContent(file);\n if (!slug) continue;\n if (bySlug.has(slug)) ambiguous.add(slug);\n else bySlug.set(slug, date);\n }\n for (const slug of ambiguous) bySlug.delete(slug);\n\n /*\n * No coverage warning here on purpose. `serialize` is called once per entry\n * with no completion signal, so any \"matched N of M\" it emitted would be a\n * running total printed mid-stream. The same fact is reported properly one step\n * later: `vitops search notify` counts entries without a `<lastmod>` and says so\n * against the finished sitemap.\n */\n return (item: GetvitopsSitemapEntry): GetvitopsSitemapEntry => {\n // An explicit lastmod from the caller wins — they know something we inferred.\n if (item.lastmod) return item;\n\n let pathname: string;\n try {\n pathname = normalise(new URL(item.url).pathname);\n } catch {\n return item;\n }\n\n const date = byRoute.get(pathname) ?? bySlug.get(pathname.split('/').pop() ?? '');\n return date ? { ...item, lastmod: date } : item;\n };\n}\n","/**\n * The `<Seo />` resolution layer — site-level defaults merged with per-page props\n * into the exact tags to emit.\n *\n * Kept separate from `Seo.astro` deliberately. The component this replaces\n * (`SEO.astro`, deleted in c949cae) fused resolution with markup, which is why it\n * was never unit-tested and why its title handling drifted: it computed a title\n * for `og:title` but left `<title>` to the layout, so the two could disagree.\n * Everything decidable lives here and is asserted in `seo.test.ts`; the component\n * only renders what this returns.\n */\n\n/** Twitter card types, per <https://developer.x.com/en/docs/twitter-for-websites/cards>. */\nexport type SeoTwitterCard = 'summary' | 'summary_large_image' | 'app' | 'player';\n\n/** Open Graph object types this component knows how to describe. */\nexport type SeoOgType = 'website' | 'article' | 'profile';\n\n/**\n * Site-level SEO defaults, passed to `vitops({ seo })` and baked into the\n * `virtual:getvitops/head` module. Per-page `<Seo />` props override these.\n *\n * `site.seo` in the site config is the source (`@getvitops/config`'s `SeoSchema`);\n * `resolveSeoDefaults()` turns it into this shape for a build locale, and `vitops({ seo })` is merged\n * over the result as an override. This interface is therefore the *resolved* shape — plain strings,\n * `Localizable` already picked — and must stay JSON-serialisable: it is `JSON.stringify`d into the\n * virtual module, so no functions, no `URL`s, no dates. `seo-defaults.test.ts` fails when a\n * `site.seo` field is added to the schema and not read here (ADR-0002).\n */\nexport interface GetvitopsSeoOptions {\n /** `og:site_name`, and the `<title>` when a page passes none. */\n siteName?: string;\n /**\n * Page-title pattern; `%s` is the page's own title. e.g. `'%s · Acme'`.\n *\n * Applies to `<title>` only. `og:title`/`twitter:title` get the untemplated\n * title: a social card already shows `og:site_name` and the domain, so the\n * suffix would sit directly above the brand it repeats. Override per page with\n * `ogTitle`.\n *\n * Skipped when a page's title already equals `siteName` — a homepage titled\n * \"Acme\" emits `Acme`, not `Acme · Acme` — and never applied to `defaultTitle`.\n */\n titleTemplate?: string;\n /** `<title>` for pages that pass none. Used verbatim — the template is not applied. */\n defaultTitle?: string;\n /** `<meta name=\"description\">` for pages that pass none. */\n defaultDescription?: string;\n /** Site-wide robots directive, e.g. `'index,follow,max-image-preview:large'`. */\n robots?: string;\n /** Google Search Console verification token. */\n googleSiteVerification?: string;\n /** Bing Webmaster Tools verification token. */\n bingSiteVerification?: string;\n openGraph?: {\n /** Default `og:type` (default `'website'`). */\n type?: SeoOgType;\n /** `og:locale`. Written either way — `en-CA` and `en_CA` both emit `en_CA`. */\n locale?: string;\n /** Additional locales the same content exists in → `og:locale:alternate`. */\n localeAlternates?: string[];\n /** Fallback share image. Relative URLs resolve against `site`. */\n image?: { url: string; alt?: string; width?: number; height?: number; type?: string };\n };\n twitter?: {\n /** Card type when no image resolves (default `'summary'`). */\n card?: SeoTwitterCard;\n /** The site's own handle, `@acme`. */\n site?: string;\n /** Default author handle, `@ada`. */\n creator?: string;\n };\n}\n\n/** Per-page `<Seo />` props. Everything is optional; defaults fill the gaps. */\nexport interface SeoProps {\n /** Page title, before `titleTemplate` is applied. */\n title?: string;\n /**\n * `og:title`/`twitter:title`, when the social headline should differ from\n * `title`. Defaults to `title` **without** `titleTemplate` — see the note on\n * `titleTemplate` for why the suffix is dropped there.\n */\n ogTitle?: string;\n description?: string;\n /** Overrides the computed canonical. Relative values resolve against `site`. */\n canonical?: string;\n /** Overrides `og:site_name` and the title fallback. */\n siteName?: string;\n /** Share image for this page. Relative URLs resolve against `site`. */\n image?: string;\n imageAlt?: string;\n imageWidth?: number;\n imageHeight?: number;\n /** MIME type of the share image, e.g. `'image/png'`. */\n imageType?: string;\n noindex?: boolean;\n nofollow?: boolean;\n noarchive?: boolean;\n nocache?: boolean;\n /** Extra robots directives, e.g. `'max-snippet:-1'`. Appended to the composed value. */\n robotsExtras?: string;\n /** Full robots override. Wins over the flags above and the site default. */\n robots?: string;\n /** `og:type` for this page (default: the site default, else `'website'`). */\n type?: SeoOgType;\n /** Emitted only when `type` is `'article'`. */\n article?: {\n publishedTime?: string;\n modifiedTime?: string;\n expirationTime?: string;\n authors?: string[];\n section?: string;\n tags?: string[];\n };\n /** `og:locale` for this page. */\n locale?: string;\n /** Twitter author handle for this page, `@ada`. */\n twitterCreator?: string;\n /**\n * Translations of *this* page, as `<link rel=\"alternate\" hreflang>`.\n *\n * Explicit only, and it must include the current page. Nothing is inferred:\n * the deleted component derived alternates from the site's locale list and\n * pointed every one of them at that locale's homepage, so every page on the\n * site claimed the homepage as its translation. Only the page knows its own\n * translated URLs. Pass `hreflang: 'x-default'` for the fallback entry.\n */\n alternates?: { hreflang: string; href: string }[];\n}\n\n/** A `<meta property>` tag (Open Graph uses `property`, not `name`). */\nexport interface SeoPropertyTag {\n property: string;\n content: string;\n}\n\n/** A `<meta name>` tag. */\nexport interface SeoNameTag {\n name: string;\n content: string;\n}\n\n/** Everything `<Seo />` renders, already decided. `null` means \"emit nothing\". */\nexport interface ResolvedSeo {\n title: string | null;\n /** `og:title`/`twitter:title` — the page title without `titleTemplate`. */\n socialTitle: string | null;\n description: string | null;\n canonical: string | null;\n robots: string | null;\n openGraph: SeoPropertyTag[];\n twitter: SeoNameTag[];\n verification: SeoNameTag[];\n alternates: { hreflang: string; href: string }[];\n}\n\n/** Render context supplied by the component from `Astro`. */\nexport interface SeoContext {\n /**\n * The deployed origin — `Astro.site`, or `config.site` via the virtual module.\n *\n * `undefined` when neither is set, and then every absolute-URL tag is dropped\n * rather than being derived from the request. `astro-seo` falls back to\n * `Astro.url` here; on a static build that is `http://localhost:4321`, which\n * would bake a localhost canonical into production HTML.\n */\n site?: URL | undefined;\n /** `Astro.url.pathname` — already includes `base`. */\n pathname: string;\n /** `Astro.url.search`, if the canonical should keep the query string. */\n search?: string;\n}\n\n/** `en-CA` → `en_CA`; Open Graph wants the underscore form. */\nconst ogLocale = (locale: string) => locale.replace('-', '_');\n\n/**\n * Absolute URLs pass through; relative ones resolve against `site`, or are\n * dropped when there is no `site` to resolve them against. A relative `og:image`\n * or canonical is worse than none — crawlers resolve it against whatever origin\n * served the page.\n */\nfunction absolute(url: string | undefined, site: URL | undefined): string | null {\n if (!url) return null;\n try {\n return new URL(url).href;\n } catch {\n return site ? new URL(url, site).href : null;\n }\n}\n\n/** Composes the robots directive, or null when nothing meaningful was asked for. */\nfunction resolveRobots(props: SeoProps, defaults: GetvitopsSeoOptions): string | null {\n if (props.robots) return props.robots;\n const { noindex, nofollow, noarchive, nocache, robotsExtras } = props;\n if (noindex || nofollow || noarchive || nocache || robotsExtras) {\n return [\n noindex ? 'noindex' : 'index',\n nofollow ? 'nofollow' : 'follow',\n ...(noarchive ? ['noarchive'] : []),\n ...(nocache ? ['nocache'] : []),\n ...(robotsExtras ? [robotsExtras] : []),\n ].join(', ');\n }\n // No explicit default either: emit nothing. `index, follow` is what crawlers\n // already assume, so stating it adds bytes and no meaning.\n return defaults.robots ?? null;\n}\n\n/**\n * Merges site defaults with page props into the final tag set.\n *\n * Pure — no `Astro`, no filesystem, no config lookup. Everything it needs comes\n * through its three arguments, which is what makes it testable and what keeps the\n * package free of the consumer-global coupling that sank the previous site model.\n */\nexport function resolveSeo(\n defaults: GetvitopsSeoOptions,\n props: SeoProps,\n ctx: SeoContext,\n): ResolvedSeo {\n const { site } = ctx;\n const siteName = props.siteName ?? defaults.siteName ?? null;\n\n // The template applies to a page's own title only, and only when that title\n // isn't already the site name. Two ways to get \"Acme · Acme\" otherwise: running\n // it over `defaultTitle`, and a homepage whose own title happens to equal the\n // site name — which is the common case, not an edge case.\n const templated = props.title && defaults.titleTemplate && props.title !== siteName;\n const fallbackTitle = defaults.defaultTitle ?? siteName;\n const title = props.title\n ? templated\n ? String(defaults.titleTemplate).replaceAll('%s', props.title)\n : props.title\n : fallbackTitle;\n\n /*\n * `og:title`/`twitter:title` take the page's title *without* the template.\n *\n * The two are read in different places. `<title>` is navigational — a browser\n * tab or a search result, competing with a dozen others, so it needs the brand\n * suffix to disambiguate. A social card is self-contained and already shows\n * `og:site_name` and the domain, so repeating the brand there is furniture:\n * \"Installation · Acme\" sitting directly above \"Acme\".\n *\n * They still derive from one value, which is the part that matters — the\n * component this replaces computed them in separate places and they drifted.\n * Pass `ogTitle` when a page wants a genuinely different social headline.\n */\n const socialTitle = props.ogTitle ?? props.title ?? fallbackTitle;\n\n const description = props.description ?? defaults.defaultDescription ?? null;\n\n const canonical = props.canonical\n ? absolute(props.canonical, site)\n : site\n ? new URL(ctx.pathname + (ctx.search ?? ''), site).href\n : null;\n\n const image = absolute(props.image, site) ?? absolute(defaults.openGraph?.image?.url, site);\n // Only inherit the default image's metadata when the page didn't bring its own\n // image — otherwise a page's image would carry the default's alt text.\n const usingDefaultImage = !props.image;\n const defaultImage = defaults.openGraph?.image;\n const imageAlt = props.imageAlt ?? (usingDefaultImage ? defaultImage?.alt : undefined);\n const imageWidth = props.imageWidth ?? (usingDefaultImage ? defaultImage?.width : undefined);\n const imageHeight = props.imageHeight ?? (usingDefaultImage ? defaultImage?.height : undefined);\n const imageType = props.imageType ?? (usingDefaultImage ? defaultImage?.type : undefined);\n\n const type = props.type ?? defaults.openGraph?.type ?? 'website';\n const locale = props.locale ?? defaults.openGraph?.locale;\n\n const openGraph: SeoPropertyTag[] = [];\n const og = (property: string, content: string | number | null | undefined) => {\n if (content !== null && content !== undefined && content !== '')\n openGraph.push({ property, content: String(content) });\n };\n\n og('og:type', type);\n og('og:title', socialTitle);\n og('og:description', description);\n og('og:url', canonical);\n og('og:site_name', siteName);\n og('og:locale', locale ? ogLocale(locale) : null);\n for (const alt of defaults.openGraph?.localeAlternates ?? [])\n og('og:locale:alternate', ogLocale(alt));\n og('og:image', image);\n if (image) {\n og('og:image:alt', imageAlt);\n og('og:image:width', imageWidth);\n og('og:image:height', imageHeight);\n og('og:image:type', imageType);\n }\n if (type === 'article' && props.article) {\n const a = props.article;\n og('article:published_time', a.publishedTime);\n og('article:modified_time', a.modifiedTime);\n og('article:expiration_time', a.expirationTime);\n for (const author of a.authors ?? []) og('article:author', author);\n og('article:section', a.section);\n for (const tag of a.tags ?? []) og('article:tag', tag);\n }\n\n const twitter: SeoNameTag[] = [];\n const tw = (name: string, content: string | null | undefined) => {\n if (content) twitter.push({ name, content });\n };\n // A card with a large image is only honoured if there *is* an image.\n tw('twitter:card', image ? 'summary_large_image' : (defaults.twitter?.card ?? 'summary'));\n tw('twitter:site', defaults.twitter?.site);\n tw('twitter:creator', props.twitterCreator ?? defaults.twitter?.creator);\n tw('twitter:title', socialTitle);\n tw('twitter:description', description);\n tw('twitter:image', image);\n tw('twitter:image:alt', image ? imageAlt : null);\n\n const verification: SeoNameTag[] = [];\n if (defaults.googleSiteVerification)\n verification.push({\n name: 'google-site-verification',\n content: defaults.googleSiteVerification,\n });\n if (defaults.bingSiteVerification)\n verification.push({ name: 'msvalidate.01', content: defaults.bingSiteVerification });\n\n return {\n title,\n socialTitle,\n description,\n canonical,\n robots: resolveRobots(props, defaults),\n openGraph,\n twitter,\n verification,\n alternates: props.alternates ?? [],\n };\n}\n"],"mappings":"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"}
|