@getvitops/astro 8.2.0 → 8.3.0-alpha.1

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/dist/index.mjs CHANGED
@@ -1,14 +1,17 @@
1
1
  import { t as resolveOrganizationName } from "./config-adapters-CbTywgEY.mjs";
2
2
  import { n as TRACKING_ENDPOINT, r as resolveTracking, t as TRACKING_COOKIE } from "./tracking-Cb3A0BrC.mjs";
3
+ import { createRequire } from "node:module";
3
4
  import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
5
  import { dirname, join, relative, resolve } from "node:path";
5
6
  import { fileURLToPath } from "node:url";
6
7
  import { isConfig, renderRobotsTxt, resolveConfig } from "@getvitops/generator";
7
8
  import { collectIconRefs, faviconLinks, generateFavicons, generateIconInclude, resolveIcon, scanFiles, writeFaviconManifest } from "@getvitops/utils";
8
9
  import vitopsPlugin from "@getvitops/vite";
10
+ import { REVIEWS_CACHE_PATH, readCache } from "@getvitops/utils/reviews";
9
11
  import { AD_PLATFORMS, AD_PROVIDER_KEYS, normalizeConversionLabels } from "@getvitops/utils/ads";
10
12
  import { fontProviders } from "astro/config";
11
13
  import { t } from "@getvitops/utils/content";
14
+ import { createHash } from "node:crypto";
12
15
  import { execFile } from "node:child_process";
13
16
  import { promisify } from "node:util";
14
17
 
@@ -383,6 +386,40 @@ function localeConsistencyWarnings(site, i18n) {
383
386
  return warnings;
384
387
  }
385
388
 
389
+ // src/tailwind-sources.ts
390
+ /**
391
+ * The `@source` paths a site's Tailwind needs so classes used only by installed packages'
392
+ * renderers are generated. Tailwind v4 does not scan `node_modules`, so `font-display` (used by
393
+ * `vitops.stats`) or a class in an `@getvitops/astro` component produced no rule and the page
394
+ * rendered at body size with no error anywhere.
395
+ *
396
+ * Resolution is injected so the choice is testable; an absent package is skipped, not an error —
397
+ * a site without `@getvitops/emdash` has nothing of its to scan.
398
+ */
399
+ /** Package → directory (relative to the package root) holding the markup that uses classes. */
400
+ const RENDERER_DIRS = {
401
+ "@getvitops/astro": "src/components",
402
+ "@getvitops/emdash": "src/astro"
403
+ };
404
+ /**
405
+ * @param cssDir directory of the emitted `tailwind.css`; `@source` paths are relative to it.
406
+ * @param resolvePackageJson returns the absolute path of a package's `package.json`, or throws.
407
+ */
408
+ function tailwindSources(cssDir, resolvePackageJson) {
409
+ const out = [];
410
+ for (const [pkg, dir] of Object.entries(RENDERER_DIRS)) {
411
+ let manifest;
412
+ try {
413
+ manifest = resolvePackageJson(pkg);
414
+ } catch {
415
+ continue;
416
+ }
417
+ const rel = relative(cssDir, join(dirname(manifest), dir)).split("\\").join("/");
418
+ out.push(rel.startsWith(".") ? rel : `./${rel}`);
419
+ }
420
+ return out;
421
+ }
422
+
386
423
  // src/seo-defaults.ts
387
424
  /** Drop `undefined`/empty-string keys so the result carries only what the config actually states. */
388
425
  function compact(value) {
@@ -545,6 +582,14 @@ function readSiteAds(configPath, siteEnv) {
545
582
  return null;
546
583
  }
547
584
  }
585
+ /** Read `site.security.turnstile.siteKey`. Best-effort, like `readSiteAds`. */
586
+ function readSiteTurnstileKey(configPath, siteEnv) {
587
+ try {
588
+ return resolveConfig(JSON.parse(readFileSync(configPath, "utf8")), siteEnv).site.security?.turnstile?.siteKey || null;
589
+ } catch {
590
+ return null;
591
+ }
592
+ }
548
593
  /**
549
594
  * Read `site.experiments` + `site.analytics.matomo.abVariantDimensions` out of
550
595
  * the site config, resolving each experiment's cookie name (default
@@ -687,6 +732,40 @@ function virtualIconsPlugin(data) {
687
732
  }
688
733
  };
689
734
  }
735
+ const REVIEWS_ID = "virtual:getvitops/reviews";
736
+ const RESOLVED_REVIEWS_ID = `\0${REVIEWS_ID}`;
737
+ /**
738
+ * The committed `.vitops/reviews.json` cache as a build-time import, for server-rendered
739
+ * components that cannot read the filesystem (the EmDash `vitops.reviews` block runs in a
740
+ * Worker — and, with pages prerendered, inside the Cloudflare adapter's local workerd).
741
+ *
742
+ * **An absent or unusable file serves an EMPTY cache, never an error.** A fresh scaffold has
743
+ * no `.vitops/reviews.json` (nothing has run `vitops reviews` yet) and a build must not fail
744
+ * for it; the block renders an editor placeholder instead. `readCache` already treats a
745
+ * corrupt or wrong-version file as absent, which is the same rule — the failure is silent
746
+ * here only because the consumer of this module reports it.
747
+ *
748
+ * Read in `load`, not at plugin construction, and the file is registered as a watch file, so
749
+ * a dev server picks up a re-run of `vitops reviews` without a restart.
750
+ */
751
+ function virtualReviewsPlugin(root) {
752
+ const path = resolve(root, REVIEWS_CACHE_PATH);
753
+ return {
754
+ name: "@getvitops/astro:virtual-reviews",
755
+ resolveId(id) {
756
+ return id === REVIEWS_ID ? RESOLVED_REVIEWS_ID : null;
757
+ },
758
+ load(id) {
759
+ if (id !== RESOLVED_REVIEWS_ID) return null;
760
+ this?.addWatchFile?.(path);
761
+ const cache = readCache(path) ?? {
762
+ version: 1,
763
+ locations: {}
764
+ };
765
+ return `export default ${JSON.stringify(cache)};`;
766
+ }
767
+ };
768
+ }
690
769
  /** Defaults live here so the component and the integration can't drift. */
691
770
  const ICON_DEFAULTS = {
692
771
  ui: "fa7-solid",
@@ -731,6 +810,46 @@ function readsAsConfig(path) {
731
810
  return false;
732
811
  }
733
812
  }
813
+ /** Whether the site config sets `site.icons.sprite` — the fact that makes the generator emit icons.svg. */
814
+ function siteWantsSprite(root, siteInput, siteEnv) {
815
+ if (!siteInput) return false;
816
+ try {
817
+ const cfg = resolveConfig(JSON.parse(readFileSync(resolve(root, siteInput), "utf8")), siteEnv ?? "production");
818
+ return Boolean(cfg.site.icons?.sprite);
819
+ } catch {
820
+ return false;
821
+ }
822
+ }
823
+ /**
824
+ * A standalone page hosting `<wc-theme-editor>`, for a host that cannot load the
825
+ * framework CSS itself (the EmDash admin embeds this in an iframe, so the design
826
+ * system never touches the admin's own styles). `?save=<url>` supplies the save
827
+ * endpoint, which is the host's to name, not this integration's.
828
+ */
829
+ function writeEditorHost(dest) {
830
+ mkdirSync(dest, { recursive: true });
831
+ writeFileSync(join(dest, "design-system-editor.html"), `<!doctype html>
832
+ <html lang="en">
833
+ <head>
834
+ <meta charset="utf-8">
835
+ <meta name="viewport" content="width=device-width, initial-scale=1">
836
+ <meta name="robots" content="noindex">
837
+ <title>Design system editor</title>
838
+ <link rel="stylesheet" href="/vitops/editor.css">
839
+ </head>
840
+ <body>
841
+ <script type="module">
842
+ await import('/vitops/editor.js');
843
+ const el = document.createElement('wc-theme-editor');
844
+ el.setAttribute('manifest', '/vitops/design-manifest.json');
845
+ const save = new URLSearchParams(location.search).get('save');
846
+ if (save && save.startsWith('/')) el.setAttribute('save-endpoint', save);
847
+ document.body.append(el);
848
+ <\/script>
849
+ </body>
850
+ </html>
851
+ `);
852
+ }
734
853
  function vitops(opts = {}) {
735
854
  return {
736
855
  name: "@getvitops/astro",
@@ -739,7 +858,7 @@ function vitops(opts = {}) {
739
858
  const publicDir = fileURLToPath(config.publicDir);
740
859
  const webComponents = opts.webComponents !== false;
741
860
  const editor = opts.editor === true;
742
- if (editor && !opts.css) logger.warn("editor: true needs a `css` config — it reads design-manifest.json.");
861
+ if ((editor || opts.editorManifest) && !opts.css) logger.warn("editor/editorManifest needs a `css` config — it reads design-manifest.json.");
743
862
  if (opts.legal && !opts.css) logger.warn("legal: needs a `css` config — the generation runs in the Vite plugin that `css` registers. Run `npx vitops legal` instead if you do not use `css`.");
744
863
  if (opts.media && !opts.css) logger.warn("media: needs a `css` config — the encoding runs in the Vite plugin that `css` registers. Run `npx vitops media` instead if you do not use `css`.");
745
864
  const cssInput = opts.css ? opts.css.input ?? "design-system.json" : void 0;
@@ -766,6 +885,7 @@ function vitops(opts = {}) {
766
885
  const analytics = resolveAnalytics(opts.analytics, { consent: consentEnabled });
767
886
  const trackingOpts = opts.tracking === true ? { enabled: true } : opts.tracking || (siteConsent?.tracking?.enabled ? siteConsent.tracking : void 0);
768
887
  const adsConfig = siteInput ? readSiteAds(resolve(root, siteInput), opts.site?.siteEnv ?? "production") : null;
888
+ const turnstileSiteKey = siteInput ? readSiteTurnstileKey(resolve(root, siteInput), opts.site?.siteEnv ?? "production") : null;
769
889
  const experimentsConfig = siteInput ? readSiteExperiments(resolve(root, siteInput), opts.site?.siteEnv ?? "production") : null;
770
890
  const detectedCategories = consentCategories([...analytics.tags, ...resolveAds(adsConfig?.properties, {
771
891
  consent: consentEnabled,
@@ -812,8 +932,15 @@ function vitops(opts = {}) {
812
932
  logger.info("generated favicons + manifest → public/");
813
933
  }
814
934
  let headFonts = [];
815
- if (opts.fonts) {
816
- const given = Array.isArray(opts.fonts) ? { families: opts.fonts } : typeof opts.fonts === "string" ? { input: opts.fonts } : opts.fonts === true ? {} : opts.fonts;
935
+ let fontsOpt = opts.fonts;
936
+ if (fontsOpt === void 0 && siteInput) try {
937
+ if ((resolveConfig(JSON.parse(readFileSync(resolve(root, siteInput), "utf8")), opts.site?.siteEnv ?? "production").site.fonts ?? []).length > 0) {
938
+ fontsOpt = true;
939
+ logger.info(`fonts: ${siteInput} declares site.fonts, so webfonts are on (pass fonts: false to opt out)`);
940
+ }
941
+ } catch {}
942
+ if (fontsOpt) {
943
+ const given = Array.isArray(fontsOpt) ? { families: fontsOpt } : typeof fontsOpt === "string" ? { input: fontsOpt } : fontsOpt === true ? {} : fontsOpt;
817
944
  const o = {
818
945
  ...given,
819
946
  ...given.input == null && given.families == null && siteInput ? {
@@ -851,11 +978,12 @@ function vitops(opts = {}) {
851
978
  "polyfills"
852
979
  ] : [];
853
980
  if (consentRuntime) bundles.push("consent.js");
854
- if (editor) bundles.push("editor.js");
981
+ if (editor || opts.editorManifest) bundles.push("editor.js");
855
982
  for (const f of bundles) {
856
983
  const src = join(coreDist, f);
857
984
  if (existsSync(src)) cpSync(src, join(dest, f), { recursive: true });
858
985
  }
986
+ if (opts.editorManifest) writeEditorHost(dest);
859
987
  logger.info("copied web-component bundles → public/vitops/");
860
988
  } else logger.warn(`@getvitops/core dist not found (${coreDist}); run build:core. Skipping WC copy.`);
861
989
  }
@@ -968,43 +1096,51 @@ function vitops(opts = {}) {
968
1096
  sprite: engine === "sprite" || o.sprite ? spriteHref : null
969
1097
  };
970
1098
  }
971
- updateConfig({ vite: { plugins: [virtualIconsPlugin(iconsData ?? {
972
- engine: "none",
973
- root,
974
- ui: ICON_DEFAULTS.ui,
975
- brand: ICON_DEFAULTS.brand,
976
- weight: null,
977
- overrides: {},
978
- sprite: null
979
- }), virtualHeadPlugin({
980
- favicons: !!opts.favicon,
981
- faviconLinks: opts.favicon ? faviconLinks({
982
- hasSvg,
983
- manifest: hasManifest
984
- }) : [],
985
- themeColor: opts.favicon?.themeColor ?? null,
986
- webComponents,
987
- wcBase: "/vitops",
988
- editor,
989
- sitemap: sitemapHref,
990
- fonts: headFonts,
991
- site: config.site ?? null,
992
- seo: seoDefaults,
993
- analytics: opts.analytics ?? {},
994
- consent: consentEnabled,
995
- consentCategories: offeredCategories,
996
- consentPolicyUrl: consentOpts.policyUrl ?? null,
997
- consentRuntime,
998
- tracking: tracking.enabled ? {
999
- enabled: true,
1000
- category: tracking.category,
1001
- cookies: tracking.cookies,
1002
- abVariantCookie: experimentsConfig?.primaryCookie ?? null
1003
- } : null,
1004
- experiments: experimentsConfig?.experiments ?? {},
1005
- matomoAbVariantDimensions: experimentsConfig?.matomoAbVariantDimensions ?? {},
1006
- ads: adsConfig
1007
- })] } });
1099
+ updateConfig({ vite: {
1100
+ ssr: { optimizeDeps: { include: ["@getvitops/astro > @getvitops/generator/icons-sprite"] } },
1101
+ plugins: [
1102
+ virtualReviewsPlugin(root),
1103
+ virtualIconsPlugin(iconsData ?? {
1104
+ engine: "none",
1105
+ root,
1106
+ ui: ICON_DEFAULTS.ui,
1107
+ brand: ICON_DEFAULTS.brand,
1108
+ weight: null,
1109
+ overrides: {},
1110
+ sprite: null
1111
+ }),
1112
+ virtualHeadPlugin({
1113
+ favicons: !!opts.favicon,
1114
+ faviconLinks: opts.favicon ? faviconLinks({
1115
+ hasSvg,
1116
+ manifest: hasManifest
1117
+ }) : [],
1118
+ themeColor: opts.favicon?.themeColor ?? null,
1119
+ webComponents,
1120
+ wcBase: "/vitops",
1121
+ editor,
1122
+ sitemap: sitemapHref,
1123
+ fonts: headFonts,
1124
+ site: config.site ?? null,
1125
+ seo: seoDefaults,
1126
+ analytics: opts.analytics ?? {},
1127
+ consent: consentEnabled,
1128
+ consentCategories: offeredCategories,
1129
+ consentPolicyUrl: consentOpts.policyUrl ?? null,
1130
+ consentRuntime,
1131
+ tracking: tracking.enabled ? {
1132
+ enabled: true,
1133
+ category: tracking.category,
1134
+ cookies: tracking.cookies,
1135
+ abVariantCookie: experimentsConfig?.primaryCookie ?? null
1136
+ } : null,
1137
+ experiments: experimentsConfig?.experiments ?? {},
1138
+ matomoAbVariantDimensions: experimentsConfig?.matomoAbVariantDimensions ?? {},
1139
+ ads: adsConfig,
1140
+ turnstileSiteKey
1141
+ })
1142
+ ]
1143
+ } });
1008
1144
  if (opts.css) {
1009
1145
  const format = opts.css.format ?? "tailwind";
1010
1146
  if (format === "design") throw new Error("[vitops] css.format: 'design' emits only DESIGN.md, not a stylesheet. Run `npx vitops generate --format design --out .` for that file, and set css.format to 'tailwind' | 'css' | 'bricks' here.");
@@ -1014,15 +1150,16 @@ function vitops(opts = {}) {
1014
1150
  ...opts.css.theme != null ? { theme: opts.css.theme } : {},
1015
1151
  format,
1016
1152
  out,
1017
- ...editor ? { editorManifestDir: publicDir } : {},
1018
- ...iconsData?.sprite ? { spriteDir: publicDir } : {},
1153
+ ...editor || opts.editorManifest ? { editorManifestDir: publicDir } : {},
1154
+ ...iconsData?.sprite || siteWantsSprite(root, siteInput, opts.site?.siteEnv) ? { spriteDir: publicDir } : {},
1019
1155
  ...opts.legal ? { legal: {
1020
1156
  ...opts.legal,
1021
1157
  input: legalInput
1022
1158
  } } : {},
1023
1159
  ...opts.media ? { media: opts.media } : {},
1024
1160
  ...opts.site ? { site: opts.site } : {},
1025
- ...opts.css.systemColorScheme != null ? { systemColorScheme: opts.css.systemColorScheme } : {}
1161
+ ...opts.css.systemColorScheme != null ? { systemColorScheme: opts.css.systemColorScheme } : {},
1162
+ ...format === "tailwind" ? { sources: tailwindSources(resolve(root, out), (pkg) => createRequire(resolve(root, "package.json")).resolve(`${pkg}/package.json`)) } : {}
1026
1163
  })];
1027
1164
  if (format === "tailwind") {
1028
1165
  let tailwindcss;
@@ -1048,6 +1185,124 @@ function vitops(opts = {}) {
1048
1185
  };
1049
1186
  }
1050
1187
 
1188
+ // src/inline-scripts.ts
1189
+ /**
1190
+ * The inline scripts `<Head />` emits, as plain strings.
1191
+ *
1192
+ * They live here rather than in `Head.astro`'s frontmatter for one reason: a
1193
+ * Content-Security-Policy that doesn't allow `'unsafe-inline'` has to name each
1194
+ * inline script by SHA-256, and the hash is of the exact emitted bytes. One
1195
+ * constant imported by both the component (which emits it) and `csp.ts` (which
1196
+ * hashes it) makes "the hash matches what the page serves" true by construction
1197
+ * rather than by a test remembering to compare two copies. `csp.test.ts` and the
1198
+ * container render test still assert it against real rendered HTML.
1199
+ *
1200
+ * No imports and no Node builtins: `Head.astro` runs in a Worker for any page
1201
+ * that is rendered on demand.
1202
+ */
1203
+ /**
1204
+ * Applies the remembered colour scheme before first paint.
1205
+ *
1206
+ * The key MUST match `WCColorSchemeToggle.STORAGE_KEY` in @getvitops/core;
1207
+ * `head.test.ts` asserts they stay in step. try/catch because localStorage
1208
+ * throws in private/partitioned contexts.
1209
+ */
1210
+ const THEME_SCRIPT = "try{var s=localStorage.getItem('vitops-color-scheme');if(s==='light'||s==='dark'){document.documentElement.dataset.theme=s;document.documentElement.style.colorScheme=s+' only'}}catch(e){}";
1211
+ /**
1212
+ * The consent gate's build-time signal: "something on this page may need
1213
+ * permission, and the real gate hasn't loaded yet."
1214
+ *
1215
+ * `consent.js` and `elements.js` are both deferred modules with no ordering
1216
+ * guarantee between them, so `<wc-color-scheme-toggle>` can upgrade and be clicked
1217
+ * before the gate exists. Without this, a caller probing for
1218
+ * `window.vitopsConsent` in that window would see nothing, correctly conclude
1219
+ * that sites without the gate should just store — and store. The stub closes the
1220
+ * window by making the gate's *existence* known synchronously in `<head>`, which
1221
+ * is the only place that beats every deferred bundle.
1222
+ *
1223
+ * It answers `false` to everything, because nothing is granted until the store
1224
+ * has been read, and queues what it was asked so `consent/runtime.ts` can replay
1225
+ * it on evaluation. `q` entries are `[category]` for `require()` and
1226
+ * `[category, resolve]` for `request()`. Same shape as the `dataLayer`/`gtag`
1227
+ * stub in `analytics.ts` and for the same reason.
1228
+ *
1229
+ * Emitted only when the gate is on: its absence is what tells a caller the site
1230
+ * has no gate at all.
1231
+ */
1232
+ const CONSENT_STUB = "window.vitopsConsent=window.vitopsConsent||{q:[],require:function(c){this.q.push([c]);return false},request:function(c){var s=this;return new Promise(function(r){s.q.push([c,r])})},granted:function(){return false},needed:function(){return false}};";
1233
+
1234
+ // src/csp.ts
1235
+ /**
1236
+ * Content-Security-Policy helpers for the inline scripts `<Head />` emits.
1237
+ *
1238
+ * `<Head />` writes up to two inline `<script>`s into every page: the pre-paint
1239
+ * colour-scheme bootstrap and the consent-gate stub. A policy without
1240
+ * `'unsafe-inline'` blocks both unless each is named by hash, and both are
1241
+ * constants (`inline-scripts.ts`), so their hashes are fixed per package version
1242
+ * and can sit in a static `_headers` file — which is exactly what the scaffolded
1243
+ * templates do, in `Content-Security-Policy-Report-Only` mode.
1244
+ *
1245
+ * Why hashes and not nonces: a nonce is per response, so it needs the page to be
1246
+ * rendered by a Worker on every request. The templates' pages are prerendered
1247
+ * static assets, which have no request to attach a nonce to. Hashes also survive
1248
+ * being cached.
1249
+ *
1250
+ * What this does NOT cover, and the policy says so rather than hiding it: scripts
1251
+ * the page gets from elsewhere (EmDash's own head tags, a server-island loader,
1252
+ * an editor's HTML block, the consent gate activating a third-party tag). Those
1253
+ * are what Report-Only is for — see the template's AGENTS.md.
1254
+ *
1255
+ * Node-only (`node:crypto`), so it is exported from the package root — a build
1256
+ * time import — and never from a Worker entry.
1257
+ */
1258
+ /** The CSP source expression for one inline script: `'sha256-<base64>'`. */
1259
+ function cspScriptHash(script) {
1260
+ return `'sha256-${createHash("sha256").update(script, "utf8").digest("base64")}'`;
1261
+ }
1262
+ /**
1263
+ * CSP hash sources for every inline script `<Head />` can emit.
1264
+ *
1265
+ * Defaults to ALL of them: listing a hash for a script the page doesn't emit is
1266
+ * harmless, while omitting one a page does emit is a violation.
1267
+ */
1268
+ function inlineScriptHashes(opts = {}) {
1269
+ const out = [];
1270
+ if (opts.webComponents !== false) out.push(cspScriptHash(THEME_SCRIPT));
1271
+ if (opts.consent !== false) out.push(cspScriptHash(CONSENT_STUB));
1272
+ return out;
1273
+ }
1274
+ /**
1275
+ * A strict starting policy for a prerendered site.
1276
+ *
1277
+ * `script-src` is `'self'` plus the `<Head />` hashes — no `'unsafe-inline'`.
1278
+ * `style-src` keeps `'unsafe-inline'` because Astro inlines scoped `<style>` and
1279
+ * components set `style=""` attributes; a style injection is far less dangerous
1280
+ * than a script one, and hashing every stylesheet would break on any edit.
1281
+ * `img-src` allows `https:` because editors paste images from anywhere.
1282
+ */
1283
+ function contentSecurityPolicy(opts = {}) {
1284
+ const list = (name, ...sources) => `${name} ${sources.flatMap((s) => s ?? []).join(" ")}`.trim();
1285
+ const directives = [
1286
+ "default-src 'self'",
1287
+ list("script-src", ["'self'"], inlineScriptHashes(opts.inline), opts.scriptSrc),
1288
+ "style-src 'self' 'unsafe-inline'",
1289
+ list("img-src", [
1290
+ "'self'",
1291
+ "data:",
1292
+ "https:"
1293
+ ], opts.imgSrc),
1294
+ "font-src 'self'",
1295
+ list("connect-src", ["'self'"], opts.connectSrc),
1296
+ list("frame-src", opts.frameSrc?.length ? opts.frameSrc : ["'none'"]),
1297
+ "object-src 'none'",
1298
+ "base-uri 'self'",
1299
+ "form-action 'self'",
1300
+ "frame-ancestors 'self'"
1301
+ ];
1302
+ if (opts.reportUri) directives.push(`report-uri ${opts.reportUri}`);
1303
+ return directives.join("; ");
1304
+ }
1305
+
1051
1306
  // src/lastmod.ts
1052
1307
  /**
1053
1308
  * Real per-page `<lastmod>` for the sitemap, derived from git.
@@ -1328,5 +1583,5 @@ function resolveSeo(defaults, props, ctx) {
1328
1583
  };
1329
1584
  }
1330
1585
 
1331
- export { TRACKING_COOKIE, consentCategories, vitops as default, gitLastmod, mergeSeoDefaults, resolveAds, resolveAnalytics, resolveSeo, resolveSeoDefaults, resolveTracking, routeFromPage, slugFromContent };
1586
+ export { TRACKING_COOKIE, consentCategories, contentSecurityPolicy, cspScriptHash, vitops as default, gitLastmod, inlineScriptHashes, mergeSeoDefaults, resolveAds, resolveAnalytics, resolveSeo, resolveSeoDefaults, resolveTracking, routeFromPage, slugFromContent };
1332
1587
  //# sourceMappingURL=index.mjs.map