@getvitops/create 0.9.0-alpha.0 → 0.9.0-alpha.2

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.
@@ -3,347 +3,179 @@ prerendered, the Worker serving the admin and API) styled by the Vitops design s
3
3
 
4
4
  ## Ground rules
5
5
 
6
- - `site.json` is the single source of truth — organization + site facts, and
7
- the design system (colors, type, spacing, patterns, animations) under
8
- `designSystem.themes.default`. Never edit `src/styles/tailwind.css` — it is
9
- generated from `site.json` by the `vitops()` integration on every dev/build
10
- and is gitignored. (There is no separate `design-system.json`: a scaffolded
11
- config can't merely reference a sibling file, so the token set lives inside
12
- `site.json` — see `AGENTS.md`'s "Client onboarding" section in the toolchain
13
- repo for why.)
14
- - `src/assets/placeholder/` holds neutral wireframe stand-ins (square,
15
- portrait, landscape; SVG + PNG) for layout work before real imagery
16
- exists. Delete the directory once real assets land — it's separate from
17
- `src/assets/logo.svg`, the actual brand asset.
18
- - Content lives in the EmDash database, not in the repo. Query it with
19
- `getEmDashCollection` / `getEmDashEntry` from `emdash`; render Portable Text
20
- with `<PortableText />` from `emdash/ui` (the Vitops blocks from
21
- `@getvitops/emdash` are merged in automatically).
22
- - **Building pages from patterns:** the editor's insert menu offers the `vitops.*` blocks —
23
- link/button, image compare, copy snippet, banner, disclosure, accordion, hero, carousel, gallery,
24
- cards, marquee (`vp exec vitops docs blocks` lists them with every field). Repeating content (cards, gallery images, accordion items, carousel slides) is
25
- a repeater field. A block cannot contain other blocks, and a `vitops.*` block inside EmDash's
26
- built-in `columns` does not render — for layout (a section, a split, a hero) use a `blocks`
27
- field, or hand-write the page template with `@getvitops/astro` components
28
- (`Subgrid`, `Cards`, `Carousel`, …). `vp exec vitops docs components` lists every pattern, which
29
- tiers provide it, and which block renders it. Override a block's rendering per site with
30
- `<PortableText components={{ type: { 'vitops.cards': MyCards } }} />`; hide one with
31
- `vitopsEmdash({ blocks: { exclude: [...] } })` in `astro.config.mjs`.
32
- - `seed/seed.json` only applies against an empty database: structure
33
- (collections, menus, settings) on first boot; the `content` entries only
34
- when the setup wizard's "include sample content" option is chosen. Changing
35
- the seed does not migrate an existing site — use the admin
36
- (`/_emdash/admin`) or the site's MCP server (`/_emdash/api/mcp`) to change
37
- the content model of a running site.
38
- - Prefer framework CSS classes (utilities, patterns, type roles like
39
- `font-heading`) over bespoke CSS. Run `vp exec vitops agents` to append the
40
- full generated class-vocabulary reference to this file.
41
- - **Layout foundations — these are the answer, not a starting point.** Six
42
- substitutions carry the structure of nearly every page:
43
-
44
- | About to write… | Write instead |
45
- | ------------------------------------------------------- | --------------- |
46
- | a `wrap`/`wrapper`/`container`/`inner` class | **`centered`** |
47
- | `max-width` + `margin-inline: auto` | **`centered`** |
48
- | `padding-block` on a section | **`region`** |
49
- | margins between headings/paragraphs/lists | **`rhythm`** |
50
- | `display: grid` + `grid-template-columns: repeat(n, …)` | **`subgrid`** |
51
- | `repeat(auto-fit, minmax(…))` | **`grid-auto`** |
52
- | `display: flex` + `gap` for a row of controls | **`cluster`** |
53
-
54
- `centered` is a grid of _named tracks_, not a max-width box — a child widens
55
- itself with `breakout`/`spotlight`/`fullbleed` without the parent knowing, which
56
- is the thing a bespoke `.wrap` can never do. **More than one card means
57
- `subgrid`**: a plain grid aligns the outer boxes but not the tranches inside
58
- them, so headings and CTAs land at different heights across the set. Write
59
- `<ul class="subgrid subgrid-cols-3" role="list">` with
60
- `<li class="card subgrid-card">` items, or `<Subgrid />` (which renders the slot
61
- verbatim — author the `<li>`s yourself; it adds `role="list"` for you, and a
62
- hand-written one needs it because a marker-less `<ul>` stops being announced as a
63
- list in Safari).
64
-
65
- **When the whole card is a link the card is still the item**, and pick by whether
66
- the card's text must stay selectable: `stretched-link` on a link inside it is
67
- zero-JS but kills text selection, while `<Cards>` (which wraps the list in
68
- `<wc-cards>`) keeps selection and falls back to that same link with no JS. Never
69
- both — the overlay wins and the JS never runs. Never `<li><a class="card">`: the
70
- `li` is the grid item, so the anchor's tranches never align and it does not fill
71
- the cell; it renders fine, which is why it keeps getting written.
72
-
73
- `vpr lint:design` reports all of these, and the pre-commit hook fails on them
74
- — the fix is the class, not a justification for the CSS.
75
-
76
- - **Rendering and freshness: everything is prerendered.** `output: 'server'` is
77
- still required (the admin, media/API routes and scheduled publishing need a
78
- Worker), but every public page — `index.astro`, `[...slug].astro`, `404.astro` —
79
- has `export const prerender = true`, and `[...slug].astro` lists the published
80
- `pages` entries in `getStaticPaths()`. The HTML is written at build time and
81
- served as a static asset; the Worker only handles `/_emdash/*` and anything
82
- you deliberately leave on-demand. A route with no `prerender` export is
83
- re-rendered on every request, so don't leave it off by accident.
84
- - **Where build-time content comes from.** The Cloudflare adapter prerenders
85
- against a _local_ D1 (`.wrangler/state`), never the deployed one — on a CI
86
- runner that is an empty database, which would bake "No home page yet" into
87
- every page. `scripts/pull-content.mjs` (the first step of `vpr deploy` and
88
- `vpr deploy:dev`) exports production's D1 and loads it locally first. It
89
- fails closed: a failed export aborts the deploy rather than publishing an
90
- empty site (the one exception is a database that doesn't exist yet, the first
91
- deploy, which builds from the seed). `SKIP_PULL_CONTENT=1` skips it.
92
- - **The local database must be migrated before the build.** The template runs
93
- `migrations: { runtime: 'check' }`, so a build against a local database with
94
- pending migrations prerenders EmDash's one-line "Database migrations are
95
- required" response into every page — and still succeeds. That is what a fresh
96
- runner, a first deploy, a `SKIP_PULL_CONTENT=1` build and an **upgrade** deploy
97
- (the pull restores production's older schema, and `emdash migrate` only runs
98
- after the build) would all publish. `vitops emdash prepare-db` (`vpr prepare-local-db`, run after the
99
- pull by `vpr deploy`, `vpr deploy:dev` and the reusable workflow) starts `astro
100
- dev` once, calls EmDash's `dev-bypass` setup (which migrates and applies the
101
- seed's sample content) and stops it; `vitops emdash check-prerender` (part of
102
- `vpr build`) then fails the build if any prerendered page is not a real HTML page
103
- with a title. Run `vpr prepare-local-db` by hand before a first local `vpr build`.
104
- It only writes the disposable `.wrangler/state`; the dev administrator it creates
105
- is never deployed.
106
- - **Freshness is a rebuild.** Production is the truth for content, and a page
107
- reflects it as of the last build. Publishing in the admin therefore has to
108
- trigger one: the deploy workflows listen for a `repository_dispatch` event of
109
- type `emdash-publish` (see "Publish-triggered rebuilds" below).
110
- - **Drafts and previews.** A prerendered page cannot show an unpublished
111
- draft, so an admin "Preview" link shows the last published build. If previews
112
- matter, add a dedicated on-demand route for them rather than removing
113
- `prerender` from the public ones.
114
- - **Dynamic parts are server islands.** Anything that must be fresh per request
115
- goes in its own component rendered with `server:defer` inside the prerendered
116
- page; the surrounding HTML stays static. Keep islands rare — each is a Worker
117
- request after load.
118
- - **Unknown URLs.** There is no `Astro.rewrite('/404')`: a slug not listed by
119
- `getStaticPaths()` has no route, so Astro serves the prerendered `404.html`
120
- with a real 404 status. Rewriting an on-demand route to a prerendered one
121
- fails (500), so don't reintroduce it.
122
- - **The contact form is the one on-demand route.** The `vitops.form` block posts a plain
123
- `<form method="post">` to `src/pages/api/track.ts` (`prerender = false`, built with
124
- `createConversionRoute()`), which also answers the conversion beacons. The block works with
125
- no JavaScript: the route answers a form post with a 303 back to the page plus `?sent=1#form-sent`
126
- or `?error=1#form-error`, and the block's two notices are revealed by that fragment. Keep one
127
- form per page. Where submissions go: `site.notifications.email` (or the primary location's
128
- `email`) and `site.crm` when set. **With neither, the route refuses the post** rather than
129
- accept a message nobody receives, so set `site.notifications.email` before launch and run
130
- `vitops email setup` for its sender domain. A failed CRM write never fails the request; it is
131
- stated in the notification email, which is the durability floor.
132
- - **Spam protection**: a honeypot field, the route's origin / size / per-IP rate checks, and
133
- Cloudflare Turnstile once `vitops setup turnstile` has put the public key in
134
- `site.security.turnstile.siteKey` and saved the Worker secret `VITOPS_TURNSTILE_SECRET`
135
- (never in `site.json`). With a key set the route fails closed if the secret is missing.
136
- `TURNSTILE_REQUIRED` in the route (default `true`) makes a token-less post a refusal, which
137
- means the form needs JavaScript; set it `false` to keep a no-JS submit working and accept
138
- that a bot can then skip the token. Turnstile is category `necessary` (it protects the form
139
- being sent), loads only when a visitor first uses the form, and its origin is allowed in the
140
- Report-Only CSP in `public/_headers`.
141
- - **Privacy policy inventory.** `vitops legal` derives "what personal information we collect"
142
- from `site.templates` entries of type `form`, not from the editor's block, so a form added in
143
- the admin is invisible to it. When you add or change form fields, mirror them in a
144
- `site.templates` form entry in `site.json`, or the generated policy under-reports.
145
- - A field named `phone` submits as `tel` (the route reserves `phone` and `event`).
146
- - The per-IP rate limit is per Worker isolate; add a KV-backed `RateLimitStore` if you need a
147
- fleet-wide one.
148
- - The Worker cron trigger in `wrangler.jsonc` drives scheduled publishing —
149
- don't remove it.
150
- - Hosting (adapter + EmDash database/storage) comes from `vitopsHosting()`
151
- (`@getvitops/emdash`) in astro.config.mjs — never hard-code an adapter or
152
- d1/r2/sqlite there. Switch targets via `vitopsHosting({ target })` or the
153
- `HOSTING` env var (see README "Hosting"); the `node` target needs
154
- `@astrojs/node` + `better-sqlite3` installed first.
155
- - **`SETUP.html` is the client-onboarding checklist.** When working it via the
156
- `.agents/skills/setup/` skill, tick a box only for a step you actually
157
- completed — never on the operator's behalf for a step marked human-only
158
- (the Cloudflare API token, the domain cutover, publishing legal documents).
6
+ - `site.json` is the single source of truth: organization + site facts, and the design system
7
+ under `designSystem.themes.default`. Never edit the generated stylesheet
8
+ (`src/styles/tailwind.css` with the default `tailwind` format; `css.format` in
9
+ `astro.config.mjs` picks it). It is rebuilt from `site.json` on every dev/build and gitignored.
10
+ Only `src/styles/tailwind.css` and `tokens.json` are ignored: put your own CSS in a separate file
11
+ (for example `src/site.css`, imported from the layout) and commit it.
12
+ - Content lives in the EmDash database, not the repo. Read it with `getEmDashCollection` /
13
+ `getEmDashEntry` from `emdash`; render Portable Text with `<PortableText />` from `emdash/ui`
14
+ (the `vitops_*` blocks from `@getvitops/emdash` are merged in).
15
+ - **Where truth lives:** content (entries, media, menus) is **production's**, edited there; code,
16
+ `site.json` and schema are **git's** (`dev` → `main`). The dev database is a disposable copy
17
+ refreshed from prod, never promoted back. See `.agents/skills/content-and-deploys/SKILL.md`.
18
+ - `seed/seed.json` applies only to an empty database. To change a running site's content model, use
19
+ the admin (`/_emdash/admin`) or its MCP server (`/_emdash/api/mcp`).
20
+ - Hosting comes from `vitopsHosting()` in `astro.config.mjs` — never hard-code an adapter or
21
+ d1/r2/sqlite. Switch with `vitopsHosting({ target })` or the `HOSTING` env var.
22
+ - Keep the Worker cron trigger in `wrangler.jsonc`: it publishes content scheduled in the admin.
23
+ - `src/assets/placeholder/` is wireframe stand-in imagery; delete it once real assets land.
24
+ - Prefer framework classes (utilities, patterns, type roles like `font-heading`) over bespoke CSS.
25
+ `vp exec vitops agents` appends the full class reference to this file.
26
+ - `SETUP.html` is the onboarding checklist (`.agents/skills/setup/`). Tick a box only for a step you
27
+ completed, never for a human-only one (the Cloudflare API token, domain cutover, legal documents).
28
+
29
+ ## Building pages
30
+
31
+ - The editor's insert menu offers the `vitops_*` blocks. `vp exec vitops docs blocks` lists them
32
+ with every field; `vp exec vitops docs components` lists every pattern, which tiers provide it
33
+ and which block renders it.
34
+ - **Nesting.** A block type with a `portableText` field, such as `vitops_section`, holds other
35
+ blocks. A `vitops_*` block holds only scalar fields and repeaters (cards, gallery images,
36
+ accordion items, slides), so it can't. EmDash's built-in `columns` doesn't render `vitops_*`
37
+ children. For layout, nest in a section, use a `blocks` field, or hand-write the template with
38
+ `@getvitops/astro` components (`Subgrid`, `Cards`, `Carousel`, …).
39
+ - **Section, block or widget area?** Copy-and-edit content is an EmDash Section. The same content
40
+ everywhere (announcement bar, pre-footer call to action, sidebar, footer columns) is a **widget
41
+ area**: define it under Widgets in the admin and render it with `<VitopsWidgetArea name="…" />`
42
+ from `@getvitops/emdash/astro` in the layout, or in page content with the `vitops_widgetArea`
43
+ block. Page-specific content is blocks. An area can't hold another area, and prerendered pages
44
+ show an edit after the next deploy (like menus).
45
+ - **Escape hatches on every block:** `theme` (a backdrop from the site's colour roles, `surface` or
46
+ `brand-primary:solid`, with the readable text for it picked by the design system, so contrast is
47
+ not the editor's judgement), `anchor` (the `id`, for `#links`), `className` (extra classes; under
48
+ the tailwind format only ones the site already uses) and `style` (CSS custom properties only,
49
+ e.g. `--p-card: 2rem`; anything else is dropped). The same backdrop is available on a widget area
50
+ (`<VitopsWidgetArea theme="…">`) and on your own markup (`themeAttrs('surface')` from
51
+ `@getvitops/emdash/astro`, spread on the layout's `<body>`). How a role looks (flat, raised,
52
+ glass) is the site's own role styling in `site.json`. Set `--header-offset` to the sticky
53
+ header's height so anchors clear it.
54
+ - Override a block per site with `<PortableText components={{ type: { 'vitops_cards': MyCards } }} />`;
55
+ hide one with `vitopsEmdash({ blocks: { exclude: [...] } })`.
56
+ - **One `<h1>` per page.** The template's page routes render the title as an `<h1>`, and a hero's
57
+ heading level defaults to `h1`, so set a hero's level to `2` on those pages (or let the hero own
58
+ the `h1`). Use `p` for a display heading that is not part of the outline. `vitops emdash build`
59
+ warns on any page without exactly one `<h1>`.
60
+ - Labels: `badge` is static (status, count, category); `tag` may be editable or dismissable. Don't
61
+ invent `chip` or `pill`. The button class is `btn`.
62
+ - **Before adding a block type** or a `src/components/blocks/*` renderer, read
63
+ `.agents/skills/custom-blocks/SKILL.md`. Types can't be renamed or deleted once pages use them.
64
+
65
+ ## Layout foundations — the answer, not a starting point
66
+
67
+ | About to write… | Write instead |
68
+ | ------------------------------------------------------- | --------------- |
69
+ | a `wrap`/`wrapper`/`container`/`inner` class | **`centered`** |
70
+ | `max-width` + `margin-inline: auto` | **`centered`** |
71
+ | `padding-block` on a section | **`region`** |
72
+ | margins between headings/paragraphs/lists | **`rhythm`** |
73
+ | `display: grid` + `grid-template-columns: repeat(n, …)` | **`subgrid`** |
74
+ | `repeat(auto-fit, minmax(…))` | **`grid-auto`** |
75
+ | `display: flex` + `gap` for a row of controls | **`cluster`** |
76
+
77
+ `centered` is a grid of _named tracks_: a child widens itself with `breakout`/`spotlight`/
78
+ `fullbleed` without the parent knowing, which a bespoke `.wrap` can't do.
79
+
80
+ - **More than one card means `subgrid`**, so headings and CTAs align across the set:
81
+ `<ul class="subgrid subgrid-cols-3" role="list">` with `<li class="card subgrid-card">`, or
82
+ `<Subgrid />` (renders the slot verbatim; author the `<li>`s).
83
+ - **A card that is a link:** `stretched-link` inside it is zero-JS but kills text selection;
84
+ `<Cards>` keeps selection and falls back to the same link with no JS. Never both. Never
85
+ `<li><a class="card">`: the `li` is the grid item, so the tranches don't align.
86
+ - `vpr lint:design` reports all of these and the pre-commit hook fails on them. Fix the class, not
87
+ the CSS.
88
+
89
+ ## Rendering and the contact form
90
+
91
+ - Every public page is **prerendered**; the Worker handles `/_emdash/*` and `src/pages/api/track.ts`.
92
+ An admin publish goes live after a rebuild. Run `vpr prepare-local-db` before a first local
93
+ `vpr build`. Details, previews and server islands: `.agents/skills/content-and-deploys/SKILL.md`.
94
+ - The contact form posts to `/api/track`. Set `site.notifications.email` before launch or the route
95
+ refuses posts, and mirror form fields in `site.templates`. Details:
96
+ `.agents/skills/contact-form/SKILL.md`.
97
+ - `public/_headers` ships a Report-Only CSP. Read `.agents/skills/csp/SKILL.md` before enforcing it
98
+ or adding embeds.
99
+ - `vitops audit` needs optional packages and a browser, checked before it starts a server:
100
+ `vp add -D playwright @axe-core/playwright lighthouse && vp exec playwright install chromium`.
101
+ Its page list is the built sitemap, then the seed's menus.
159
102
 
160
103
  ## Branch model
161
104
 
162
- - Work on **`dev`** (the integration branch). Every push auto-deploys the
163
- isolated `-dev` worker (own D1/R2; `dev.<domain>` with a custom domain).
164
- - **`main`** is production and branch-protected — never push or merge to it
165
- directly. Production ships only via the manual **promote** flow: the
166
- `promote` skill (`.agents/skills/promote/SKILL.md`) dispatches
167
- `.github/workflows/promote.yml`, which opens a `dev → main` PR with
168
- auto-merge; merging deploys prod. Only run it when the user explicitly asks
169
- to promote/ship to production.
170
- - Sample legal pages (`/terms`, `/privacy`) ship as seeded placeholder content
171
- — remind the user to replace them before launch. They are deliberately short
172
- and obviously unfinished; do not "improve" them into something that reads as
173
- a finished policy, because the risk here is a plausible-looking document
174
- getting shipped unread.
175
- To replace them properly, fill in `site.json`'s `organization`/`site.legal`
176
- fields and run `vp exec vitops legal --format portable-text --out ./legal`, then
177
- paste each document's blocks into the page in the admin. That renders a real
178
- policy from what the site actually does — the analytics provider it uses,
179
- the fields its forms collect, where it deploys — rather than from guesses.
180
- It still needs review before launch.
181
-
182
- ## EmDash 1.x behaviours worth knowing
183
-
184
- - **Passkeys are bound to the hostname they were created on.** After the
185
- workers.dev → custom-domain cutover, use **Settings → General → Change
186
- domain**, then **Continue on** to add a passkey at the new address.
187
- - **HTML blocks created in the admin run JavaScript** in a sandboxed iframe
188
- (Contributor and up can add it). Astro's `security.csp` blocks their styles
189
- and scripts, so don't turn CSP on without allowing for that.
190
- - **Link entries by `entry.data.slug`, never `entry.id`** — on multilingual
191
- sites `id` carries a locale prefix.
192
- - **Submit `/sitemap.xml`** to search engines, not a per-collection sitemap;
193
- collection sitemaps now split into pages of 2,000 entries.
194
- - **A stuck D1 migration lock** is cleared with
195
- `vp exec emdash migrate --take-over-lock <id>` (confirms the target, or
196
- needs `--expected-target-fingerprint` non-interactively).
197
-
198
- ## Docs MCP servers
199
-
200
- `.mcp.json` preconfigures the Astro docs and EmDash docs MCP servers — prefer
201
- searching them over pre-trained knowledge for Astro/EmDash questions.
202
-
203
- ## Labels: badge and tag
204
-
205
- Two label patterns, both enabled in `site.json`'s `patterns.items`: **`badge`** is a
206
- static label (a status, count or category) and **`tag`** is a label that may be
207
- static _or_ dynamic (editable, dismissable, in a `tag-list`). Don't invent a third
208
- name — `chip` and `pill` are not part of this vocabulary. The button pattern's class
209
- is `btn` (`<a class="btn">` is styled, not just a bare `<button>`).
210
-
211
- ## Custom blocks
212
-
213
- Before adding a block type, a `src/components/blocks/*` renderer, or anything an
214
- editor places on a page, read `.agents/skills/custom-blocks/SKILL.md`: compose the
215
- existing `vitops.*` blocks first, extend one second, go site-local third, upstream
216
- last. Block types can't be renamed or deleted once pages use them, so check the
217
- framework's pattern names before choosing one.
218
-
219
- ## Publish-triggered rebuilds
220
-
221
- Pages are static, so a publish in the admin goes live only after a rebuild.
222
- `.github/workflows/deploy-prod.yml` also runs on a `repository_dispatch` event of
223
- type `emdash-publish` (it pins `ref` to `main`, because a dispatch runs from the
224
- default branch, `dev`). Wire an EmDash plugin hook to send it:
225
-
226
- ```jsonc
227
- // emdash-plugin.jsonc
228
- "capabilities": ["content:read", "network:request"],
229
- "allowedHosts": ["api.github.com"]
230
- ```
231
-
232
- ```ts
233
- // src/plugin.ts
234
- hooks: {
235
- 'content:afterPublish': async (_event, ctx) => {
236
- const token = await ctx.settings.get<string>('githubToken'); // an encrypted plugin setting
237
- if (!token || !ctx.http) return;
238
- await ctx.http.fetch(`https://api.github.com/repos/${OWNER}/${REPO}/dispatches`, {
239
- method: 'POST',
240
- headers: { authorization: `Bearer ${token}`, accept: 'application/vnd.github+json' },
241
- body: JSON.stringify({ event_type: 'emdash-publish' }),
242
- });
243
- },
244
- }
245
- ```
246
-
247
- The token is a fine-grained GitHub token with `Contents: write` on this repo only,
248
- stored as the plugin's encrypted setting (a sandboxed plugin can't read Worker
249
- environment variables), never in the repo. The hook name, capabilities and
250
- `ctx.http` / `ctx.settings` calls follow EmDash's plugin reference but this exact
251
- plugin has not been run against a live site, so treat it as a starting point. Two
252
- costs to know: the run goes through the `production` environment, so it **waits for
253
- the reviewers' approval** like any prod deploy (relax that rule if editors' publishes
254
- should go live unattended), and bursts of publishes queue behind the
255
- `deploy-prod` concurrency group rather than cancelling each other. Without the hook,
256
- a manual run of **Deploy prod** (or a push to `main`) does the same job.
257
-
258
- ## Content Security Policy
259
-
260
- `public/_headers` ships a **`Content-Security-Policy-Report-Only`** header: it logs
261
- violations to the browser console and blocks nothing, so it can't break a page. Its
262
- `script-src` is `'self'` plus the SHA-256 of each inline script `<Head />` emits (the
263
- colour-scheme bootstrap and the consent-gate stub) — no `'unsafe-inline'`. Hashes,
264
- not nonces, because prerendered pages have no per-request response to put a nonce in.
265
- `contentSecurityPolicy()` and `inlineScriptHashes()` from `@getvitops/astro` produce
266
- the same strings (`templates.test.ts` in the toolchain keeps the file in step).
267
-
268
- Know what it does not cover before you flip it to enforcing (rename the header to
269
- `Content-Security-Policy`):
270
-
271
- - **`_headers` applies to static assets only.** Worker-rendered responses — the EmDash
272
- admin (`/_emdash/*`), its API, anything left on-demand — don't get it. Setting a
273
- header there means doing it in code (e.g. Astro middleware calling
274
- `contentSecurityPolicy()`), and the admin's own inline scripts and editor HTML
275
- would need allowing; leave the admin out of an enforced policy.
276
- - **Editor-authored HTML blocks are the hard limit.** Any block that lets an editor
277
- paste raw HTML or a `<script>` (an embed, a tracking snippet) puts arbitrary inline
278
- script on the page, which a hash policy cannot name ahead of time. Enforcing a strict
279
- policy means removing such blocks or moving embeds to a consent-gated
280
- `data-consent` tag whose origin you allow explicitly.
281
- - **Third-party tags** the consent gate activates need their origins in `script-src`
282
- / `connect-src`; anything with `frame` needs `frame-src`.
283
- - **A server island** (`server:defer`) adds an inline loader script and a request to
284
- the Worker; expect a report for the loader until you hash it.
285
-
286
- Run the site, browse the pages you ship with the console open, fix or allow what is
287
- reported, and only then enforce.
288
-
289
- ## EmDash agent skills
290
-
291
- The EmDash skills (`building-emdash-site`, `creating-plugins`, `emdash-cli`,
292
- `upgrading-emdash` and the two WordPress porting skills) are installed per
293
- project, not globally, so they match the `emdash` version this repo pins. If
294
- `.agents/skills/building-emdash-site/` is missing, install them (the setup guide
295
- and `SETUP.html` have the full step):
296
-
297
- ```bash
298
- vp dlx skills add emdash-cms/skills --skill '*' -y
299
- ```
300
-
301
- Refresh them with `vp dlx skills update` after bumping `emdash` — the
302
- `upgrading-emdash` skill covers the rest of the upgrade.
105
+ - Work on **`dev`**; every push auto-deploys the isolated `-dev` worker.
106
+ - **`main`** is production and protected: never push or merge to it. It ships only through the
107
+ `promote` skill (`.agents/skills/promote/`), and only when the user explicitly asks.
108
+ - Sample legal pages (`/terms`, `/privacy`) are deliberately short, unfinished placeholders; don't
109
+ "improve" them into something that reads as a finished policy. To replace them, fill in
110
+ `site.json`'s `organization`/`site.legal`, run
111
+ `vp exec vitops legal --format portable-text --out ./legal`, and paste each document into the page
112
+ in the admin. It still needs review before launch.
113
+
114
+ ## EmDash 1.x behaviours
115
+
116
+ - Passkeys are bound to the hostname they were created on. After a domain cutover use **Settings →
117
+ General → Change domain**, then **Continue on**.
118
+ - Link entries by `entry.data.slug`, never `entry.id` (multilingual sites prefix `id` with a locale).
119
+ - Submit `/sitemap.xml` to search engines, not a per-collection sitemap.
120
+ - Clear a stuck D1 migration lock with `vp exec emdash migrate --take-over-lock <id>` (needs
121
+ `--expected-target-fingerprint` non-interactively).
122
+
123
+ ## Improving the toolkit
124
+
125
+ You are also a source of feedback for `@getvitops/*`. When you struggle with a framework built-in,
126
+ write a workaround, add bespoke CSS or a site-local block, or find a doc wrong or missing, and the
127
+ need is **not** unique to this site, record it in `UPSTREAM.md` at the repo root (create it if
128
+ absent). One entry each, with: what you tried, what failed or was missing, the workaround (file
129
+ path), the change you'd propose, and the package (`@getvitops/emdash`, `astro`, `core`, `cli`,
130
+ `create`, or docs). Mention new entries in your end-of-task summary. Don't log choices that are
131
+ specific to this site, and never patch `node_modules`.
132
+
133
+ ## Docs and skills
134
+
135
+ - `.mcp.json` preconfigures the Astro and EmDash docs MCP servers; search them before relying on
136
+ pre-trained knowledge.
137
+ - **Skills.** `@getvitops/skills` ships `custom-blocks`, `content-and-deploys`, `contact-form` and
138
+ `csp`. `skills experimental_sync` (run by `prepare`, so by `vp install`) links them into
139
+ `.agents/skills/` and `.claude/skills/`; they are gitignored and update with `vp update`. A skill
140
+ you write yourself with the same name is never replaced. `promote` and `setup` are this site's own.
141
+ - The EmDash skills (`building-emdash-site`, `creating-plugins`, `emdash-cli`, `upgrading-emdash`,
142
+ two WordPress porting skills) are installed per project to match the pinned `emdash`. If
143
+ `.agents/skills/building-emdash-site/` is missing: `vp dlx skills add emdash-cms/skills --skill '*' -y`.
144
+ Refresh with `vp dlx skills update` after bumping `emdash`.
303
145
 
304
146
  ## Commands
305
147
 
306
- This is a **Vite+** project: use `vp` (and its `vpr` shorthand), not `pnpm`/`npm`/`npx`.
307
- The package manager underneath is pnpm, but `vp` picks it up for you.
148
+ This is a **Vite+** project: use `vp` (and `vpr`), not `pnpm`/`npm`/`npx`. Scripts that wrap Astro
149
+ go through `vpr` (`vpr dev`, `vpr build`, `vpr preview`), because `vp dev`/`vp build` skip Astro.
150
+ `vpr dev` returns immediately (the server runs in the background): check it with
151
+ `vp exec astro dev status`, read `vp exec astro dev logs --follow`, stop with
152
+ `vp exec astro dev stop`; the site is at <http://localhost:4321>.
308
153
 
309
154
  | To… | Run |
310
155
  | ---------------------------------------- | ------------------------------------------------------- |
311
156
  | install dependencies | `vp install` |
312
157
  | add / remove / update a package | `vp add <pkg>` · `vp remove <pkg>` · `vp update` |
313
- | run a `package.json` script | `vpr <script>` (alias for `vp run <script>`) |
158
+ | run a `package.json` script | `vpr <script>` |
314
159
  | run a locally installed binary | `vp exec <bin>` (e.g. `vp exec vitops docs components`) |
315
160
  | run a one-off package without installing | `vp dlx <pkg>` |
316
161
  | format + lint + typecheck (files) | `vp check` |
317
162
  | typecheck Astro (`.astro` files) | `vpr typecheck` |
318
-
319
- **Scripts that wrap Astro must go through `vpr`.** `vp dev` / `vp build` run Vite
320
- directly and skip Astro, so use `vpr dev`, `vpr build`, `vpr preview`.
321
-
322
- **`vpr dev` returns immediately.** `astro dev` now starts the server in the
323
- background. Check it with `vp exec astro dev status`, read its output with
324
- `vp exec astro dev logs --follow`, and stop it with `vp exec astro dev stop`; the
325
- site is at <http://localhost:4321>. Don't wait on it as if it were a foreground
326
- process.
163
+ | design-lint the markup | `vpr lint:design` |
327
164
 
328
165
  | Script | What it does |
329
166
  | ----------------------------------- | ------------------------------------------------------------------------------------------- |
330
167
  | `vpr dev` | local dev with local D1/R2 emulation (no Cloudflare account) |
331
168
  | `vpr deploy` | pull production content, build, `wrangler deploy` prod (provisions D1/R2 on first run) |
332
169
  | `vpr deploy:dev` | same, for the isolated dev worker (needs `DEV_D1_ID`) |
333
- | `vpr pull-content` | just the content pull (see "Rendering and freshness") |
170
+ | `vpr pull-content` | just the content pull |
334
171
  | `vpr prepare-local-db` | migrate and seed the local D1 the build prerenders from |
335
172
  | `vpr init:github` | one-time: create + push the GitHub repo (main + dev, dev default). Only when the user asks. |
336
173
  | `vpr init:hosting` | one-time: provision hosting (dev D1/R2 + repo secrets). Only when the user asks. |
337
174
  | `vp exec vitops validate site.json` | schema-check the site config |
338
175
 
339
- **Release flow.** A site is not a published package, so there is no changeset or
340
- version bump: ship by committing to `dev` (auto-deploys the dev worker) and then
341
- running the **promote** workflow. `vp` has no `vp change` / `vp pm change` command
342
- (checked against vp 1.5.0); for a _package_ it does have `vp pm version`,
343
- `vp pm publish` and `vp pm stage`.
176
+ Shipping is not a package release: no changeset or version bump. Commit to `dev`, then promote.
177
+ (`vp` has no `vp change` command.)
344
178
 
345
- **Naming the project.** `vp create` takes the package name from the target
346
- directory and invents a random slug when none is given, so pass one:
347
- `vp create @getvitops:emdash --directory <client-slug>`. `wrangler.jsonc`'s
348
- `name`, D1 `database_name` and R2 `bucket_name` are separate and still say
349
- `my-emdash-site` — rename them to match before the first deploy.
179
+ `vp create` names the package after the target directory (pass `--directory <client-slug>`).
180
+ `wrangler.jsonc`'s `name`, D1 `database_name` and R2 `bucket_name` are separate and still say
181
+ `my-emdash-site`; rename them to match before the first deploy.
@@ -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.md "Rendering and freshness" for how production content
54
+ per request — see `.agents/skills/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
 
@@ -189,7 +189,7 @@ vpr build:css # regenerate the CSS manually
189
189
  | `src/pages/index.astro` | Home — prerendered from the `home` page entry |
190
190
  | `src/pages/[...slug].astro` | Prerendered once per published `pages` entry (`getStaticPaths`) |
191
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.md "Content Security Policy") |
192
+ | `public/_headers` | Report-Only CSP for the static pages (see `.agents/skills/csp`) |
193
193
  | `scripts/pull-content.mjs` | Loads production's D1 into the local build DB before the prerender (`deploy`/`deploy:dev`) |
194
194
  | `wrangler.jsonc` | Cloudflare bindings (D1/R2) + cron trigger |
195
195
  | `scripts/build-dev-wrangler.mjs` | Rewrites the built wrangler config for the isolated dev worker |
@@ -18,8 +18,9 @@ worker-configuration.d.ts
18
18
  # node hosting target: SQLite db + local uploads
19
19
  data/
20
20
 
21
- # generated by the vitops() integration on dev/build
22
- src/styles/
21
+ # generated by the vitops() integration on dev/build (site CSS goes in src/site.css, not here)
22
+ src/styles/tailwind.css
23
+ src/styles/tokens.json
23
24
  public/vitops/
24
25
 
25
26
  # generated by `pnpm run sop` — styles SETUP.html for a file:// open
@@ -32,3 +33,13 @@ public/vitops/
32
33
 
33
34
  # os / editor
34
35
  .DS_Store
36
+
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
@@ -35,6 +35,8 @@ const brandColor = colors.palette?.[roleHue('brand-primary')]?.seed ?? '#2e9b73'
35
35
  export default defineConfig({
36
36
  output: 'server',
37
37
  adapter,
38
+ // The seed's `pages` collection serves entries at `/{slug}` (urlPattern), and `home` lives at `/`.
39
+ redirects: { '/home': '/' },
38
40
  image: {
39
41
  layout: 'constrained',
40
42
  responsiveStyles: true,
@@ -0,0 +1,3 @@
1
+ Signature: 8a477f597d28d172789f06886806bc55
2
+ # This file is a cache directory tag created by Vitest.
3
+ # For information about cache directory tags see https://bford.info/cachedir/
@@ -0,0 +1,3 @@
1
+ {
2
+ "lockfileHash": "4b2ac6bd"
3
+ }