@getvitops/create 0.9.0-alpha.3 → 0.9.0-alpha.5

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,83 @@
1
1
  # @getvitops/create
2
2
 
3
+ ## 0.9.0-alpha.5
4
+
5
+ ### Minor Changes
6
+
7
+ - 6b5ea17: **Leaner scaffolded `AGENTS.md`, and skills named by scope.** The EmDash template's `AGENTS.md` went from 212 to about 70 lines and the static one from 141 to about 50: only always-true rules stay (source of truth, generated CSS, branches, `vp`/`vpr`, where to look), and task-specific guidance moved into skills loaded when the task matches.
8
+
9
+ - **Breaking, `@getvitops/skills`: skills are renamed by scope.** `custom-blocks` → `emdash-custom-blocks`, `content-and-deploys` → `emdash-content-and-deploys`, `contact-form` → `emdash-contact-form`, `csp` → `vitops-csp`, `see-the-page` → `vitops-see-the-page`, `ads` → `vitops-ads`. `emdash-*` skills only apply to an EmDash site; `vitops-*` apply anywhere, and both prefixes are reserved for toolkit skills. New: `emdash-building-pages` (blocks, nesting, page opener, widget areas, escape hatches, layout primitives, `site.chrome`) and `vitops-consent` (split out of `vitops-design-system`, whose description and body now cover styling and tokens only). Existing sites: run `vp install` so `skills experimental_sync` relinks, and replace the per-skill `.agents/skills/<name>` / `.claude/skills/<name>` lines in `.gitignore` with `.agents/skills/vitops-*`, `.agents/skills/emdash-*` and the same two under `.claude/skills/`. A site's own skill must not use those prefixes.
10
+ - **`vitops agents sync`:** the block lists only skills that apply (a project without `emdash` in its dependencies gets no `emdash-*` ones, and they are not linked), summaries are cut at a clause rather than with an ellipsis, the pointer says `vp exec vitops docs`, and the version stamp is gone so the file no longer changes on every CLI bump.
11
+ - **`@getvitops/create`:** both templates ship the trimmed `AGENTS.md`; the project-naming steps moved into the `setup` skill and EmDash 1.x behaviours, legal-page replacement and the audit prerequisites into `emdash-content-and-deploys`. Existing sites: nothing to change beyond the skill rename; delete the sections you no longer need.
12
+
13
+ - 7069f93: **Agents now look at rendered pages with `playwright-cli`.** Both templates add `playwright` as a devDependency, run `playwright cli install --skills` from `prepare` (writing the upstream command reference to `.claude/skills/playwright-cli/`), ship `.playwright/cli.config.json`, allow `pnpm exec playwright cli`, and deny `mcp__claude-in-chrome__*` and `mcp__playwright__*` in `.claude/settings.json`. `@getvitops/skills` adds a short `see-the-page` skill (serve first, snapshot before screenshot, dark mode, mobile, no-JS) that defers to the upstream one. Existing sites: add `playwright` as a devDependency, extend `prepare`, and copy the settings. `vitops audit` now needs only `@axe-core/playwright lighthouse` added.
14
+ - 4d94c8b: **Added: `site.chrome` — a configurable masthead and nav behaviour.** Non-breaking.
15
+
16
+ - `site.chrome.masthead` (new `@getvitops/config` schema): `position` (`static|sticky|fixed`), `behavior` (`always|hide-on-scroll`), `surface` (`solid|glass|transparent|transparent-until-scrolled`), `tone` (a `colors.roles` key), `layout` (`full|inset|floating`), `width`, `elevation` (`shadow`, `border`, `onScroll`), `height` and `inset`. `height` and `inset` take fluid lengths (`calc()`, `clamp()`, `min()`, `max()`, with `cqi`/`vw`/… units), checked against a closed grammar because they are written into CSS. `validateConfig` rejects a `tone` or `shadow` the design system does not define (only when the default theme states them directly, not via `extends`).
17
+ - `site.chrome.nav`: `compact` (`drawer|dropdown`), `drawerSide`, `breakpoint`/`mobileBreakpoint`/`megaBreakpoint`, `depth`, `megaDepth`.
18
+ - `<Masthead config={site}>` (`@getvitops/astro`, slots `brand`/default/`actions`) and `resolveMasthead()` / `resolveNavOptions()`. A prop overrides one field per layout. The key is `chrome`/`masthead` on purpose: `<header>` is valid in any section and `banner` already means the announcement bar and the landmark role.
19
+ - `<SiteNav>` gains `compact="dropdown"` (a columnar panel anchored under the toggle), `drawerSide` (previously hardcoded to the end edge in Astro), `megaDepth` and `megaPanel`. `.sitenav--mega` now styles second-level items as column headings and has an optional featured column.
20
+ - `@getvitops/emdash`: `VitopsMegaPanel` fills that column from the widget area `mega-<item url slug>` (or a `mega-area:<name>` class on the menu item). No area means no column.
21
+ - `@getvitops/create` (emdash template): the layout renders through `<Masthead>`; `site.json` ships a sticky glass masthead with a shadow once scrolled. The hand-written `.navbar { block-size: 64px }` is gone (use `masthead.height`).
22
+ - CSS (`patterns/masthead.css`, `patterns/sitenav.css`): **all formats**, including tailwind, where the patterns are inlined into `@layer components`. Hide-on-scroll and the `*-on-scroll` states are `scroll-state()` container queries that only subtract from a settled (solid, elevated, visible) bar, so a browser without them (Firefox today) shows that bar rather than a transparent one. A sticky or fixed masthead sets `--header-offset` / `--top-offset` so anchors land below it.
23
+ - `@getvitops/generator`: new `TIERS.masthead`; `TIERS.sitenav` documents the new props.
24
+
25
+ ### Patch Changes
26
+
27
+ - bb61a35: **Call tracking numbers: show an ad visitor a number that attributes the phone call.** Two additive config blocks, no CSS, no breaking change — all formats unaffected.
28
+
29
+ - **`site.ads.google.callTracking`** — Google Ads _website call conversions_. `{ label, number, cache? }` makes the consent-gated Google tag fetch a Google forwarding number (Canada supported) and swap it in for visitors who arrived from a Google ad. Google replaces text only, so the capture script also rewrites the matching `tel:` href. Needs `pixelId` and `site.tracking.enabled`; rejected on any other platform (only Google can put a number on the website — Microsoft's exist only inside the ad, in the US/UK/AU).
30
+ - **`site.tracking.phoneNumbers`** — per-platform numbers for every other platform: the real `number`, `sources` keyed by `site.ads` platform (plus `default-paid`), and an informational `provider`. You provision the numbers with a call-tracking service (Twilio, CallRail, …). The landing URL decides first, then the `_ac` cookie, so a visitor who declines `marketing` sees a tracking number on the landing page only. An organic visitor is never swapped, and the server-rendered HTML always carries the real number.
31
+ - The `tel:` tap beacon now carries which number was shown (`phoneSource`), and the conversion notification says so.
32
+ - `vitops ads lint` flags a call-tracking provider that is not in `site.legal.privacyPolicy.processors`, and notes that `callTracking` plus `conversionLabel.call` can count one call twice (make one secondary in Google Ads).
33
+ - `@getvitops/utils/tracking` exports the pure `selectPhoneNumber()` and helpers; the capture script mirrors it and `tracking.test.ts` pins the two.
34
+ - `@getvitops/skills` gains a **`vitops-ads`** skill (overview plus a `call-tracking.md` topic). The templates gitignore synced skills by prefix (`vitops-*`, `emdash-*`), so it needs no `.gitignore` line of its own.
35
+
36
+ `@getvitops/core`, `@getvitops/vite` and `@getvitops/emdash` — no consumer-facing change in these packages.
37
+
38
+ - 74a87ed: **Scaffolded sites no longer exempt third-party tooling from the 3-day release cooldown.** The templates floated `vite-plus` and `emdash` on `latest` and exempted them (plus `@voidzero-dev/*`, `oxlint`, `oxfmt`) from `minimumReleaseAge`. An exempt package's exact-pinned dependencies are not exempt, so `vp create` then failed with `ERR_PNPM_NO_MATURE_MATCHING_VERSION` whenever vite-plus had just pinned a fresh `@oxc-project/*`. Now only `@getvitops/*` is exempt; `vite-plus` and `vite` (the `vite-plus-core` alias) are `^1.5.1` (the security fix for 1.5.0) and `emdash` and `@emdash-cms/cloudflare` are `^1.4.0`, all ranges, so pnpm resolves the newest version that has cleared the cooldown. Existing sites: in `pnpm-workspace.yaml` reduce `minimumReleaseAgeExclude` to `'@getvitops/*'` and change the `catalog` entries to `^1.5.1`; in `package.json` change those four dependencies from `latest` to those ranges (`vite` as `npm:@voidzero-dev/vite-plus-core@^1.5.1`).
39
+
40
+ ## 0.9.0-alpha.4
41
+
42
+ ### Minor Changes
43
+
44
+ - 99e66a2: Fixes from the vitops.ca build (alpha.3 findings 1–5, 19, 29).
45
+
46
+ - **Scaffold patterns.** Both templates now define `cta`, `link` and `details` in `patterns.items` (a `content` group too), and a `ui-accent` role. `.cta` had no CSS at all, so every hero, card and callout button rendered as plain text; a test now fails if a template omits a pattern the blocks emit. `vitops_accordion` renders a sprite chevron like `vitops_details`, and `details.css` styles an `.accordion` group (a divider and padded summaries from the `details` pattern tokens) — all formats.
47
+ - **Tone paints only on surfaces.** `.tone` now sets colour variables and `border-color`; the background comes from `.tone--tint` (callout, panel card, hero card, toned section/hero/nav/reviews, `theme`) or `.tone--solid`. A steps, stats or form block with a tone is no longer turned into a band. `ResolvedTone` gains `surface` (`tone tone--tint`). **If you styled `.tone` directly,** add `tone--tint` where you want the background. A `.cta` inside a tinted or filled tone takes the tone's solid. all formats.
48
+ - **`colors.tint: { mix: "8%" }`** derives the tint from the role's solid (`--tone-tint`) instead of its `muted` step, which turns brown for amber on a dark page. Off by default; not covered by the contrast contract.
49
+ - **Per-card backdrop.** `vitops_cards` and `vitops_hero` rows take a `theme` (tint or fill of a site colour), so one card of a pair can be tinted.
50
+ - **`colors.roles.<r>.acknowledgeContrast`**: a number of Lc a pairing may miss its APCA bar by and still only warn (e.g. pine's solid at Lc 59.5 against 60). A larger miss still fails.
51
+ - **Select:** where `appearance: base-select` is supported the wrapper's own chevron is hidden, so a select no longer shows two arrows, and the wrapper and control fill the field (`.custom-select` is `display: block`; add `display: inline-block` if you used it inline). all formats.
52
+
53
+ ### Patch Changes
54
+
55
+ - 00a9d86: - **`vitops audit` lets the page settle before axe reads colours.** At `load` Chromium can still be running the CSS transitions every pattern declares, so axe measured a filled button mid-way from its unstyled colour and reported ten `color-contrast` failures no visitor sees. The audit now waits (up to 2 s) for CSS transitions to finish first.
56
+ - `.carousel__hint` reads `text-surface-muted` instead of `x-muted` (the placeholder tier the contrast contract exempts), and its scroll-driven fade ends hidden instead of at 0.2 opacity (`--carousel-hint-dimmed` now defaults to 0): a hint parked at 20% was still text to an audit (1.34:1). all formats.
57
+ - The templates' `button` pattern becomes the toolkit's `btn` affordance (`fill: false`, transparent, text colour inherited), as `defaultSiteConfig()` already shipped: the old filled entry lost its white text to the new `link` pattern on `<a class="btn">` (dark green on green, 1.36:1). A filled call to action is `.cta`.
58
+ - The templates move the `info` and `danger` solids to step 700. Their step-600 solids (sky, rose) clear the generator's APCA bar for `text-on-<role>` (Lc 60) but not WCAG AA's 4.5:1 that axe and Lighthouse apply (4.2:1 and 4.4:1), so a `.cta-info` or a CTA inside an `info` callout failed the audit. Step 700 reads at 6.4:1 and 6.8:1 with the recomputed text. The APCA/WCAG gap in the contract itself is logged in TODO.md.
59
+ - d64fb20: Layout and spacing fixes from the vitops.ca build (findings 6–9).
60
+
61
+ - **`vitops_section` is the page opener.** New fields `cta_label`/`cta_href`/`secondary_label`/`secondary_href` (a `.cta` and a `.link` in the head), `*accent*` runs in the heading, and `stack: rhythm`, which puts `.rhythm` on `.section__inner` so head and body use the vertical-rhythm scale. With level 1 it replaces nesting a `vitops_hero` in a section, which rendered `section > .centered > … > section > .centered`. `vitops_hero` stays for a stand-alone hero with option cards. A plain (`stack: grid`) section also puts a gap between head and body now (`--section-gap`, default 2rem) instead of sitting flush. css/bricks/tailwind.
62
+ - **`.rhythm` spaces blocks.** A card row (`wc-cards`, classed `ul`/`ol`), accordion, `details`, form, table, figure or `.section__body` directly under a `.rhythm` is `--rhythm-block` (2) apart, not a paragraph apart; a button row (`p.cluster`) is `--rhythm-block-action` (1.6). Classless lists stay prose and a form's own field spacing is unchanged. A bare `<a>` that is a direct child of `.rhythm` is now `display: block; inline-size: fit-content`, since an inline box ignored `margin-top` and sat flush under the paragraph above (`.cta` and `.btn` are unaffected). all formats.
63
+ - **`rhythm-flex`** utility: an opt-in flex-column stack for the case the margin stack does not suit; margins apply to any child. all formats (`@utility` in tailwind).
64
+
65
+ - 322dd6a: Block fixes from the vitops.ca build (findings 11–13, 15, 17, 18, 21).
66
+
67
+ - **Steps.** The marker is an outlined disc in the tone (number and ring in `--tone-solid`, falling back to `currentColor`), so a steps block with no tone no longer shows black discs with no number. Items keep `align-content: start` and no `.rhythm li + li` margin, so markers in a row line up. Two modifiers through the block's `className`: `steps--cards` (each step in a card) and `steps--animated` (the connector draws and the marker fills as each step scrolls in; static under reduced motion or without scroll-driven animations). `steps` is now a config pattern: `patterns.items.steps` in `site.json` owns the card geometry (`content` group tokens) and the marker size (`--steps-marker-size`, `--steps-line-width`); both templates and the reference config ship an entry. all formats.
68
+ - **Stats.** `<wc-counter>` leaves a figure it cannot count as authored (`"1 in 6"`, `"24/7"`, `"—"`) instead of counting down from zero to a wrong value. `animate: false` on the block still turns the count-up off for a whole block.
69
+ - **Logos.** `vitops_logos` takes an optional `kicker`, `note` and `linkLabel`/`linkHref`; with any of them set, the row is wrapped in `.logos-block`. The list keeps the block's attributes either way.
70
+ - **Pricing.** Plans take `yearly` (a second price line) and `note` (under the plan name).
71
+ - **`<SiteFooter>`** has a `brand` named slot, rendered as the first column (`.sitefooter__brand`) in the site's own markup.
72
+
73
+ - 322dd6a: Tooling fixes from the vitops.ca build (findings 14, 23, 24, 25).
74
+
75
+ - **`vitops emdash form-template --from <seed.json | export.json> [--name contact] [--site site.json] [--dry]`** copies a `vitops_form` block into `site.templates` with the renderer's own field resolver, so the generated privacy policy and `check-prerender` know what the form collects. A re-run is a no-op, and the result is validated before it is written.
76
+ - `site.templates` form fields accept `checkboxes` (the block's spelling), and the template union now discriminates on `type`, so a bad entry is reported at its path instead of as a bare "Invalid input".
77
+ - **`prepare-db` fails fast and says why.** It validates `site.json` before starting the dev server, writes the server's output to `.vitops/prepare-db.log`, stops waiting as soon as the log shows an error, and prints the log's tail on failure, instead of waiting out a five-minute timeout with nothing to read.
78
+ - **`check-prerender`** skips the toolkit's own assets under `vitops/` (no more "0 h1" for the theme-editor page) and warns when the sitemap lists URLs the build produced no page for (a collection with no `urlPattern` is listed as `/<collection>/<slug>` while the site serves `/<slug>`).
79
+ - The emdash template ignores the duplicate `pnpm-@getvitops+skills-*` links `skills experimental_sync` leaves beside each skill; the `content-and-deploys` skill explains the sitemap warning.
80
+
3
81
  ## 0.9.0-alpha.3
4
82
 
5
83
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getvitops/create",
3
- "version": "0.9.0-alpha.3",
3
+ "version": "0.9.0-alpha.5",
4
4
  "description": "Project templates for the Vitops design system — scaffold wired-up sites with `vp create @getvitops`.",
5
5
  "homepage": "https://docs.vitops.ca",
6
6
  "bugs": "https://github.com/getvitops/design-system/issues",
@@ -19,7 +19,7 @@
19
19
  "access": "public"
20
20
  },
21
21
  "devDependencies": {
22
- "@getvitops/generator": "8.3.0-alpha.3"
22
+ "@getvitops/generator": "8.3.0-alpha.5"
23
23
  },
24
24
  "engines": {
25
25
  "node": ">=24"
@@ -59,3 +59,10 @@ report that verbatim rather than guessing at a fix. A step reported as
59
59
  "blocked" is not a failure to retry; it is waiting on one of the hard stops
60
60
  above, or on a `site.json` field the user still needs to fill in (stage 5,
61
61
  Configuration).
62
+
63
+ ## Naming the project
64
+
65
+ `vp create` takes the package name from the target directory and invents a random slug when none
66
+ is given, so pass one: `vp create @getvitops:<template> --directory <client-slug>`. `wrangler.jsonc`'s
67
+ `name` (and on EmDash the D1 `database_name` and R2 `bucket_name`) are separate and still say what
68
+ the template shipped; rename them to match before the first deploy.
@@ -1,6 +1,12 @@
1
1
  {
2
2
  "permissions": {
3
- "allow": ["mcp__astro-docs__search_astro_docs", "mcp__emdash-docs__search_docs"]
3
+ "allow": [
4
+ "mcp__astro-docs__search_astro_docs",
5
+ "mcp__emdash-docs__search_docs",
6
+ "Bash(pnpm exec playwright cli:*)",
7
+ "Bash(npx --no-install playwright cli:*)"
8
+ ],
9
+ "deny": ["mcp__claude-in-chrome__*", "mcp__playwright__*"]
4
10
  },
5
11
  "hooks": {
6
12
  "SessionStart": [
@@ -10,7 +10,7 @@
10
10
  #
11
11
  # Also runs on a `repository_dispatch` of type `emdash-publish`: pages are
12
12
  # prerendered, so a publish in the EmDash admin only goes live after a rebuild,
13
- # and an EmDash `content:afterPublish` hook POSTs that event (.agents/skills/content-and-deploys,
13
+ # and an EmDash `content:afterPublish` hook POSTs that event (.agents/skills/emdash-content-and-deploys,
14
14
  # "Publish-triggered rebuilds"). A dispatch runs this file from the DEFAULT branch
15
15
  # (`dev`), so `ref` pins the build to `main` — production's code, not dev's. It
16
16
  # still goes through the `production` environment, so it still waits for the
@@ -0,0 +1,7 @@
1
+ {
2
+ "browser": {
3
+ "browserName": "chromium",
4
+ "launchOptions": { "headless": true }
5
+ },
6
+ "outputDir": ".playwright-cli"
7
+ }
@@ -3,194 +3,72 @@
3
3
  Built with the Vitops design system (`@getvitops/*`); tokens and site facts live in `site.json`.
4
4
  Prefer its classes and patterns over hand-written CSS. Load the skill that matches the task:
5
5
 
6
- - [contact-form](.agents/skills/contact-form/SKILL.md) — Use when adding or changing a form, fixing submissions that don't arrive, setting up noti…
7
- - [content-and-deploys](.agents/skills/content-and-deploys/SKILL.md) — Use for any deploy or build problem, stale or missing content, "my publish isn't live", s…
8
- - [csp](.agents/skills/csp/SKILL.md) — Use when changing `public/_headers`, adding an embed, tracking snippet or third-party scr…
9
- - [custom-blocks](.agents/skills/custom-blocks/SKILL.md) — Use before creating, renaming or deleting any block type, any `src/components/blocks/*` r…
10
- - [vitops-design-system](.agents/skills/vitops-design-system/SKILL.md) — Use when styling or authoring pages or components (choosing utility/pattern classes, colo…
11
-
12
- Reference, rendered from this project: `vitops docs <topic>` (no topic lists them).
13
- <!-- managed by `vitops agents sync` · @getvitops/cli@8.3.0-alpha.2 — edit outside this block -->
6
+ - [emdash-building-pages](.agents/skills/emdash-building-pages/SKILL.md) — Use when authoring or changing a page's layout or content on this EmDash site — composing `vitops_*` blocks
7
+ - [emdash-contact-form](.agents/skills/emdash-contact-form/SKILL.md) — Use when adding or changing a form, fixing submissions that don't arrive, setting up notifications, CRM or Turnstile
8
+ - [emdash-content-and-deploys](.agents/skills/emdash-content-and-deploys/SKILL.md) — Use for any deploy or build problem, stale or missing content, "my publish isn't live", scheduled posts, previews
9
+ - [emdash-custom-blocks](.agents/skills/emdash-custom-blocks/SKILL.md) — Use before creating, renaming or deleting any block type, any `src/components/blocks/*` renderer
10
+ - [vitops-ads](.agents/skills/vitops-ads/SKILL.md) — Use when adding or changing a Google/Meta/LinkedIn/TikTok/Microsoft/Reddit/Pinterest/Snapchat pixel
11
+ - [vitops-consent](.agents/skills/vitops-consent/SKILL.md) — Use before gating, adding or changing any non-essential third-party tag, cookie, embed
12
+ - [vitops-csp](.agents/skills/vitops-csp/SKILL.md) — Use when changing `public/_headers`, adding an embed, tracking snippet or third-party script, turning the CSP on
13
+ - [vitops-design-system](.agents/skills/vitops-design-system/SKILL.md) — Use when styling or authoring pages or components (choosing utility/pattern classes, colours, spacing, typography)
14
+ - [vitops-see-the-page](.agents/skills/vitops-see-the-page/SKILL.md) — How to look at a rendered page of this site
15
+
16
+ Reference, rendered from this project: `vp exec vitops docs [topic]` (no topic lists them).
14
17
  <!-- vitops:end -->
15
18
 
16
- This is an EmDash CMS website (Astro on Cloudflare Workers, D1 + R2 — public pages
17
- prerendered, the Worker serving the admin and API) styled by the Vitops design system.
19
+ This is an EmDash CMS website (Astro on Cloudflare Workers, D1 + R2: public pages prerendered, the
20
+ Worker serving the admin and API) styled by the Vitops design system.
18
21
 
19
22
  ## Ground rules
20
23
 
21
- - `site.json` is the single source of truth: organization + site facts, and the design system
22
- under `designSystem.themes.default`. Never edit the generated stylesheet
23
- (`src/styles/tailwind.css` with the default `tailwind` format; `css.format` in
24
- `astro.config.mjs` picks it). It is rebuilt from `site.json` on every dev/build and gitignored.
25
- Only `src/styles/tailwind.css` and `tokens.json` are ignored: put your own CSS in a separate file
26
- (for example `src/site.css`, imported from the layout) and commit it.
24
+ - `site.json` is the single source of truth: organization + site facts, and the design system under
25
+ `designSystem.themes.default`. Never edit the generated stylesheet (`src/styles/tailwind.css`,
26
+ rebuilt from `site.json` on every dev/build, gitignored); put your own CSS in a separate file
27
+ (e.g. `src/site.css`, imported from the layout) and commit it.
27
28
  - Content lives in the EmDash database, not the repo. Read it with `getEmDashCollection` /
28
- `getEmDashEntry` from `emdash`; render Portable Text with `<PortableText />` from `emdash/ui`
29
- (the `vitops_*` blocks from `@getvitops/emdash` are merged in).
29
+ `getEmDashEntry` from `emdash`; render Portable Text with `<PortableText />` from `emdash/ui`.
30
30
  - **Where truth lives:** content (entries, media, menus) is **production's**, edited there; code,
31
31
  `site.json` and schema are **git's** (`dev` → `main`). The dev database is a disposable copy
32
- refreshed from prod, never promoted back. See `.agents/skills/content-and-deploys/SKILL.md`.
32
+ refreshed from prod, never promoted back.
33
33
  - `seed/seed.json` applies only to an empty database. To change a running site's content model, use
34
34
  the admin (`/_emdash/admin`) or its MCP server (`/_emdash/api/mcp`).
35
- - Hosting comes from `vitopsHosting()` in `astro.config.mjs` — never hard-code an adapter or
36
- d1/r2/sqlite. Switch with `vitopsHosting({ target })` or the `HOSTING` env var.
37
- - Keep the Worker cron trigger in `wrangler.jsonc`: it publishes content scheduled in the admin.
38
- - `src/assets/placeholder/` is wireframe stand-in imagery; delete it once real assets land.
35
+ - Hosting comes from `vitopsHosting()` in `astro.config.mjs`; never hard-code an adapter or
36
+ d1/r2/sqlite. Keep the Worker cron trigger in `wrangler.jsonc`: it publishes scheduled content.
39
37
  - Prefer framework classes (utilities, patterns, type roles like `font-heading`) over bespoke CSS.
40
- `vp exec vitops docs classes` prints the full class reference.
41
- - `SETUP.html` is the onboarding checklist (`.agents/skills/setup/`). Tick a box only for a step you
42
- completed, never for a human-only one (the Cloudflare API token, domain cutover, legal documents).
43
-
44
- ## Building pages
45
-
46
- - The editor's insert menu offers the `vitops_*` blocks. `vp exec vitops docs blocks` lists them
47
- with every field; `vp exec vitops docs components` lists every pattern, which tiers provide it
48
- and which block renders it.
49
- - **Nesting.** A block type with a `portableText` field, such as `vitops_section`, holds other
50
- blocks. A `vitops_*` block holds only scalar fields and repeaters (cards, gallery images,
51
- accordion items, slides), so it can't. EmDash's built-in `columns` doesn't render `vitops_*`
52
- children. For layout, nest in a section, use a `blocks` field, or hand-write the template with
53
- `@getvitops/astro` components (`Subgrid`, `Cards`, `Carousel`, …).
54
- - **Section, block or widget area?** Copy-and-edit content is an EmDash Section. The same content
55
- everywhere (announcement bar, pre-footer call to action, sidebar, footer columns) is a **widget
56
- area**: define it under Widgets in the admin and render it with `<VitopsWidgetArea name="…" />`
57
- from `@getvitops/emdash/astro` in the layout, or in page content with the `vitops_widgetArea`
58
- block. Page-specific content is blocks. An area can't hold another area, and prerendered pages
59
- show an edit after the next deploy (like menus).
60
- - **Escape hatches on every block:** `theme` (a backdrop from the site's colour roles, `surface` or
61
- `brand-primary:solid`, with the readable text for it picked by the design system, so contrast is
62
- not the editor's judgement), `anchor` (the `id`, for `#links`), `className` (extra classes; under
63
- the tailwind format only ones the site already uses) and `style` (CSS custom properties only,
64
- e.g. `--p-card: 2rem`; anything else is dropped). The same backdrop is available on a widget area
65
- (`<VitopsWidgetArea theme="…">`) and on your own markup (`themeAttrs('surface')` from
66
- `@getvitops/emdash/astro`, spread on the layout's `<body>`). How a role looks (flat, raised,
67
- glass) is the site's own role styling in `site.json`. Set `--header-offset` to the sticky
68
- header's height so anchors clear it.
69
- - Override a block per site with `<PortableText components={{ type: { 'vitops_cards': MyCards } }} />`;
70
- hide one with `vitopsEmdash({ blocks: { exclude: [...] } })`.
71
- - **One `<h1>` per page.** The template's page routes render the title as an `<h1>`, and a hero's
72
- heading level defaults to `h1`, so set a hero's level to `2` on those pages (or let the hero own
73
- the `h1`). Use `p` for a display heading that is not part of the outline. `vitops emdash build`
74
- warns on any page without exactly one `<h1>`.
75
- - Labels: `badge` is static (status, count, category); `tag` may be editable or dismissable. Don't
76
- invent `chip` or `pill`. The button class is `btn`.
77
- - **Before adding a block type** or a `src/components/blocks/*` renderer, read
78
- `.agents/skills/custom-blocks/SKILL.md`. Types can't be renamed or deleted once pages use them.
79
-
80
- ## Layout foundations — the answer, not a starting point
81
-
82
- | About to write… | Write instead |
83
- | ------------------------------------------------------- | --------------- |
84
- | a `wrap`/`wrapper`/`container`/`inner` class | **`centered`** |
85
- | `max-width` + `margin-inline: auto` | **`centered`** |
86
- | `padding-block` on a section | **`region`** |
87
- | margins between headings/paragraphs/lists | **`rhythm`** |
88
- | `display: grid` + `grid-template-columns: repeat(n, …)` | **`subgrid`** |
89
- | `repeat(auto-fit, minmax(…))` | **`grid-auto`** |
90
- | `display: flex` + `gap` for a row of controls | **`cluster`** |
91
-
92
- `centered` is a grid of _named tracks_: a child widens itself with `breakout`/`spotlight`/
93
- `fullbleed` without the parent knowing, which a bespoke `.wrap` can't do.
94
-
95
- - **More than one card means `subgrid`**, so headings and CTAs align across the set:
96
- `<ul class="subgrid subgrid-cols-3" role="list">` with `<li class="card subgrid-card">`, or
97
- `<Subgrid />` (renders the slot verbatim; author the `<li>`s).
98
- - **A card that is a link:** `stretched-link` inside it is zero-JS but kills text selection;
99
- `<Cards>` keeps selection and falls back to the same link with no JS. Never both. Never
100
- `<li><a class="card">`: the `li` is the grid item, so the tranches don't align.
101
- - `vpr lint:design` reports all of these and the pre-commit hook fails on them. Fix the class, not
102
- the CSS.
103
-
104
- ## Rendering and the contact form
105
-
106
- - Every public page is **prerendered**; the Worker handles `/_emdash/*` and `src/pages/api/track.ts`.
107
- An admin publish goes live after a rebuild. Run `vpr prepare-local-db` before a first local
108
- `vpr build`. Details, previews and server islands: `.agents/skills/content-and-deploys/SKILL.md`.
109
- - The contact form posts to `/api/track`. Set `site.notifications.email` before launch or the route
110
- refuses posts, and mirror form fields in `site.templates`. Details:
111
- `.agents/skills/contact-form/SKILL.md`.
112
- - `public/_headers` ships a Report-Only CSP. Read `.agents/skills/csp/SKILL.md` before enforcing it
113
- or adding embeds.
114
- - `vitops audit` needs optional packages and a browser, checked before it starts a server:
115
- `vp add -D playwright @axe-core/playwright lighthouse && vp exec playwright install chromium`.
116
- Its page list is the built sitemap, then the seed's menus.
117
-
118
- ## Branch model
119
-
120
- - Work on **`dev`**; every push auto-deploys the isolated `-dev` worker.
121
- - **`main`** is production and protected: never push or merge to it. It ships only through the
122
- `promote` skill (`.agents/skills/promote/`), and only when the user explicitly asks.
38
+ `vpr lint:design` fails the pre-commit hook on unresolvable classes and layout primitives you
39
+ hand-rolled (`centered`, `region`, `rhythm`, `subgrid`, `grid-auto`, `cluster` exist for those).
40
+ - `src/assets/placeholder/` is wireframe stand-in imagery; delete it once real assets land.
123
41
  - Sample legal pages (`/terms`, `/privacy`) are deliberately short, unfinished placeholders; don't
124
- "improve" them into something that reads as a finished policy. To replace them, fill in
125
- `site.json`'s `organization`/`site.legal`, run
126
- `vp exec vitops legal --format portable-text --out ./legal`, and paste each document into the page
127
- in the admin. It still needs review before launch.
128
-
129
- ## EmDash 1.x behaviours
130
-
131
- - Passkeys are bound to the hostname they were created on. After a domain cutover use **Settings →
132
- General → Change domain**, then **Continue on**.
133
- - Link entries by `entry.data.slug`, never `entry.id` (multilingual sites prefix `id` with a locale).
134
- - Submit `/sitemap.xml` to search engines, not a per-collection sitemap.
135
- - Clear a stuck D1 migration lock with `vp exec emdash migrate --take-over-lock <id>` (needs
136
- `--expected-target-fingerprint` non-interactively).
137
-
138
- ## Improving the toolkit
139
-
140
- You are also a source of feedback for `@getvitops/*`. When you struggle with a framework built-in,
141
- write a workaround, add bespoke CSS or a site-local block, or find a doc wrong or missing, and the
142
- need is **not** unique to this site, record it in `UPSTREAM.md` at the repo root (create it if
143
- absent). One entry each, with: what you tried, what failed or was missing, the workaround (file
144
- path), the change you'd propose, and the package (`@getvitops/emdash`, `astro`, `core`, `cli`,
145
- `create`, or docs). Mention new entries in your end-of-task summary. Don't log choices that are
146
- specific to this site, and never patch `node_modules`.
42
+ "improve" them into something that reads as a finished policy.
43
+ - `SETUP.html` is the onboarding checklist (the `setup` skill). Tick a box only for a step you
44
+ completed, never for a human-only one (the Cloudflare API token, domain cutover, legal documents).
147
45
 
148
- ## Docs and skills
46
+ ## Branches
149
47
 
150
- - `.mcp.json` preconfigures the Astro and EmDash docs MCP servers; search them before relying on
151
- pre-trained knowledge.
152
- - **Skills.** `@getvitops/skills` ships `custom-blocks`, `content-and-deploys`, `contact-form` and
153
- `csp`. `skills experimental_sync` (run by `prepare`, so by `vp install`) links them into
154
- `.agents/skills/` and `.claude/skills/`; they are gitignored and update with `vp update`. A skill
155
- you write yourself with the same name is never replaced. `promote` and `setup` are this site's own.
156
- - The EmDash skills (`building-emdash-site`, `creating-plugins`, `emdash-cli`, `upgrading-emdash`,
157
- two WordPress porting skills) are installed per project to match the pinned `emdash`. If
158
- `.agents/skills/building-emdash-site/` is missing: `vp dlx skills add emdash-cms/skills --skill '*' -y`.
159
- Refresh with `vp dlx skills update` after bumping `emdash`.
48
+ Work on **`dev`**; every push auto-deploys the isolated `-dev` worker. **`main`** is production and
49
+ protected: never push or merge to it. It ships only through the `promote` skill, and only when the
50
+ user explicitly asks. Shipping is not a package release: no changeset or version bump.
160
51
 
161
52
  ## Commands
162
53
 
163
- This is a **Vite+** project: use `vp` (and `vpr`), not `pnpm`/`npm`/`npx`. Scripts that wrap Astro
164
- go through `vpr` (`vpr dev`, `vpr build`, `vpr preview`), because `vp dev`/`vp build` skip Astro.
54
+ This is a **Vite+** project: use `vp` (and `vpr` for scripts), not `pnpm`/`npm`/`npx`. Scripts that
55
+ wrap Astro go through `vpr` (`vpr dev`, `vpr build`), because `vp dev`/`vp build` skip Astro.
165
56
  `vpr dev` returns immediately (the server runs in the background): check it with
166
57
  `vp exec astro dev status`, read `vp exec astro dev logs --follow`, stop with
167
- `vp exec astro dev stop`; the site is at <http://localhost:4321>.
168
-
169
- | To… | Run |
170
- | ---------------------------------------- | ------------------------------------------------------- |
171
- | install dependencies | `vp install` |
172
- | add / remove / update a package | `vp add <pkg>` · `vp remove <pkg>` · `vp update` |
173
- | run a `package.json` script | `vpr <script>` |
174
- | run a locally installed binary | `vp exec <bin>` (e.g. `vp exec vitops docs components`) |
175
- | run a one-off package without installing | `vp dlx <pkg>` |
176
- | format + lint + typecheck (files) | `vp check` |
177
- | typecheck Astro (`.astro` files) | `vpr typecheck` |
178
- | design-lint the markup | `vpr lint:design` |
58
+ `vp exec astro dev stop`; the site is at <http://localhost:4321>. `package.json` lists the scripts;
59
+ `vp check` and `vpr typecheck` verify a change. `vpr init:github` and `vpr init:hosting` are one-time
60
+ setup, only when the user asks.
179
61
 
180
- | Script | What it does |
181
- | ----------------------------------- | ------------------------------------------------------------------------------------------- |
182
- | `vpr dev` | local dev with local D1/R2 emulation (no Cloudflare account) |
183
- | `vpr deploy` | pull production content, build, `wrangler deploy` prod (provisions D1/R2 on first run) |
184
- | `vpr deploy:dev` | same, for the isolated dev worker (needs `DEV_D1_ID`) |
185
- | `vpr pull-content` | just the content pull |
186
- | `vpr prepare-local-db` | migrate and seed the local D1 the build prerenders from |
187
- | `vpr init:github` | one-time: create + push the GitHub repo (main + dev, dev default). Only when the user asks. |
188
- | `vpr init:hosting` | one-time: provision hosting (dev D1/R2 + repo secrets). Only when the user asks. |
189
- | `vp exec vitops validate site.json` | schema-check the site config |
62
+ ## Where to look
190
63
 
191
- Shipping is not a package release: no changeset or version bump. Commit to `dev`, then promote.
192
- (`vp` has no `vp change` command.)
193
-
194
- `vp create` names the package after the target directory (pass `--directory <client-slug>`).
195
- `wrangler.jsonc`'s `name`, D1 `database_name` and R2 `bucket_name` are separate and still say
196
- `my-emdash-site`; rename them to match before the first deploy.
64
+ - **Skills** are listed in the block at the top; load the one matching the task before starting.
65
+ - **Reference** for this project's own tokens, classes, blocks and config: `vp exec vitops docs`.
66
+ - `.mcp.json` preconfigures the Astro and EmDash docs MCP servers; search them before relying on
67
+ pre-trained knowledge. The EmDash skills (`building-emdash-site`, `emdash-cli`, …) are installed
68
+ per project to match the pinned `emdash`; if `.agents/skills/building-emdash-site/` is missing:
69
+ `vp dlx skills add emdash-cms/skills --skill '*' -y`.
70
+ - **Improving the toolkit:** when you work around a framework built-in, add bespoke CSS or a
71
+ site-local block, or find a doc wrong or missing, and the need is **not** unique to this site,
72
+ record it in `UPSTREAM.md` at the repo root: what you tried, what failed or was missing, the
73
+ workaround (file path), the change you'd propose, and the package. Mention new entries in your
74
+ end-of-task summary. Never patch `node_modules`.
@@ -51,7 +51,7 @@ get the prerendered `src/pages/404.astro` with a real 404 status.
51
51
 
52
52
  **Pages are prerendered.** `vpr build` writes static HTML for every published
53
53
  `pages` entry, so an edit in the admin goes live on the next rebuild rather than
54
- per request — see `.agents/skills/content-and-deploys` for how production content
54
+ per request — see `.agents/skills/emdash-content-and-deploys` for how production content
55
55
  reaches the build (`scripts/pull-content.mjs`) and for the publish-triggered rebuild.
56
56
  In `vpr dev` the pages read your local database, so edits appear straight away.
57
57
 
@@ -179,23 +179,23 @@ vpr build:css # regenerate the CSS manually
179
179
 
180
180
  ## Project map
181
181
 
182
- | Path | Purpose |
183
- | -------------------------------- | -------------------------------------------------------------------------------------------------- |
184
- | `site.json` | Org + site config; design tokens/patterns at `designSystem.themes.default` → generated CSS |
185
- | `seed/seed.json` | First-boot content model + starter content |
186
- | `src/worker.ts` | Worker entry (fetch + EmDash scheduled handler) |
187
- | `src/live.config.ts` | Astro live content collections wiring for EmDash |
188
- | `src/layouts/Layout.astro` | Site chrome: EmDash + Vitops head tags, nav from the CMS menu |
189
- | `src/pages/index.astro` | Home — prerendered from the `home` page entry |
190
- | `src/pages/[...slug].astro` | Prerendered once per published `pages` entry (`getStaticPaths`) |
191
- | `src/pages/404.astro` | Prerendered not-found page, served with a 404 status for any unmatched URL |
192
- | `public/_headers` | Report-Only CSP for the static pages (see `.agents/skills/csp`) |
193
- | `scripts/pull-content.mjs` | Loads production's D1 into the local build DB before the prerender (`deploy`/`deploy:dev`) |
194
- | `wrangler.jsonc` | Cloudflare bindings (D1/R2) + cron trigger |
195
- | `scripts/build-dev-wrangler.mjs` | Rewrites the built wrangler config for the isolated dev worker |
196
- | `SETUP.html` | Client-onboarding checklist — run `vpr sop` first (see "One-time setup") |
197
- | `.github/workflows/` | deploy-dev (push to `dev`), deploy-prod (push to `main`, or an `emdash-publish` dispatch), promote |
198
- | `.agents/skills/promote/` | Agent skill: dispatch the dev → main promotion |
199
- | `.agents/skills/custom-blocks/` | Agent skill: when (and how) to add a block type — compose, extend, local, upstream |
200
- | `.mcp.json` | Astro + EmDash docs MCP servers for coding agents |
201
- | `.claude/hooks/` | Claude Code web: Node 24 + pnpm setup on session start |
182
+ | Path | Purpose |
183
+ | -------------------------------------- | -------------------------------------------------------------------------------------------------- |
184
+ | `site.json` | Org + site config; design tokens/patterns at `designSystem.themes.default` → generated CSS |
185
+ | `seed/seed.json` | First-boot content model + starter content |
186
+ | `src/worker.ts` | Worker entry (fetch + EmDash scheduled handler) |
187
+ | `src/live.config.ts` | Astro live content collections wiring for EmDash |
188
+ | `src/layouts/Layout.astro` | Site chrome: EmDash + Vitops head tags, nav from the CMS menu |
189
+ | `src/pages/index.astro` | Home — prerendered from the `home` page entry |
190
+ | `src/pages/[...slug].astro` | Prerendered once per published `pages` entry (`getStaticPaths`) |
191
+ | `src/pages/404.astro` | Prerendered not-found page, served with a 404 status for any unmatched URL |
192
+ | `public/_headers` | Report-Only CSP for the static pages (see `.agents/skills/vitops-csp`) |
193
+ | `scripts/pull-content.mjs` | Loads production's D1 into the local build DB before the prerender (`deploy`/`deploy:dev`) |
194
+ | `wrangler.jsonc` | Cloudflare bindings (D1/R2) + cron trigger |
195
+ | `scripts/build-dev-wrangler.mjs` | Rewrites the built wrangler config for the isolated dev worker |
196
+ | `SETUP.html` | Client-onboarding checklist — run `vpr sop` first (see "One-time setup") |
197
+ | `.github/workflows/` | deploy-dev (push to `dev`), deploy-prod (push to `main`, or an `emdash-publish` dispatch), promote |
198
+ | `.agents/skills/promote/` | Agent skill: dispatch the dev → main promotion |
199
+ | `.agents/skills/emdash-custom-blocks/` | Agent skill: when (and how) to add a block type — compose, extend, local, upstream |
200
+ | `.mcp.json` | Astro + EmDash docs MCP servers for coding agents |
201
+ | `.claude/hooks/` | Claude Code web: Node 24 + pnpm setup on session start |
@@ -35,11 +35,14 @@ public/vitops/
35
35
  .DS_Store
36
36
 
37
37
  # agent skills linked from @getvitops/skills by `skills experimental_sync` (see skills-lock.json)
38
- .agents/skills/custom-blocks
39
- .agents/skills/content-and-deploys
40
- .agents/skills/contact-form
41
- .agents/skills/csp
42
- .claude/skills/custom-blocks
43
- .claude/skills/content-and-deploys
44
- .claude/skills/contact-form
45
- .claude/skills/csp
38
+ # (the `vitops-` and `emdash-` prefixes are reserved for toolkit skills)
39
+ .agents/skills/vitops-*
40
+ .agents/skills/emdash-*
41
+ .claude/skills/vitops-*
42
+ .claude/skills/emdash-*
43
+ # `skills experimental_sync` also leaves a duplicate `pnpm-@getvitops+skills-*` link per skill
44
+ .agents/skills/pnpm-*
45
+ .claude/skills/pnpm-*
46
+ # playwright-cli: skill regenerated by `prepare`; output is per-session scratch
47
+ .claude/skills/playwright-cli
48
+ .playwright-cli/
@@ -18,19 +18,19 @@
18
18
  "build:css": "vitops generate -i site.json -f tailwind -o src/styles",
19
19
  "lint:design": "vitops lint -i site.json -f tailwind --strict",
20
20
  "sop": "vitops generate -i site.json -f css -o .vitops/sop",
21
- "prepare": "vp config && skills experimental_sync -y -a claude-code --no-frozen-lockfile && vitops agents sync -i site.json"
21
+ "prepare": "vp config && skills experimental_sync -y -a claude-code --no-frozen-lockfile && playwright cli install --skills && vitops agents sync -i site.json"
22
22
  },
23
23
  "dependencies": {
24
24
  "@astrojs/cloudflare": "^14.0.0",
25
25
  "@astrojs/react": "^7.1.2",
26
- "@emdash-cms/cloudflare": "latest",
26
+ "@emdash-cms/cloudflare": "^1.4.0",
27
27
  "@getvitops/astro": "latest",
28
28
  "@getvitops/core": "latest",
29
29
  "@getvitops/emdash": "latest",
30
30
  "@getvitops/skills": "latest",
31
31
  "@tailwindcss/vite": "^4.3.3",
32
32
  "astro": "^7.4.4",
33
- "emdash": "latest",
33
+ "emdash": "^1.4.0",
34
34
  "react": "^19.3.1",
35
35
  "react-dom": "^19.3.1",
36
36
  "tailwindcss": "^4.3.3"
@@ -40,10 +40,11 @@
40
40
  "@cloudflare/workers-types": "^4.20260305.1",
41
41
  "@getvitops/cli": "latest",
42
42
  "@iconify-json/fa7-solid": "^1.2.5",
43
+ "playwright": "^1.62.1",
43
44
  "skills": "^1.7.6",
44
45
  "typescript": "^6.0.3",
45
- "vite": "latest",
46
- "vite-plus": "latest",
46
+ "vite": "npm:@voidzero-dev/vite-plus-core@^1.5.1",
47
+ "vite-plus": "^1.5.1",
47
48
  "wrangler": "^4.111.0"
48
49
  },
49
50
  "devEngines": {
@@ -6,25 +6,18 @@ allowBuilds:
6
6
  better-sqlite3: false
7
7
  sharp: false
8
8
 
9
- # Supply-chain hardening: cooldown on brand-new releases (toolchain exempt)
10
- # and no non-registry transitive sources.
9
+ # Supply-chain hardening: nothing installs within 3 days of its publish, and no non-registry
10
+ # transitive sources. Only our own lockstep `@getvitops/*` is exempt. Do NOT exempt a third-party
11
+ # toolchain: an exempt package's exact pins are not exempt (vite-plus pins `@oxc-project/*`
12
+ # exactly), so the install fails whenever one is fresher than the window. A security patch may be
13
+ # admitted with an exact `name@x.y.z` line and a dated reason; remove it once it matures.
11
14
  minimumReleaseAge: 4320
12
15
  minimumReleaseAgeExclude:
13
16
  - '@getvitops/*'
14
- - emdash
15
- - '@emdash-cms/*'
16
- - vite-plus
17
- - '@voidzero-dev/*'
18
- - oxlint
19
- - '@oxlint/*'
20
- - oxlint-tsgolint
21
- - '@oxlint-tsgolint/*'
22
- - oxfmt
23
- - '@oxfmt/*'
24
17
  blockExoticSubdeps: true
25
18
  catalog:
26
- vite: npm:@voidzero-dev/vite-plus-core@latest
27
- vite-plus: latest
19
+ vite: npm:@voidzero-dev/vite-plus-core@^1.5.1
20
+ vite-plus: ^1.5.1
28
21
  overrides:
29
22
  vite: 'catalog:'
30
23
  peerDependencyRules:
@@ -1,7 +1,7 @@
1
1
  # Cloudflare static-assets response headers. They apply to files served from the
2
2
  # static asset store — the prerendered pages — and NOT to responses the Worker
3
3
  # renders itself (the EmDash admin at /_emdash/*, its API, anything left
4
- # on-demand). See .agents/skills/csp.
4
+ # on-demand). See .agents/skills/vitops-csp.
5
5
  #
6
6
  # REPORT-ONLY on purpose: violations are logged to the browser console and
7
7
  # nothing is blocked, so this cannot break a page. Read the console on the
@@ -6,7 +6,7 @@
6
6
  // `astro build`. The Cloudflare adapter prerenders inside a local workerd with a
7
7
  // LOCAL D1 (`.wrangler/state`), never the deployed database — so on a CI runner
8
8
  // the build would see an empty, auto-seeded database and bake "No home page yet"
9
- // into every page. Production is the truth for content (see .agents/skills/content-and-deploys), so this
9
+ // into every page. Production is the truth for content (see .agents/skills/emdash-content-and-deploys), so this
10
10
  // copies production's D1 into the local one first.
11
11
  //
12
12
  // wrangler d1 export <db> --remote → .vitops/content.sql