@getvitops/astro 8.2.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 CHANGED
@@ -1,5 +1,104 @@
1
1
  # @getvitops/astro
2
2
 
3
+ ## 8.3.0-alpha.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 93d2067: New framework patterns and an `Entries` component for the EmDash block library.
8
+
9
+ **CSS, all formats** (css, bricks and inlined into tailwind's `components` layer; no existing class changes): `tone` (a shared tint/solid colour convention driven by `--tone-*` custom properties, plus `.accent` for a run inside a heading), `section` (head and heading-left `--split` layout), `steps` (numbered `<ol>` with a connector line), `stats` (figures and `--tiles`), `callout` (a call-to-action band), `logos` (a still partner grid) and `embed` (a consent-gated iframe frame). All are structural partials, classified `vitops.components`.
10
+
11
+ **`@getvitops/astro`:** new `Entries.astro` (`@getvitops/astro/components/Entries.astro`) emits `<wc-entries>` itself with the semantic heading + `<dl>` fallback.
12
+
13
+ **`@getvitops/generator`:** `TierEntry` gains `emdashExempt` (a reason a shipped web component has no EmDash block). `tiers.test.ts` now fails if a shipped `wc-*` has neither `emdash` nor `emdashExempt`, and the components doc and the docs site list the exemptions.
14
+
15
+ No other consumer-facing change in `@getvitops/core`, `@getvitops/utils`, `@getvitops/cli` or `@getvitops/vite` beyond the version bump.
16
+
17
+ - 161c7f3: A contact form that works end to end: `vitops setup turnstile`, `<wc-turnstile>`, the `vitops.form` EmDash block, and the route the emdash template ships to receive it.
18
+
19
+ **CSS, all formats** (no existing class changes): `forms.css` gains `.form-notice--target:not(:target)` (a message revealed only when the URL fragment names it, which is how a prerendered page shows a no-JS form's result) and `:where(wc-turnstile) { display: block }`.
20
+
21
+ **`@getvitops/core`:** new `<wc-turnstile sitekey="…">` in `elements.js`. It loads Cloudflare Turnstile on the first interaction with its form (no request to `challenges.cloudflare.com` for a visitor who only reads the page), holds an early submit until the token arrives, and leaves its slotted no-JS explanation in place until JS takes over. Turnstile is category `necessary`, so it is not behind the consent banner.
22
+
23
+ **`@getvitops/utils` / `@getvitops/cli`:** new `@getvitops/utils/turnstile` and `vitops setup turnstile [--dry] [--check] [--no-write] [--no-secret]`. It plans from the site's hostnames, creates a managed widget unless one already covers them (an existing widget is adopted, never edited, rotated or deleted), writes the public key to `site.security.turnstile.siteKey`, and pipes the one-time secret to `wrangler secret put VITOPS_TURNSTILE_SECRET` without writing it to any file. Needs `CLOUDFLARE_API_TOKEN` with **Account > Turnstile > Edit** and `CLOUDFLARE_ACCOUNT_ID`.
24
+
25
+ **`@getvitops/astro`:** `virtual:getvitops/head` gains `turnstileSiteKey` (read from `site.security.turnstile.siteKey`) so blocks can render the widget. No other change.
26
+
27
+ **`@getvitops/generator`:** `TIERS` gains a `turnstile` entry (wc-turnstile, claimed by `vitops.form`); `wc-multi-field`'s `emdashExempt` reason is reworded now the form block exists.
28
+
29
+ **`@getvitops/emdash`:** new `vitops.form` block: a field repeater (text, email, phone, long text, choose one, tick box), required flags, help text, success/error messages, optional Turnstile. It renders a semantic `<form method="post">` that works with no JavaScript, with a honeypot named per `createConversionRoute`'s defaults. A field named `phone` submits as `tel`, because the route reserves `phone` and `event`.
30
+
31
+ **`@getvitops/create`:** the emdash template ships `src/pages/api/track.ts` (`prerender = false`, `createConversionRoute()`), a seeded contact form, and `challenges.cloudflare.com` in both templates' Report-Only CSP (`script-src`, `frame-src`). The route refuses a post with nowhere to deliver and fails closed when a site key exists without `VITOPS_TURNSTILE_SECRET`. SETUP.html gains the Turnstile step. Existing sites: copy the route and the CSP change, then run `vitops setup turnstile`.
32
+
33
+ - 175d16e: **Added: `vitops.reviews` and `vitops.nav` EmDash blocks.** Non-breaking.
34
+
35
+ - **`vitops.reviews`** renders Google reviews from the committed `.vitops/reviews.json` cache (`vitops reviews`) as a grid, carousel or marquee of `<ReviewCard>`. Fields: location key, layout, columns, limit, lowest rating, order, featured ids, reviewer photos (off by default), tone/colour. Which reviews show is `selectReviews()`, the one shared decision. Review JSON-LD is not offered (Google treats self-marked-up reviews as ineligible for rich results). A new review reaches the page after `vitops reviews` is re-run, committed and the site rebuilt, not when an editor saves; with no cache file the block renders nothing (a note in `astro dev`).
36
+ - **`vitops.nav`** renders a named EmDash menu inside page content (site map, section links, footer-style columns), never the header. Fields: menu (picked from the site's menus via a new `menus/list` plugin route, with `footer-sitemap`/`footer-legal`/`primary` as the static fallback), accessible name, visible heading and level, layout `list | inline | columns | sitenav`, depth 1-3, tone/colour. It is a `<nav>` with a name that is never `Primary`, `Site map` or `Legal` (the layout's landmarks), `aria-current="page"`, no runtime JS; a missing or empty menu renders nothing.
37
+ - `@getvitops/astro`: **`virtual:getvitops/reviews`**, served by `vitops()`, is `.vitops/reviews.json` as a build-time import (an empty cache when absent or unreadable), because a Worker cannot read the file. `<ReviewCard>` now renders the review's source link (`.review__source`, `rel="nofollow noopener noreferrer"`), which the pattern already styled. **Fix:** `src/json-ld.ts` was missing from the published `files`, so any consumer importing `<ReviewCard>` (which composes `schemas/Review.astro`) failed to build with an unresolved import. `@getvitops/emdash` now needs this version of `@getvitops/astro` for the `vitops.reviews` block.
38
+ - `@getvitops/utils`: new Worker-safe subpath **`@getvitops/utils/reviews/select`** (`selectReviews` and the review types, no `node:fs`). `selectReviews` now takes only the display fields (`featured`, `minRating`, `limit`, `sort`), so a caller with no configured `locationId` can use it; existing calls are unchanged.
39
+ - `@getvitops/core`: new `.navlist` pattern in `patterns/sitenav.css` (`.navlist`, `__item`, `__link`, `__label`, `__sub`, `--inline`, `--columns`). **All formats** (css, bricks, tailwind). No existing class changes.
40
+ - `@getvitops/generator`: `TIERS.review` lists `vitops.reviews` and `TIERS.sitenav` lists `vitops.nav` (and `.navlist`), so `vitops docs components` and the docsite show them.
41
+
42
+ - 06d6112: Fix the EmDash `/design-system` admin page erroring on a tailwind site. It fetches `/vitops/design-manifest.json`, which the `tailwind` format never emitted and the integration only mirrored under `editor: true` (which also loads `editor.js` on every public page).
43
+
44
+ - **generator** (`tailwind` format only): `design-manifest.json` is now emitted alongside `tailwind.css`. No change to the CSS.
45
+ - **astro**: new `vitops({ editorManifest: true })` writes everything an outside host needs into `public/vitops/` (`design-manifest.json`, `editor.js`, a standalone `editor.css`, and `design-system-editor.html`) without loading the editor on public pages. `editor: true` still loads it from `<Head />`.
46
+ - **vite**: `editorManifestDir` also writes `vitops/editor.css`, from a throwaway `css`-format pass on tailwind sites.
47
+ - **emdash**: the `/design-system` admin page was unstyled and overflowed the admin, because the editor borrows the framework CSS and the admin never loads it. It now embeds `/vitops/design-system-editor.html` in an iframe, so the design system can't leak into the admin's styles. **Requires `editorManifest: true`** in `vitops({ … })`.
48
+ - **create**: the EmDash template sets `editorManifest: true`. Existing sites: add it to `vitops({ … })` in `astro.config.mjs`.
49
+
50
+ No consumer-facing change in the other toolchain packages.
51
+
52
+ - 224a731: **`@getvitops/astro` (no CSS change; all formats unaffected)**
53
+
54
+ - **`site.fonts` is no longer silently ignored.** `vitops()` now turns webfonts on by default when the site config (`site` option, or a `css.input` that is a site config) declares `site.fonts`, and logs that it did. Before, no `@font-face`, preload or warning was emitted unless you also passed `fonts: true`. Pass `fonts: false` to opt out. Migration: none — sites that already set `fonts` behave the same; sites that declared `site.fonts` without it will start loading those families.
55
+ - **The icon sprite is served when the site config asks for it.** `site.icons.sprite: true` made the generator write `icons.svg`, but only the `icons` integration option copied it to `public/vitops/`, so `/vitops/icons.svg` was never served. It is now mirrored whenever the site config sets it.
56
+ - **Strict CSP is achievable.** New build-time exports `inlineScriptHashes()`, `cspScriptHash()` and `contentSecurityPolicy()` give the SHA-256 hashes of the two inline scripts `<Head />` emits (colour-scheme bootstrap, consent-gate stub) and a starting policy built on them, with no `'unsafe-inline'` in `script-src`. The scripts moved from `Head.astro`'s frontmatter to `src/inline-scripts.ts` (shipped in the package) so the hash and the emitted bytes are the same constant; the rendered output is byte-identical.
57
+
58
+ **`@getvitops/create` (templates; existing scaffolded sites are not changed)**
59
+
60
+ - **emdash: every public page is prerendered.** `index.astro`, `[...slug].astro` (via `getStaticPaths` over the published `pages`) and `404.astro` set `prerender = true`. Unknown URLs now return the prerendered 404 with a real 404 status — the old `Astro.rewrite('/404')` from an on-demand route to a prerendered one was refused by Astro, so every unknown URL was a 500. `scripts/pull-content.mjs` (first step of `deploy`/`deploy:dev`) loads production's D1 into the local build database, because the Cloudflare adapter prerenders against a local D1 that is empty on CI; it fails closed. The deploy-prod workflow also runs on a `repository_dispatch` of type `emdash-publish`. Migration for an existing site: copy those pages, the script and the two `package.json` scripts, and add the trigger; previews of unpublished drafts no longer work on the public routes (see AGENTS.md).
61
+ - **emdash: seed menus are `primary` (Contact), `footer-sitemap` (Home, Contact) and `footer-legal` (Privacy Policy, Terms & Conditions, Cookie Notice)**, replacing `primary` (Home, About) and `footer` (Terms, Privacy), with placeholder `contact` and `cookies` pages so every seeded link resolves. The template Layout still reads the old menu names until it is updated; the seed only applies to an empty database, so existing sites keep their menus.
62
+ - **Both templates: `badge` and `tag` patterns (group `label`) are enabled, `button` carries `class: "btn"` so `<a class="btn">` is styled, and `neutral`/`surface` are declared as surface roles** (they were chromatic, which emitted no `.bg-neutral`/`text-neutral-muted`). css/bricks/tailwind output of a scaffolded site changes accordingly.
63
+ - **Both templates ship `public/_headers` with a `Content-Security-Policy-Report-Only` default** naming `<Head />`'s inline scripts by hash. It blocks nothing. `_headers` applies to static assets only, not Worker-rendered responses; see AGENTS.md for the EmDash admin and HTML-block trade-off.
64
+ - **Guidance is Vite+-first**: `AGENTS.md`, README, the `setup` skill, `SETUP.html` and the session hook use `vp`/`vpr`/`vp exec` instead of `pnpm`/`npx`, with one command table; `vpr dev` is documented as returning immediately. There is no `vp change`/`vp pm change` (checked against vp 1.5.0); the release flow is `dev` → promote.
65
+ - emdash ships `.agents/skills/custom-blocks/SKILL.md` (compose, extend, site-local, upstream; naming and lifecycle rules) and the favicon `themeColor`/`name` now derive from `site.json`.
66
+ - Label vocabulary: `badge` is a static label, `tag` is static or dynamic; `chip`/`pill` are not used.
67
+
68
+ **`@getvitops/cli`**: the bundled `vitops-design-system` skill points at `vp exec vitops docs <topic>`.
69
+
70
+ No consumer-facing change in `@getvitops/core`, `@getvitops/generator`, `@getvitops/utils` or `@getvitops/vite`.
71
+
72
+ - 55061e2: **Added: `<SiteNav>` and `<SiteFooter>` for Astro/EmDash sites.** Both are tier-1 markup over the existing `sitenav` pattern (no runtime JS: `<details>` accordions + Popover API drawer). Non-breaking.
73
+
74
+ - `<SiteNav {...await loadNavMenus(getMenu)} />` renders EmDash menu `primary` (the default: depth 3, dropdowns from the `md` container width, drawer below). If a `primary-mobile` menu exists it replaces the nav below `md` (depth 1); if `primary-mega` exists it replaces it from `xl` (mega panels). Adding the menu in the admin is the whole change. Only one tier is in the accessibility tree at a time. An item's `titleAttr` is its description (EmDash has no description field yet).
75
+ - `<SiteFooter sitemap legal name>` renders `footer-sitemap` + `footer-legal` + a copyright line.
76
+ - `@getvitops/astro/components/sitenav.ts` exports the pure helpers (`loadNavMenus`, `selectTiers`, ...).
77
+ - CSS (`patterns/sitenav.css`, **all formats**): tier visibility classes (`.sitenav-tier--below-*`/`--from-*`), `.sitenav--mega`, `.sitenav__desc`, `.sitefooter*`, `aria-current` styling, reduced-motion and forced-colors handling. Existing `.sitenav` rules are unchanged, so the Bricks element is unaffected.
78
+ - `@getvitops/create` (emdash template): the layout now renders header and footer through these components, with the colour-scheme toggle in the header. It reads menus `primary`, `footer-sitemap` and `footer-legal`; a template whose seed still has the old `footer` menu renders no footer links until the seed is updated.
79
+ - `@getvitops/generator`: `TIERS.sitenav` now lists the two Astro components.
80
+
81
+ ### Patch Changes
82
+
83
+ - efd4e72: Fix `astro dev` answering every route with `module is not defined` on the Cloudflare adapter. `<Icon>` imports `@getvitops/generator/icons-sprite`, which imports `@iconify/utils`; workerd's dev runner cannot load that un-prebundled, and Vite only pre-bundles what a site's own sources import, so a site that reached `<Icon>` only through a package's `.astro` file (for example the `vitops.reviews` EmDash block, which renders `<ReviewCard>`) crashed in dev while building fine. `vitops()` now adds the module to `ssr.optimizeDeps.include`. No change to builds or to output.
84
+
85
+ No consumer-facing change in the other toolchain packages.
86
+
87
+ - Updated dependencies [93d2067]
88
+ - Updated dependencies [161c7f3]
89
+ - Updated dependencies [175d16e]
90
+ - Updated dependencies [83d6f3c]
91
+ - Updated dependencies [892b204]
92
+ - Updated dependencies [06d6112]
93
+ - Updated dependencies [6297337]
94
+ - Updated dependencies [55061e2]
95
+ - Updated dependencies [b47df4d]
96
+ - @getvitops/core@8.3.0-alpha.0
97
+ - @getvitops/generator@8.3.0-alpha.0
98
+ - @getvitops/utils@8.3.0-alpha.0
99
+ - @getvitops/vite@8.3.0-alpha.0
100
+ - @getvitops/config@8.3.0-alpha.0
101
+
3
102
  ## 8.2.0
4
103
 
5
104
  ### Minor Changes
package/dist/index.d.mts CHANGED
@@ -584,6 +584,17 @@ interface GetvitopsOptions {
584
584
  * dev server, so on a static build that button simply isn't rendered.
585
585
  */
586
586
  editor?: boolean;
587
+ /**
588
+ * Ship what a host needs to embed `<wc-theme-editor>`, WITHOUT loading it from `<Head />`
589
+ * (default false). Writes into `public/vitops/`: `design-manifest.json`, `editor.js`, a
590
+ * standalone `editor.css`, and `design-system-editor.html`, a page that mounts the editor.
591
+ *
592
+ * For hosts outside the site's own pages, such as the EmDash admin
593
+ * (`@getvitops/emdash`'s `designSystemEditor`, which iframes that page so the design
594
+ * system never leaks into the admin's styles). Public pages don't pay for the bundle.
595
+ * Requires `css`.
596
+ */
597
+ editorManifest?: boolean;
587
598
  /**
588
599
  * Render the site's legal documents (privacy policy, terms, cookie notice)
589
600
  * from a site config, and re-render when it changes. Off unless provided.
@@ -801,6 +812,45 @@ interface ResolveAdsContext {
801
812
  }
802
813
  /** Resolve the configured ad properties into the tags `<Ads />` emits. */
803
814
  export declare function resolveAds(ads: GetvitopsAdsOptions | undefined, ctx?: ResolveAdsContext): ResolvedAds;
815
+ /** The CSP source expression for one inline script: `'sha256-<base64>'`. */
816
+ export declare function cspScriptHash(script: string): string;
817
+ interface InlineScriptOptions {
818
+ /** `<Head />` emits the colour-scheme bootstrap (its `webComponents` prop). Default true. */
819
+ webComponents?: boolean;
820
+ /** `<Head />` emits the consent-gate stub (a site with the gate on). Default true. */
821
+ consent?: boolean;
822
+ }
823
+ /**
824
+ * CSP hash sources for every inline script `<Head />` can emit.
825
+ *
826
+ * Defaults to ALL of them: listing a hash for a script the page doesn't emit is
827
+ * harmless, while omitting one a page does emit is a violation.
828
+ */
829
+ export declare function inlineScriptHashes(opts?: InlineScriptOptions): string[];
830
+ interface ContentSecurityPolicyOptions {
831
+ /** Extra `script-src` sources, e.g. a hash for your own inline script or an origin. */
832
+ scriptSrc?: string[];
833
+ /** Origins for `connect-src` beyond `'self'` (analytics endpoints, a form API). */
834
+ connectSrc?: string[];
835
+ /** Origins for `img-src` beyond `'self'`, `data:` and `https:`. */
836
+ imgSrc?: string[];
837
+ /** Origins for `frame-src` (embeds). Default none. */
838
+ frameSrc?: string[];
839
+ /** A `report-uri` endpoint. Omit to rely on the browser console. */
840
+ reportUri?: string;
841
+ /** Which `<Head />` inline scripts to allow. Default all. */
842
+ inline?: InlineScriptOptions;
843
+ }
844
+ /**
845
+ * A strict starting policy for a prerendered site.
846
+ *
847
+ * `script-src` is `'self'` plus the `<Head />` hashes — no `'unsafe-inline'`.
848
+ * `style-src` keeps `'unsafe-inline'` because Astro inlines scoped `<style>` and
849
+ * components set `style=""` attributes; a style injection is far less dangerous
850
+ * than a script one, and hashing every stylesheet would break on any edit.
851
+ * `img-src` allows `https:` because editors paste images from anywhere.
852
+ */
853
+ export declare function contentSecurityPolicy(opts?: ContentSecurityPolicyOptions): string;
804
854
  interface GitLastmodOptions {
805
855
  /** Directories to read history from (default `src/pages`, `src/content`, `src/data`). */
806
856
  dirs?: string[];
@@ -840,5 +890,5 @@ export declare function resolveSeoDefaults(config: Config, options?: ResolveSeoD
840
890
  * setting `openGraph.locale` in code doesn't discard the config's fallback image.
841
891
  */
842
892
  export declare function mergeSeoDefaults(fromConfig: GetvitopsSeoOptions, override: GetvitopsSeoOptions | undefined): GetvitopsSeoOptions;
843
- export { type AnalyticsStrategy, type ClarityOptions, type ConsentCategory, type GatedTag, type GetvitopsAdProperty, type GetvitopsAdsOptions, type GetvitopsAnalyticsOptions, type GetvitopsConsentOptions, type GetvitopsCssOptions, type GetvitopsFaviconOptions, type GetvitopsMediaOptions, type GetvitopsOptions, type GetvitopsSeoOptions, type GetvitopsSitemapChangeFreq, type GetvitopsSitemapEntry, type GetvitopsSitemapOptions, type GetvitopsTrackingOptions, type GitLastmodOptions, type GoogleAnalyticsOptions, type MatomoOptions, type OptionalConsentCategory, type PlausibleOptions, type ResolveAdsContext, type ResolveAnalyticsContext, type ResolveSeoDefaultsOptions, type ResolveTrackingContext, type ResolvedAdTag, type ResolvedAds, type ResolvedAnalytics, type ResolvedSeo, type ResolvedTag, type ResolvedTracking, type SeoContext, type SeoNameTag, type SeoOgType, type SeoPropertyTag, type SeoProps, type SeoTwitterCard, TRACKING_COOKIE, type TrackingCategory, resolveTracking };
893
+ export { type AnalyticsStrategy, type ClarityOptions, type ConsentCategory, type ContentSecurityPolicyOptions, type GatedTag, type GetvitopsAdProperty, type GetvitopsAdsOptions, type GetvitopsAnalyticsOptions, type GetvitopsConsentOptions, type GetvitopsCssOptions, type GetvitopsFaviconOptions, type GetvitopsMediaOptions, type GetvitopsOptions, type GetvitopsSeoOptions, type GetvitopsSitemapChangeFreq, type GetvitopsSitemapEntry, type GetvitopsSitemapOptions, type GetvitopsTrackingOptions, type GitLastmodOptions, type GoogleAnalyticsOptions, type InlineScriptOptions, type MatomoOptions, type OptionalConsentCategory, type PlausibleOptions, type ResolveAdsContext, type ResolveAnalyticsContext, type ResolveSeoDefaultsOptions, type ResolveTrackingContext, type ResolvedAdTag, type ResolvedAds, type ResolvedAnalytics, type ResolvedSeo, type ResolvedTag, type ResolvedTracking, type SeoContext, type SeoNameTag, type SeoOgType, type SeoPropertyTag, type SeoProps, type SeoTwitterCard, TRACKING_COOKIE, type TrackingCategory, resolveTracking };
844
894
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":["MediaOutputKind"],"sources":["../src/analytics.ts","../src/seo.ts","../src/integration.ts","../src/ads.ts","../src/lastmod.ts","../src/seo-defaults.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.mts","names":["MediaOutputKind"],"sources":["../src/analytics.ts","../src/seo.ts","../src/integration.ts","../src/ads.ts","../src/csp.ts","../src/lastmod.ts","../src/seo-defaults.ts"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -6,9 +6,11 @@ import { fileURLToPath } from "node:url";
6
6
  import { isConfig, renderRobotsTxt, resolveConfig } from "@getvitops/generator";
7
7
  import { collectIconRefs, faviconLinks, generateFavicons, generateIconInclude, resolveIcon, scanFiles, writeFaviconManifest } from "@getvitops/utils";
8
8
  import vitopsPlugin from "@getvitops/vite";
9
+ import { REVIEWS_CACHE_PATH, readCache } from "@getvitops/utils/reviews";
9
10
  import { AD_PLATFORMS, AD_PROVIDER_KEYS, normalizeConversionLabels } from "@getvitops/utils/ads";
10
11
  import { fontProviders } from "astro/config";
11
12
  import { t } from "@getvitops/utils/content";
13
+ import { createHash } from "node:crypto";
12
14
  import { execFile } from "node:child_process";
13
15
  import { promisify } from "node:util";
14
16
 
@@ -545,6 +547,14 @@ function readSiteAds(configPath, siteEnv) {
545
547
  return null;
546
548
  }
547
549
  }
550
+ /** Read `site.security.turnstile.siteKey`. Best-effort, like `readSiteAds`. */
551
+ function readSiteTurnstileKey(configPath, siteEnv) {
552
+ try {
553
+ return resolveConfig(JSON.parse(readFileSync(configPath, "utf8")), siteEnv).site.security?.turnstile?.siteKey || null;
554
+ } catch {
555
+ return null;
556
+ }
557
+ }
548
558
  /**
549
559
  * Read `site.experiments` + `site.analytics.matomo.abVariantDimensions` out of
550
560
  * the site config, resolving each experiment's cookie name (default
@@ -687,6 +697,40 @@ function virtualIconsPlugin(data) {
687
697
  }
688
698
  };
689
699
  }
700
+ const REVIEWS_ID = "virtual:getvitops/reviews";
701
+ const RESOLVED_REVIEWS_ID = `\0${REVIEWS_ID}`;
702
+ /**
703
+ * The committed `.vitops/reviews.json` cache as a build-time import, for server-rendered
704
+ * components that cannot read the filesystem (the EmDash `vitops.reviews` block runs in a
705
+ * Worker — and, with pages prerendered, inside the Cloudflare adapter's local workerd).
706
+ *
707
+ * **An absent or unusable file serves an EMPTY cache, never an error.** A fresh scaffold has
708
+ * no `.vitops/reviews.json` (nothing has run `vitops reviews` yet) and a build must not fail
709
+ * for it; the block renders an editor placeholder instead. `readCache` already treats a
710
+ * corrupt or wrong-version file as absent, which is the same rule — the failure is silent
711
+ * here only because the consumer of this module reports it.
712
+ *
713
+ * Read in `load`, not at plugin construction, and the file is registered as a watch file, so
714
+ * a dev server picks up a re-run of `vitops reviews` without a restart.
715
+ */
716
+ function virtualReviewsPlugin(root) {
717
+ const path = resolve(root, REVIEWS_CACHE_PATH);
718
+ return {
719
+ name: "@getvitops/astro:virtual-reviews",
720
+ resolveId(id) {
721
+ return id === REVIEWS_ID ? RESOLVED_REVIEWS_ID : null;
722
+ },
723
+ load(id) {
724
+ if (id !== RESOLVED_REVIEWS_ID) return null;
725
+ this?.addWatchFile?.(path);
726
+ const cache = readCache(path) ?? {
727
+ version: 1,
728
+ locations: {}
729
+ };
730
+ return `export default ${JSON.stringify(cache)};`;
731
+ }
732
+ };
733
+ }
690
734
  /** Defaults live here so the component and the integration can't drift. */
691
735
  const ICON_DEFAULTS = {
692
736
  ui: "fa7-solid",
@@ -731,6 +775,46 @@ function readsAsConfig(path) {
731
775
  return false;
732
776
  }
733
777
  }
778
+ /** Whether the site config sets `site.icons.sprite` — the fact that makes the generator emit icons.svg. */
779
+ function siteWantsSprite(root, siteInput, siteEnv) {
780
+ if (!siteInput) return false;
781
+ try {
782
+ const cfg = resolveConfig(JSON.parse(readFileSync(resolve(root, siteInput), "utf8")), siteEnv ?? "production");
783
+ return Boolean(cfg.site.icons?.sprite);
784
+ } catch {
785
+ return false;
786
+ }
787
+ }
788
+ /**
789
+ * A standalone page hosting `<wc-theme-editor>`, for a host that cannot load the
790
+ * framework CSS itself (the EmDash admin embeds this in an iframe, so the design
791
+ * system never touches the admin's own styles). `?save=<url>` supplies the save
792
+ * endpoint, which is the host's to name, not this integration's.
793
+ */
794
+ function writeEditorHost(dest) {
795
+ mkdirSync(dest, { recursive: true });
796
+ writeFileSync(join(dest, "design-system-editor.html"), `<!doctype html>
797
+ <html lang="en">
798
+ <head>
799
+ <meta charset="utf-8">
800
+ <meta name="viewport" content="width=device-width, initial-scale=1">
801
+ <meta name="robots" content="noindex">
802
+ <title>Design system editor</title>
803
+ <link rel="stylesheet" href="/vitops/editor.css">
804
+ </head>
805
+ <body>
806
+ <script type="module">
807
+ await import('/vitops/editor.js');
808
+ const el = document.createElement('wc-theme-editor');
809
+ el.setAttribute('manifest', '/vitops/design-manifest.json');
810
+ const save = new URLSearchParams(location.search).get('save');
811
+ if (save && save.startsWith('/')) el.setAttribute('save-endpoint', save);
812
+ document.body.append(el);
813
+ <\/script>
814
+ </body>
815
+ </html>
816
+ `);
817
+ }
734
818
  function vitops(opts = {}) {
735
819
  return {
736
820
  name: "@getvitops/astro",
@@ -739,7 +823,7 @@ function vitops(opts = {}) {
739
823
  const publicDir = fileURLToPath(config.publicDir);
740
824
  const webComponents = opts.webComponents !== false;
741
825
  const editor = opts.editor === true;
742
- if (editor && !opts.css) logger.warn("editor: true needs a `css` config — it reads design-manifest.json.");
826
+ if ((editor || opts.editorManifest) && !opts.css) logger.warn("editor/editorManifest needs a `css` config — it reads design-manifest.json.");
743
827
  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
828
  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
829
  const cssInput = opts.css ? opts.css.input ?? "design-system.json" : void 0;
@@ -766,6 +850,7 @@ function vitops(opts = {}) {
766
850
  const analytics = resolveAnalytics(opts.analytics, { consent: consentEnabled });
767
851
  const trackingOpts = opts.tracking === true ? { enabled: true } : opts.tracking || (siteConsent?.tracking?.enabled ? siteConsent.tracking : void 0);
768
852
  const adsConfig = siteInput ? readSiteAds(resolve(root, siteInput), opts.site?.siteEnv ?? "production") : null;
853
+ const turnstileSiteKey = siteInput ? readSiteTurnstileKey(resolve(root, siteInput), opts.site?.siteEnv ?? "production") : null;
769
854
  const experimentsConfig = siteInput ? readSiteExperiments(resolve(root, siteInput), opts.site?.siteEnv ?? "production") : null;
770
855
  const detectedCategories = consentCategories([...analytics.tags, ...resolveAds(adsConfig?.properties, {
771
856
  consent: consentEnabled,
@@ -812,8 +897,15 @@ function vitops(opts = {}) {
812
897
  logger.info("generated favicons + manifest → public/");
813
898
  }
814
899
  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;
900
+ let fontsOpt = opts.fonts;
901
+ if (fontsOpt === void 0 && siteInput) try {
902
+ if ((resolveConfig(JSON.parse(readFileSync(resolve(root, siteInput), "utf8")), opts.site?.siteEnv ?? "production").site.fonts ?? []).length > 0) {
903
+ fontsOpt = true;
904
+ logger.info(`fonts: ${siteInput} declares site.fonts, so webfonts are on (pass fonts: false to opt out)`);
905
+ }
906
+ } catch {}
907
+ if (fontsOpt) {
908
+ const given = Array.isArray(fontsOpt) ? { families: fontsOpt } : typeof fontsOpt === "string" ? { input: fontsOpt } : fontsOpt === true ? {} : fontsOpt;
817
909
  const o = {
818
910
  ...given,
819
911
  ...given.input == null && given.families == null && siteInput ? {
@@ -851,11 +943,12 @@ function vitops(opts = {}) {
851
943
  "polyfills"
852
944
  ] : [];
853
945
  if (consentRuntime) bundles.push("consent.js");
854
- if (editor) bundles.push("editor.js");
946
+ if (editor || opts.editorManifest) bundles.push("editor.js");
855
947
  for (const f of bundles) {
856
948
  const src = join(coreDist, f);
857
949
  if (existsSync(src)) cpSync(src, join(dest, f), { recursive: true });
858
950
  }
951
+ if (opts.editorManifest) writeEditorHost(dest);
859
952
  logger.info("copied web-component bundles → public/vitops/");
860
953
  } else logger.warn(`@getvitops/core dist not found (${coreDist}); run build:core. Skipping WC copy.`);
861
954
  }
@@ -968,43 +1061,51 @@ function vitops(opts = {}) {
968
1061
  sprite: engine === "sprite" || o.sprite ? spriteHref : null
969
1062
  };
970
1063
  }
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
- })] } });
1064
+ updateConfig({ vite: {
1065
+ ssr: { optimizeDeps: { include: ["@getvitops/astro > @getvitops/generator/icons-sprite"] } },
1066
+ plugins: [
1067
+ virtualReviewsPlugin(root),
1068
+ virtualIconsPlugin(iconsData ?? {
1069
+ engine: "none",
1070
+ root,
1071
+ ui: ICON_DEFAULTS.ui,
1072
+ brand: ICON_DEFAULTS.brand,
1073
+ weight: null,
1074
+ overrides: {},
1075
+ sprite: null
1076
+ }),
1077
+ virtualHeadPlugin({
1078
+ favicons: !!opts.favicon,
1079
+ faviconLinks: opts.favicon ? faviconLinks({
1080
+ hasSvg,
1081
+ manifest: hasManifest
1082
+ }) : [],
1083
+ themeColor: opts.favicon?.themeColor ?? null,
1084
+ webComponents,
1085
+ wcBase: "/vitops",
1086
+ editor,
1087
+ sitemap: sitemapHref,
1088
+ fonts: headFonts,
1089
+ site: config.site ?? null,
1090
+ seo: seoDefaults,
1091
+ analytics: opts.analytics ?? {},
1092
+ consent: consentEnabled,
1093
+ consentCategories: offeredCategories,
1094
+ consentPolicyUrl: consentOpts.policyUrl ?? null,
1095
+ consentRuntime,
1096
+ tracking: tracking.enabled ? {
1097
+ enabled: true,
1098
+ category: tracking.category,
1099
+ cookies: tracking.cookies,
1100
+ abVariantCookie: experimentsConfig?.primaryCookie ?? null
1101
+ } : null,
1102
+ experiments: experimentsConfig?.experiments ?? {},
1103
+ matomoAbVariantDimensions: experimentsConfig?.matomoAbVariantDimensions ?? {},
1104
+ ads: adsConfig,
1105
+ turnstileSiteKey
1106
+ })
1107
+ ]
1108
+ } });
1008
1109
  if (opts.css) {
1009
1110
  const format = opts.css.format ?? "tailwind";
1010
1111
  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,8 +1115,8 @@ function vitops(opts = {}) {
1014
1115
  ...opts.css.theme != null ? { theme: opts.css.theme } : {},
1015
1116
  format,
1016
1117
  out,
1017
- ...editor ? { editorManifestDir: publicDir } : {},
1018
- ...iconsData?.sprite ? { spriteDir: publicDir } : {},
1118
+ ...editor || opts.editorManifest ? { editorManifestDir: publicDir } : {},
1119
+ ...iconsData?.sprite || siteWantsSprite(root, siteInput, opts.site?.siteEnv) ? { spriteDir: publicDir } : {},
1019
1120
  ...opts.legal ? { legal: {
1020
1121
  ...opts.legal,
1021
1122
  input: legalInput
@@ -1048,6 +1149,124 @@ function vitops(opts = {}) {
1048
1149
  };
1049
1150
  }
1050
1151
 
1152
+ // src/inline-scripts.ts
1153
+ /**
1154
+ * The inline scripts `<Head />` emits, as plain strings.
1155
+ *
1156
+ * They live here rather than in `Head.astro`'s frontmatter for one reason: a
1157
+ * Content-Security-Policy that doesn't allow `'unsafe-inline'` has to name each
1158
+ * inline script by SHA-256, and the hash is of the exact emitted bytes. One
1159
+ * constant imported by both the component (which emits it) and `csp.ts` (which
1160
+ * hashes it) makes "the hash matches what the page serves" true by construction
1161
+ * rather than by a test remembering to compare two copies. `csp.test.ts` and the
1162
+ * container render test still assert it against real rendered HTML.
1163
+ *
1164
+ * No imports and no Node builtins: `Head.astro` runs in a Worker for any page
1165
+ * that is rendered on demand.
1166
+ */
1167
+ /**
1168
+ * Applies the remembered colour scheme before first paint.
1169
+ *
1170
+ * The key MUST match `WCColorSchemeToggle.STORAGE_KEY` in @getvitops/core;
1171
+ * `head.test.ts` asserts they stay in step. try/catch because localStorage
1172
+ * throws in private/partitioned contexts.
1173
+ */
1174
+ 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){}";
1175
+ /**
1176
+ * The consent gate's build-time signal: "something on this page may need
1177
+ * permission, and the real gate hasn't loaded yet."
1178
+ *
1179
+ * `consent.js` and `elements.js` are both deferred modules with no ordering
1180
+ * guarantee between them, so `<wc-color-scheme-toggle>` can upgrade and be clicked
1181
+ * before the gate exists. Without this, a caller probing for
1182
+ * `window.vitopsConsent` in that window would see nothing, correctly conclude
1183
+ * that sites without the gate should just store — and store. The stub closes the
1184
+ * window by making the gate's *existence* known synchronously in `<head>`, which
1185
+ * is the only place that beats every deferred bundle.
1186
+ *
1187
+ * It answers `false` to everything, because nothing is granted until the store
1188
+ * has been read, and queues what it was asked so `consent/runtime.ts` can replay
1189
+ * it on evaluation. `q` entries are `[category]` for `require()` and
1190
+ * `[category, resolve]` for `request()`. Same shape as the `dataLayer`/`gtag`
1191
+ * stub in `analytics.ts` and for the same reason.
1192
+ *
1193
+ * Emitted only when the gate is on: its absence is what tells a caller the site
1194
+ * has no gate at all.
1195
+ */
1196
+ 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}};";
1197
+
1198
+ // src/csp.ts
1199
+ /**
1200
+ * Content-Security-Policy helpers for the inline scripts `<Head />` emits.
1201
+ *
1202
+ * `<Head />` writes up to two inline `<script>`s into every page: the pre-paint
1203
+ * colour-scheme bootstrap and the consent-gate stub. A policy without
1204
+ * `'unsafe-inline'` blocks both unless each is named by hash, and both are
1205
+ * constants (`inline-scripts.ts`), so their hashes are fixed per package version
1206
+ * and can sit in a static `_headers` file — which is exactly what the scaffolded
1207
+ * templates do, in `Content-Security-Policy-Report-Only` mode.
1208
+ *
1209
+ * Why hashes and not nonces: a nonce is per response, so it needs the page to be
1210
+ * rendered by a Worker on every request. The templates' pages are prerendered
1211
+ * static assets, which have no request to attach a nonce to. Hashes also survive
1212
+ * being cached.
1213
+ *
1214
+ * What this does NOT cover, and the policy says so rather than hiding it: scripts
1215
+ * the page gets from elsewhere (EmDash's own head tags, a server-island loader,
1216
+ * an editor's HTML block, the consent gate activating a third-party tag). Those
1217
+ * are what Report-Only is for — see the template's AGENTS.md.
1218
+ *
1219
+ * Node-only (`node:crypto`), so it is exported from the package root — a build
1220
+ * time import — and never from a Worker entry.
1221
+ */
1222
+ /** The CSP source expression for one inline script: `'sha256-<base64>'`. */
1223
+ function cspScriptHash(script) {
1224
+ return `'sha256-${createHash("sha256").update(script, "utf8").digest("base64")}'`;
1225
+ }
1226
+ /**
1227
+ * CSP hash sources for every inline script `<Head />` can emit.
1228
+ *
1229
+ * Defaults to ALL of them: listing a hash for a script the page doesn't emit is
1230
+ * harmless, while omitting one a page does emit is a violation.
1231
+ */
1232
+ function inlineScriptHashes(opts = {}) {
1233
+ const out = [];
1234
+ if (opts.webComponents !== false) out.push(cspScriptHash(THEME_SCRIPT));
1235
+ if (opts.consent !== false) out.push(cspScriptHash(CONSENT_STUB));
1236
+ return out;
1237
+ }
1238
+ /**
1239
+ * A strict starting policy for a prerendered site.
1240
+ *
1241
+ * `script-src` is `'self'` plus the `<Head />` hashes — no `'unsafe-inline'`.
1242
+ * `style-src` keeps `'unsafe-inline'` because Astro inlines scoped `<style>` and
1243
+ * components set `style=""` attributes; a style injection is far less dangerous
1244
+ * than a script one, and hashing every stylesheet would break on any edit.
1245
+ * `img-src` allows `https:` because editors paste images from anywhere.
1246
+ */
1247
+ function contentSecurityPolicy(opts = {}) {
1248
+ const list = (name, ...sources) => `${name} ${sources.flatMap((s) => s ?? []).join(" ")}`.trim();
1249
+ const directives = [
1250
+ "default-src 'self'",
1251
+ list("script-src", ["'self'"], inlineScriptHashes(opts.inline), opts.scriptSrc),
1252
+ "style-src 'self' 'unsafe-inline'",
1253
+ list("img-src", [
1254
+ "'self'",
1255
+ "data:",
1256
+ "https:"
1257
+ ], opts.imgSrc),
1258
+ "font-src 'self'",
1259
+ list("connect-src", ["'self'"], opts.connectSrc),
1260
+ list("frame-src", opts.frameSrc?.length ? opts.frameSrc : ["'none'"]),
1261
+ "object-src 'none'",
1262
+ "base-uri 'self'",
1263
+ "form-action 'self'",
1264
+ "frame-ancestors 'self'"
1265
+ ];
1266
+ if (opts.reportUri) directives.push(`report-uri ${opts.reportUri}`);
1267
+ return directives.join("; ");
1268
+ }
1269
+
1051
1270
  // src/lastmod.ts
1052
1271
  /**
1053
1272
  * Real per-page `<lastmod>` for the sitemap, derived from git.
@@ -1328,5 +1547,5 @@ function resolveSeo(defaults, props, ctx) {
1328
1547
  };
1329
1548
  }
1330
1549
 
1331
- export { TRACKING_COOKIE, consentCategories, vitops as default, gitLastmod, mergeSeoDefaults, resolveAds, resolveAnalytics, resolveSeo, resolveSeoDefaults, resolveTracking, routeFromPage, slugFromContent };
1550
+ export { TRACKING_COOKIE, consentCategories, contentSecurityPolicy, cspScriptHash, vitops as default, gitLastmod, inlineScriptHashes, mergeSeoDefaults, resolveAds, resolveAnalytics, resolveSeo, resolveSeoDefaults, resolveTracking, routeFromPage, slugFromContent };
1332
1551
  //# sourceMappingURL=index.mjs.map