@getvitops/create 0.8.2 → 0.9.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
- This is an EmDash CMS website (Astro SSR on Cloudflare Workers, D1 + R2)
2
- styled by the Vitops design system.
1
+ This is an EmDash CMS website (Astro on Cloudflare Workers, D1 + R2 — public pages
2
+ prerendered, the Worker serving the admin and API) styled by the Vitops design system.
3
3
 
4
4
  ## Ground rules
5
5
 
@@ -21,14 +21,30 @@ styled by the Vitops design system.
21
21
  `@getvitops/emdash` are merged in automatically).
22
22
  - **Building pages from patterns:** the editor's insert menu offers the `vitops.*` blocks —
23
23
  link/button, image compare, copy snippet, banner, disclosure, accordion, hero, carousel, gallery,
24
- cards, marquee (`npx vitops docs blocks` lists them with every field). Repeating content (cards, gallery images, accordion items, carousel slides) is
24
+ cards, marquee (`vp exec vitops docs blocks` lists them with every field). Repeating content (cards, gallery images, accordion items, carousel slides) is
25
25
  a repeater field. A block cannot contain other blocks, and a `vitops.*` block inside EmDash's
26
26
  built-in `columns` does not render — for layout (a section, a split, a hero) use a `blocks`
27
27
  field, or hand-write the page template with `@getvitops/astro` components
28
- (`Subgrid`, `Cards`, `Carousel`, …). `npx vitops docs components` lists every pattern, which
28
+ (`Subgrid`, `Cards`, `Carousel`, …). `vp exec vitops docs components` lists every pattern, which
29
29
  tiers provide it, and which block renders it. Override a block's rendering per site with
30
30
  `<PortableText components={{ type: { 'vitops.cards': MyCards } }} />`; hide one with
31
31
  `vitopsEmdash({ blocks: { exclude: [...] } })` in `astro.config.mjs`.
32
+ - **Escape hatches on every block.** Each `vitops.*` block (and the section type) has an
33
+ `anchor` (becomes the `id`, for `#links`), `className` (extra classes — with the tailwind format
34
+ only ones the site already uses) and `style` (CSS custom properties only, e.g. `--p-card: 2rem`;
35
+ anything else is dropped). `vitops_section` also has a `theme`: one pick for a tinted or solid
36
+ backdrop with readable text. Set `--header-offset` to the sticky header's height so anchors
37
+ clear it.
38
+ - **`vitops audit`** needs optional packages and a browser, checked before it starts a server:
39
+ `vp add -D playwright @axe-core/playwright lighthouse && vp exec playwright install chromium`.
40
+ Its page list is the built sitemap, then the seed's menus.
41
+ - **Section, block or widget area?** Copy-and-edit content is an EmDash Section. The same content
42
+ everywhere (an announcement bar, a pre-footer call to action, a sidebar, footer columns) is a
43
+ **widget area**: define it under Widgets in the admin, render it with
44
+ `<VitopsWidgetArea name="…" />` from `@getvitops/emdash/astro` in the layout, or place it in
45
+ page content with the `vitops.widgetArea` block. The layout renders `announcement` and
46
+ `pre-footer`. Page-specific content is blocks. An area cannot hold another area, and prerendered
47
+ pages show an edit after the next deploy (like menus).
32
48
  - `seed/seed.json` only applies against an empty database: structure
33
49
  (collections, menus, settings) on first boot; the `content` entries only
34
50
  when the setup wizard's "include sample content" option is chosen. Changing
@@ -36,7 +52,7 @@ styled by the Vitops design system.
36
52
  (`/_emdash/admin`) or the site's MCP server (`/_emdash/api/mcp`) to change
37
53
  the content model of a running site.
38
54
  - Prefer framework CSS classes (utilities, patterns, type roles like
39
- `font-heading`) over bespoke CSS. Run `npx vitops agents` to append the
55
+ `font-heading`) over bespoke CSS. Run `vp exec vitops agents` to append the
40
56
  full generated class-vocabulary reference to this file.
41
57
  - **Layout foundations — these are the answer, not a starting point.** Six
42
58
  substitutions carry the structure of nearly every page:
@@ -70,18 +86,81 @@ styled by the Vitops design system.
70
86
  `li` is the grid item, so the anchor's tranches never align and it does not fill
71
87
  the cell; it renders fine, which is why it keeps getting written.
72
88
 
73
- `npm run lint:design` reports all of these, and the pre-commit hook fails on them
89
+ `vpr lint:design` reports all of these, and the pre-commit hook fails on them
74
90
  — the fix is the class, not a justification for the CSS.
75
91
 
76
- - **Prerender by default.** `output: 'server'` is required (the admin, media/API
77
- routes, preview and scheduled publishing need a server), so prerendering is
78
- opt-in per route: put `export const prerender = true` on every page that
79
- doesn't need per-request data, and give dynamic routes a `getStaticPaths()`
80
- that queries the collection at build time. A route with no `prerender` export
81
- is re-rendered for every visitor on every request. The cost is that a
82
- prerendered page reflects the database as of the last build, so an admin
83
- publish needs a redeploy — leave a route server-rendered only where that is
84
- unacceptable (preview, personalised content, must-be-live-in-seconds).
92
+ - **Rendering and freshness: everything is prerendered.** `output: 'server'` is
93
+ still required (the admin, media/API routes and scheduled publishing need a
94
+ Worker), but every public page — `index.astro`, `[...slug].astro`, `404.astro` —
95
+ has `export const prerender = true`, and `[...slug].astro` lists the published
96
+ `pages` entries in `getStaticPaths()`. The HTML is written at build time and
97
+ served as a static asset; the Worker only handles `/_emdash/*` and anything
98
+ you deliberately leave on-demand. A route with no `prerender` export is
99
+ re-rendered on every request, so don't leave it off by accident.
100
+ - **Where build-time content comes from.** The Cloudflare adapter prerenders
101
+ against a _local_ D1 (`.wrangler/state`), never the deployed one — on a CI
102
+ runner that is an empty database, which would bake "No home page yet" into
103
+ every page. `scripts/pull-content.mjs` (the first step of `vpr deploy` and
104
+ `vpr deploy:dev`) exports production's D1 and loads it locally first. It
105
+ fails closed: a failed export aborts the deploy rather than publishing an
106
+ empty site (the one exception is a database that doesn't exist yet, the first
107
+ deploy, which builds from the seed). `SKIP_PULL_CONTENT=1` skips it.
108
+ - **The local database must be migrated before the build.** The template runs
109
+ `migrations: { runtime: 'check' }`, so a build against a local database with
110
+ pending migrations prerenders EmDash's one-line "Database migrations are
111
+ required" response into every page — and still succeeds. That is what a fresh
112
+ runner, a first deploy, a `SKIP_PULL_CONTENT=1` build and an **upgrade** deploy
113
+ (the pull restores production's older schema, and `emdash migrate` only runs
114
+ after the build) would all publish. `vitops emdash prepare-db` (`vpr prepare-local-db`, run after the
115
+ pull by `vpr deploy`, `vpr deploy:dev` and the reusable workflow) starts `astro
116
+ dev` once, calls EmDash's `dev-bypass` setup (which migrates and applies the
117
+ seed's sample content) and stops it; `vitops emdash check-prerender` (part of
118
+ `vpr build`) then fails the build if any prerendered page is not a real HTML page
119
+ with a title. Run `vpr prepare-local-db` by hand before a first local `vpr build`.
120
+ It only writes the disposable `.wrangler/state`; the dev administrator it creates
121
+ is never deployed.
122
+ - **Freshness is a rebuild.** Production is the truth for content, and a page
123
+ reflects it as of the last build. Publishing in the admin therefore has to
124
+ trigger one: the deploy workflows listen for a `repository_dispatch` event of
125
+ type `emdash-publish` (see "Publish-triggered rebuilds" below).
126
+ - **Drafts and previews.** A prerendered page cannot show an unpublished
127
+ draft, so an admin "Preview" link shows the last published build. If previews
128
+ matter, add a dedicated on-demand route for them rather than removing
129
+ `prerender` from the public ones.
130
+ - **Dynamic parts are server islands.** Anything that must be fresh per request
131
+ goes in its own component rendered with `server:defer` inside the prerendered
132
+ page; the surrounding HTML stays static. Keep islands rare — each is a Worker
133
+ request after load.
134
+ - **Unknown URLs.** There is no `Astro.rewrite('/404')`: a slug not listed by
135
+ `getStaticPaths()` has no route, so Astro serves the prerendered `404.html`
136
+ with a real 404 status. Rewriting an on-demand route to a prerendered one
137
+ fails (500), so don't reintroduce it.
138
+ - **The contact form is the one on-demand route.** The `vitops.form` block posts a plain
139
+ `<form method="post">` to `src/pages/api/track.ts` (`prerender = false`, built with
140
+ `createConversionRoute()`), which also answers the conversion beacons. The block works with
141
+ no JavaScript: the route answers a form post with a 303 back to the page plus `?sent=1#form-sent`
142
+ or `?error=1#form-error`, and the block's two notices are revealed by that fragment. Keep one
143
+ form per page. Where submissions go: `site.notifications.email` (or the primary location's
144
+ `email`) and `site.crm` when set. **With neither, the route refuses the post** rather than
145
+ accept a message nobody receives, so set `site.notifications.email` before launch and run
146
+ `vitops email setup` for its sender domain. A failed CRM write never fails the request; it is
147
+ stated in the notification email, which is the durability floor.
148
+ - **Spam protection**: a honeypot field, the route's origin / size / per-IP rate checks, and
149
+ Cloudflare Turnstile once `vitops setup turnstile` has put the public key in
150
+ `site.security.turnstile.siteKey` and saved the Worker secret `VITOPS_TURNSTILE_SECRET`
151
+ (never in `site.json`). With a key set the route fails closed if the secret is missing.
152
+ `TURNSTILE_REQUIRED` in the route (default `true`) makes a token-less post a refusal, which
153
+ means the form needs JavaScript; set it `false` to keep a no-JS submit working and accept
154
+ that a bot can then skip the token. Turnstile is category `necessary` (it protects the form
155
+ being sent), loads only when a visitor first uses the form, and its origin is allowed in the
156
+ Report-Only CSP in `public/_headers`.
157
+ - **Privacy policy inventory.** `vitops legal` derives "what personal information we collect"
158
+ from `site.templates` entries of type `form`, not from the editor's block, so a form added in
159
+ the admin is invisible to it. When you add or change form fields, mirror them in a
160
+ `site.templates` form entry in `site.json`, or the generated policy under-reports.
161
+ - A field named `phone` submits as `tel` (the route reserves `phone` and `event`).
162
+ - The per-IP rate limit is per Worker isolate; add a KV-backed `RateLimitStore` if you need a
163
+ fleet-wide one.
85
164
  - The Worker cron trigger in `wrangler.jsonc` drives scheduled publishing —
86
165
  don't remove it.
87
166
  - Hosting (adapter + EmDash database/storage) comes from `vitopsHosting()`
@@ -110,7 +189,7 @@ styled by the Vitops design system.
110
189
  a finished policy, because the risk here is a plausible-looking document
111
190
  getting shipped unread.
112
191
  To replace them properly, fill in `site.json`'s `organization`/`site.legal`
113
- fields and run `npx vitops legal --format portable-text --out ./legal`, then
192
+ fields and run `vp exec vitops legal --format portable-text --out ./legal`, then
114
193
  paste each document's blocks into the page in the admin. That renders a real
115
194
  policy from what the site actually does — the analytics provider it uses,
116
195
  the fields its forms collect, where it deploys — rather than from guesses.
@@ -129,7 +208,7 @@ styled by the Vitops design system.
129
208
  - **Submit `/sitemap.xml`** to search engines, not a per-collection sitemap;
130
209
  collection sitemaps now split into pages of 2,000 entries.
131
210
  - **A stuck D1 migration lock** is cleared with
132
- `pnpm exec emdash migrate --take-over-lock <id>` (confirms the target, or
211
+ `vp exec emdash migrate --take-over-lock <id>` (confirms the target, or
133
212
  needs `--expected-target-fingerprint` non-interactively).
134
213
 
135
214
  ## Docs MCP servers
@@ -137,6 +216,92 @@ styled by the Vitops design system.
137
216
  `.mcp.json` preconfigures the Astro docs and EmDash docs MCP servers — prefer
138
217
  searching them over pre-trained knowledge for Astro/EmDash questions.
139
218
 
219
+ ## Labels: badge and tag
220
+
221
+ Two label patterns, both enabled in `site.json`'s `patterns.items`: **`badge`** is a
222
+ static label (a status, count or category) and **`tag`** is a label that may be
223
+ static _or_ dynamic (editable, dismissable, in a `tag-list`). Don't invent a third
224
+ name — `chip` and `pill` are not part of this vocabulary. The button pattern's class
225
+ is `btn` (`<a class="btn">` is styled, not just a bare `<button>`).
226
+
227
+ ## Custom blocks
228
+
229
+ Before adding a block type, a `src/components/blocks/*` renderer, or anything an
230
+ editor places on a page, read `.agents/skills/custom-blocks/SKILL.md`: compose the
231
+ existing `vitops.*` blocks first, extend one second, go site-local third, upstream
232
+ last. Block types can't be renamed or deleted once pages use them, so check the
233
+ framework's pattern names before choosing one.
234
+
235
+ ## Publish-triggered rebuilds
236
+
237
+ Pages are static, so a publish in the admin goes live only after a rebuild.
238
+ `.github/workflows/deploy-prod.yml` also runs on a `repository_dispatch` event of
239
+ type `emdash-publish` (it pins `ref` to `main`, because a dispatch runs from the
240
+ default branch, `dev`). Wire an EmDash plugin hook to send it:
241
+
242
+ ```jsonc
243
+ // emdash-plugin.jsonc
244
+ "capabilities": ["content:read", "network:request"],
245
+ "allowedHosts": ["api.github.com"]
246
+ ```
247
+
248
+ ```ts
249
+ // src/plugin.ts
250
+ hooks: {
251
+ 'content:afterPublish': async (_event, ctx) => {
252
+ const token = await ctx.settings.get<string>('githubToken'); // an encrypted plugin setting
253
+ if (!token || !ctx.http) return;
254
+ await ctx.http.fetch(`https://api.github.com/repos/${OWNER}/${REPO}/dispatches`, {
255
+ method: 'POST',
256
+ headers: { authorization: `Bearer ${token}`, accept: 'application/vnd.github+json' },
257
+ body: JSON.stringify({ event_type: 'emdash-publish' }),
258
+ });
259
+ },
260
+ }
261
+ ```
262
+
263
+ The token is a fine-grained GitHub token with `Contents: write` on this repo only,
264
+ stored as the plugin's encrypted setting (a sandboxed plugin can't read Worker
265
+ environment variables), never in the repo. The hook name, capabilities and
266
+ `ctx.http` / `ctx.settings` calls follow EmDash's plugin reference but this exact
267
+ plugin has not been run against a live site, so treat it as a starting point. Two
268
+ costs to know: the run goes through the `production` environment, so it **waits for
269
+ the reviewers' approval** like any prod deploy (relax that rule if editors' publishes
270
+ should go live unattended), and bursts of publishes queue behind the
271
+ `deploy-prod` concurrency group rather than cancelling each other. Without the hook,
272
+ a manual run of **Deploy prod** (or a push to `main`) does the same job.
273
+
274
+ ## Content Security Policy
275
+
276
+ `public/_headers` ships a **`Content-Security-Policy-Report-Only`** header: it logs
277
+ violations to the browser console and blocks nothing, so it can't break a page. Its
278
+ `script-src` is `'self'` plus the SHA-256 of each inline script `<Head />` emits (the
279
+ colour-scheme bootstrap and the consent-gate stub) — no `'unsafe-inline'`. Hashes,
280
+ not nonces, because prerendered pages have no per-request response to put a nonce in.
281
+ `contentSecurityPolicy()` and `inlineScriptHashes()` from `@getvitops/astro` produce
282
+ the same strings (`templates.test.ts` in the toolchain keeps the file in step).
283
+
284
+ Know what it does not cover before you flip it to enforcing (rename the header to
285
+ `Content-Security-Policy`):
286
+
287
+ - **`_headers` applies to static assets only.** Worker-rendered responses — the EmDash
288
+ admin (`/_emdash/*`), its API, anything left on-demand — don't get it. Setting a
289
+ header there means doing it in code (e.g. Astro middleware calling
290
+ `contentSecurityPolicy()`), and the admin's own inline scripts and editor HTML
291
+ would need allowing; leave the admin out of an enforced policy.
292
+ - **Editor-authored HTML blocks are the hard limit.** Any block that lets an editor
293
+ paste raw HTML or a `<script>` (an embed, a tracking snippet) puts arbitrary inline
294
+ script on the page, which a hash policy cannot name ahead of time. Enforcing a strict
295
+ policy means removing such blocks or moving embeds to a consent-gated
296
+ `data-consent` tag whose origin you allow explicitly.
297
+ - **Third-party tags** the consent gate activates need their origins in `script-src`
298
+ / `connect-src`; anything with `frame` needs `frame-src`.
299
+ - **A server island** (`server:defer`) adds an inline loader script and a request to
300
+ the Worker; expect a report for the loader until you hash it.
301
+
302
+ Run the site, browse the pages you ship with the console open, fix or allow what is
303
+ reported, and only then enforce.
304
+
140
305
  ## EmDash agent skills
141
306
 
142
307
  The EmDash skills (`building-emdash-site`, `creating-plugins`, `emdash-cli`,
@@ -146,20 +311,55 @@ project, not globally, so they match the `emdash` version this repo pins. If
146
311
  and `SETUP.html` have the full step):
147
312
 
148
313
  ```bash
149
- npx skills add emdash-cms/skills --skill '*' -y
314
+ vp dlx skills add emdash-cms/skills --skill '*' -y
150
315
  ```
151
316
 
152
- Refresh them with `npx skills update` after bumping `emdash` — the
317
+ Refresh them with `vp dlx skills update` after bumping `emdash` — the
153
318
  `upgrading-emdash` skill covers the rest of the upgrade.
154
319
 
155
320
  ## Commands
156
321
 
157
- - `pnpm dev` — local dev with local D1/R2 emulation (no Cloudflare account)
158
- - `pnpm typecheck` — `astro check`
159
- - `pnpm deploy` — build + `wrangler deploy` prod (provisions D1/R2 on first run)
160
- - `pnpm deploy:dev` — build + deploy the isolated dev worker (needs `DEV_D1_ID`)
161
- - `pnpm run init:github` — one-time: create + push the GitHub repo (main + dev,
162
- dev default). Only run when the user asks to set up GitHub.
163
- - `pnpm run init:hosting` — one-time: provision the hosting platform (dev
164
- D1/R2 + repo secrets on cloudflare). Only run when the user asks for it.
165
- - `npx vitops validate site.json` — schema-check the site config
322
+ This is a **Vite+** project: use `vp` (and its `vpr` shorthand), not `pnpm`/`npm`/`npx`.
323
+ The package manager underneath is pnpm, but `vp` picks it up for you.
324
+
325
+ | To… | Run |
326
+ | ---------------------------------------- | ------------------------------------------------------- |
327
+ | install dependencies | `vp install` |
328
+ | add / remove / update a package | `vp add <pkg>` · `vp remove <pkg>` · `vp update` |
329
+ | run a `package.json` script | `vpr <script>` (alias for `vp run <script>`) |
330
+ | run a locally installed binary | `vp exec <bin>` (e.g. `vp exec vitops docs components`) |
331
+ | run a one-off package without installing | `vp dlx <pkg>` |
332
+ | format + lint + typecheck (files) | `vp check` |
333
+ | typecheck Astro (`.astro` files) | `vpr typecheck` |
334
+
335
+ **Scripts that wrap Astro must go through `vpr`.** `vp dev` / `vp build` run Vite
336
+ directly and skip Astro, so use `vpr dev`, `vpr build`, `vpr preview`.
337
+
338
+ **`vpr dev` returns immediately.** `astro dev` now starts the server in the
339
+ background. Check it with `vp exec astro dev status`, read its output with
340
+ `vp exec astro dev logs --follow`, and stop it with `vp exec astro dev stop`; the
341
+ site is at <http://localhost:4321>. Don't wait on it as if it were a foreground
342
+ process.
343
+
344
+ | Script | What it does |
345
+ | ----------------------------------- | ------------------------------------------------------------------------------------------- |
346
+ | `vpr dev` | local dev with local D1/R2 emulation (no Cloudflare account) |
347
+ | `vpr deploy` | pull production content, build, `wrangler deploy` prod (provisions D1/R2 on first run) |
348
+ | `vpr deploy:dev` | same, for the isolated dev worker (needs `DEV_D1_ID`) |
349
+ | `vpr pull-content` | just the content pull (see "Rendering and freshness") |
350
+ | `vpr prepare-local-db` | migrate and seed the local D1 the build prerenders from |
351
+ | `vpr init:github` | one-time: create + push the GitHub repo (main + dev, dev default). Only when the user asks. |
352
+ | `vpr init:hosting` | one-time: provision hosting (dev D1/R2 + repo secrets). Only when the user asks. |
353
+ | `vp exec vitops validate site.json` | schema-check the site config |
354
+
355
+ **Release flow.** A site is not a published package, so there is no changeset or
356
+ version bump: ship by committing to `dev` (auto-deploys the dev worker) and then
357
+ running the **promote** workflow. `vp` has no `vp change` / `vp pm change` command
358
+ (checked against vp 1.5.0); for a _package_ it does have `vp pm version`,
359
+ `vp pm publish` and `vp pm stage`.
360
+
361
+ **Naming the project.** `vp create` takes the package name from the target
362
+ directory and invents a random slug when none is given, so pass one:
363
+ `vp create @getvitops:emdash --directory <client-slug>`. `wrangler.jsonc`'s
364
+ `name`, D1 `database_name` and R2 `bucket_name` are separate and still say
365
+ `my-emdash-site` — rename them to match before the first deploy.
@@ -3,7 +3,7 @@
3
3
  An [EmDash CMS](https://docs.emdashcms.com) website on Cloudflare Workers,
4
4
  styled by the [Vitops design system](https://www.npmjs.com/org/getvitops).
5
5
 
6
- Scaffolded from `vp create @getvitops:emdash`.
6
+ Scaffolded from `vp create @getvitops:emdash --directory <client-slug>` (pass a directory: `vp create` derives the package name from it and invents a random slug otherwise).
7
7
 
8
8
  ## Stack
9
9
 
@@ -15,7 +15,7 @@ Scaffolded from `vp create @getvitops:emdash`.
15
15
  `src/assets/logo.svg`, and serves the web-component runtime
16
16
  - **`@getvitops/emdash`** — design-system Portable Text blocks in the editor
17
17
  slash menu (hero, cards, accordion, carousel, gallery, marquee, banner, …);
18
- `npx vitops docs blocks` lists every block and field
18
+ `vp exec vitops docs blocks` lists every block and field
19
19
 
20
20
  `src/assets/placeholder/` ships neutral wireframe images for layout work
21
21
  before real imagery exists — delete once real assets land (separate from
@@ -24,8 +24,8 @@ before real imagery exists — delete once real assets land (separate from
24
24
  ## Local development
25
25
 
26
26
  ```bash
27
- pnpm install
28
- pnpm dev
27
+ vp install
28
+ vpr dev
29
29
  ```
30
30
 
31
31
  Once this is its own repo (not sitting inside the `getvitops/design-system`
@@ -36,22 +36,30 @@ honours that option at a project's actual root, and errors on it otherwise.
36
36
  No Cloudflare account needed: `astro dev` runs the Worker locally with local
37
37
  D1/R2 emulation (state in `.wrangler/`). On first boot EmDash runs its
38
38
  migrations and applies the structure from `seed/seed.json` — a `pages`
39
- collection, a `primary` menu, and the site settings.
39
+ collection, three menus (`primary`, `footer-sitemap`, `footer-legal`), and the site
40
+ settings.
40
41
 
41
42
  - Site: <http://localhost:4321>
42
43
  - Admin: <http://localhost:4321/_emdash/admin>
43
44
 
44
45
  Open the admin and complete the setup wizard — it creates your first account,
45
46
  and choosing to **include sample content** publishes the starter pages from
46
- the seed: home, about, terms (`/terms`), and privacy policy (`/privacy`).
47
- Until then the home route shows a friendly empty state. Unknown paths render
48
- `src/pages/404.astro`.
47
+ the seed: home, about, contact (`/contact`), terms (`/terms`), privacy policy
48
+ (`/privacy`) and cookie notice (`/cookies`) — every link in the seeded menus
49
+ resolves. Until then the home route shows a friendly empty state. Unknown paths
50
+ get the prerendered `src/pages/404.astro` with a real 404 status.
51
+
52
+ **Pages are prerendered.** `vpr build` writes static HTML for every published
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
55
+ reaches the build (`scripts/pull-content.mjs`) and for the publish-triggered rebuild.
56
+ In `vpr dev` the pages read your local database, so edits appear straight away.
49
57
 
50
58
  The two legal pages are placeholders. To generate real ones from what your site
51
59
  actually does, fill in `site.json`'s `organization`/`site.legal` fields and run:
52
60
 
53
61
  ```sh
54
- npx vitops legal --format portable-text --out ./legal
62
+ vp exec vitops legal --format portable-text --out ./legal
55
63
  ```
56
64
 
57
65
  Then paste each document's blocks into the matching page in the admin. The
@@ -61,8 +69,8 @@ before you publish.
61
69
  ## Deploy
62
70
 
63
71
  ```bash
64
- npx wrangler login
65
- pnpm deploy
72
+ vp exec wrangler login
73
+ vpr deploy
66
74
  ```
67
75
 
68
76
  Wrangler provisions the D1 database and R2 bucket named in `wrangler.jsonc`
@@ -92,7 +100,7 @@ One-time setup — see [Start a new EmDash site](https://docs.vitops.ca/guides/n
92
100
  procedure, then work through the checklist:
93
101
 
94
102
  ```bash
95
- pnpm run sop && open SETUP.html # or: xdg-open SETUP.html
103
+ vpr sop && open SETUP.html # or: xdg-open SETUP.html
96
104
  ```
97
105
 
98
106
  It covers host choice, GitHub + branch policy, hosting resources, going live
@@ -102,7 +110,7 @@ command per step. An AI agent can work the same list autonomously via
102
110
  `.agents/skills/setup/`; it stops at the steps that genuinely need a human
103
111
  (the Cloudflare API token, the domain cutover, publishing legal documents).
104
112
 
105
- `pnpm deploy:dev` reproduces the dev deploy locally: it rewrites the
113
+ `vpr deploy:dev` reproduces the dev deploy locally: it rewrites the
106
114
  adapter-generated wrangler config (`scripts/build-dev-wrangler.mjs`) so the
107
115
  dev worker never touches prod data — it hard-fails without `DEV_D1_ID`,
108
116
  on purpose. Schema/content changes live in the EmDash database, not git, so
@@ -119,7 +127,7 @@ is a configurable dimension of the project, not a rewrite:
119
127
  belong to this target.
120
128
  - **`node`** — `@astrojs/node` (standalone) + SQLite file + local uploads,
121
129
  for a VPS / docker-compose / k8s host. Not installed by default; to switch:
122
- `pnpm add @astrojs/node better-sqlite3`, then `vitopsHosting({ target:
130
+ `vp add @astrojs/node better-sqlite3`, then `vitopsHosting({ target:
123
131
  'node' })` (or set `HOSTING=node` — the env var wins). Run the built site
124
132
  with `node ./dist/server/entry.mjs`; data lives in `./data/` (gitignored) —
125
133
  persist it as a volume. Scheduled publishing runs in-process on Node, so
@@ -139,12 +147,12 @@ and media in the storage backend (bucket/directory copy).
139
147
  agents answer from current documentation. Your running site also exposes its
140
148
  own content MCP server at `/_emdash/api/mcp` (authenticated).
141
149
  - **Skills** (`.agents/skills/`): `promote` dispatches the dev → main
142
- promotion workflow. Run `npx vitops agents` to also write the design-system
150
+ promotion workflow. Run `vp exec vitops agents` to also write the design-system
143
151
  class reference into `AGENTS.md`.
144
152
  - **EmDash skills** are installed per project so they match the pinned `emdash`
145
- version: `npx skills add emdash-cms/skills --skill '*' -y`,
153
+ version: `vp dlx skills add emdash-cms/skills --skill '*' -y`,
146
154
  then commit `.agents/skills/`, the `.claude/skills/` symlinks and `skills-lock.json`. Update with
147
- `npx skills update` after an `emdash` bump.
155
+ `vp dlx skills update` after an `emdash` bump.
148
156
  - **Claude Code on the web**: the SessionStart hook
149
157
  (`.claude/hooks/session-start.sh`) installs Node 24 (matching
150
158
  `devEngines`), activates the pinned pnpm, and installs dependencies in the
@@ -164,27 +172,30 @@ routes stay unstyled by it.
164
172
  Useful commands (via `@getvitops/cli`):
165
173
 
166
174
  ```bash
167
- npx vitops validate site.json # check the config against the schema
168
- npx vitops agents # write the design-system reference into AGENTS.md
169
- pnpm build:css # regenerate the CSS manually
175
+ vp exec vitops validate site.json # check the config against the schema
176
+ vp exec vitops agents # write the design-system reference into AGENTS.md
177
+ vpr build:css # regenerate the CSS manually
170
178
  ```
171
179
 
172
180
  ## Project map
173
181
 
174
- | Path | Purpose |
175
- | -------------------------------- | ------------------------------------------------------------------------------------------ |
176
- | `site.json` | Org + site config; design tokens/patterns at `designSystem.themes.default` → generated CSS |
177
- | `seed/seed.json` | First-boot content model + starter content |
178
- | `src/worker.ts` | Worker entry (fetch + EmDash scheduled handler) |
179
- | `src/live.config.ts` | Astro live content collections wiring for EmDash |
180
- | `src/layouts/Layout.astro` | Site chrome: EmDash + Vitops head tags, nav from the CMS menu |
181
- | `src/pages/index.astro` | Home — renders the `home` page entry |
182
- | `src/pages/[...slug].astro` | Catch-all — renders any `pages` entry at its slug |
183
- | `src/pages/404.astro` | Not-found page (the catch-all rewrites here) |
184
- | `wrangler.jsonc` | Cloudflare bindings (D1/R2) + cron trigger |
185
- | `scripts/build-dev-wrangler.mjs` | Rewrites the built wrangler config for the isolated dev worker |
186
- | `SETUP.html` | Client-onboarding checklist — run `pnpm run sop` first (see "One-time setup") |
187
- | `.github/workflows/` | deploy-dev (push to `dev`), deploy-prod (push to `main`), promote |
188
- | `.agents/skills/promote/` | Agent skill: dispatch the dev → main promotion |
189
- | `.mcp.json` | Astro + EmDash docs MCP servers for coding agents |
190
- | `.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.md "Content Security Policy") |
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 |