@getvitops/create 0.8.1 → 0.9.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 '*' -a claude-code -y`,
146
- then commit `.agents/skills/` and `skills-lock.json`. Update with
147
- `npx skills update` after an `emdash` bump.
153
+ version: `vp dlx skills add emdash-cms/skills --skill '*' -y`,
154
+ then commit `.agents/skills/`, the `.claude/skills/` symlinks and `skills-lock.json`. Update with
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 |
@@ -7,7 +7,7 @@
7
7
  <!--
8
8
  Opened straight from disk (file://) before anything is deployed, which is
9
9
  when most of these steps happen — so it links a relative, pre-generated
10
- stylesheet rather than the site's own build output. Run `pnpm run sop`
10
+ stylesheet rather than the site's own build output. Run `vpr sop`
11
11
  first (it's a one-line `vitops generate -f css` into .vitops/sop/); the
12
12
  banner below explains this if the stylesheet 404s, and the markup stays
13
13
  legible either way — every item here is a real checkbox in a real
@@ -21,7 +21,7 @@
21
21
  -->
22
22
  <link id="sop-stylesheet" rel="stylesheet" href=".vitops/sop/styles.css" />
23
23
  <style>
24
- /* Minimal fallback so the page is still legible before `pnpm run sop`
24
+ /* Minimal fallback so the page is still legible before `vpr sop`
25
25
  has run once — intentionally not framework CSS, since it only ever
26
26
  applies in the narrow window before the real stylesheet exists. */
27
27
  body {
@@ -42,7 +42,7 @@
42
42
  <header class="region rhythm">
43
43
  <h1>Client onboarding checklist</h1>
44
44
  <p id="no-stylesheet-notice" class="banner" hidden>
45
- This page isn't styled yet — run <code>pnpm run sop</code> once (it generates
45
+ This page isn't styled yet — run <code>vpr sop</code> once (it generates
46
46
  <code>.vitops/sop/styles.css</code>), then reload.
47
47
  </p>
48
48
  <p>
@@ -90,7 +90,7 @@
90
90
  <span class="custom-check__box"
91
91
  ><span class="custom-check__indicator custom-check__indicator--check"></span
92
92
  ></span>
93
- <span> Preview it: <code>npx vitops setup repo --dry</code> </span>
93
+ <span> Preview it: <code>vp exec vitops setup repo --dry</code> </span>
94
94
  </label>
95
95
  <label class="custom-check">
96
96
  <input type="checkbox" data-sop="repo.run" />
@@ -98,7 +98,7 @@
98
98
  ><span class="custom-check__indicator custom-check__indicator--check"></span
99
99
  ></span>
100
100
  <span>
101
- Run it: <code>npx vitops setup repo</code> — creates the GitHub repo, pushes
101
+ Run it: <code>vp exec vitops setup repo</code> — creates the GitHub repo, pushes
102
102
  <code>main</code> + <code>dev</code>, makes <code>dev</code> default, and requests
103
103
  branch protection + the <code>production</code>/<code>dev</code>
104
104
  environments.
@@ -126,7 +126,7 @@
126
126
  <span class="custom-check__box"
127
127
  ><span class="custom-check__indicator custom-check__indicator--check"></span
128
128
  ></span>
129
- <span> Preview it: <code>npx vitops setup hosting --dry</code> </span>
129
+ <span> Preview it: <code>vp exec vitops setup hosting --dry</code> </span>
130
130
  </label>
131
131
  <label class="custom-check">
132
132
  <input type="checkbox" data-sop="hosting.run" />
@@ -134,10 +134,10 @@
134
134
  ><span class="custom-check__indicator custom-check__indicator--check"></span
135
135
  ></span>
136
136
  <span>
137
- Run it: <code>npx vitops setup hosting</code> — provisions the isolated dev D1
137
+ Run it: <code>vp exec vitops setup hosting</code> — provisions the isolated dev D1
138
138
  database + R2 bucket (EmDash template) and sets the <code>DEV_D1_ID</code> /
139
139
  <code>CLOUDFLARE_ACCOUNT_ID</code> repo secrets. Requires
140
- <code>npx wrangler login</code> first.
140
+ <code>vp exec wrangler login</code> first.
141
141
  </span>
142
142
  </label>
143
143
  <label class="custom-check">
@@ -276,7 +276,7 @@
276
276
  ><span class="custom-check__indicator custom-check__indicator--check"></span
277
277
  ></span>
278
278
  <span>
279
- Deploy once (<code>pnpm deploy</code>), note the printed
279
+ Deploy once (<code>vpr deploy</code>), note the printed
280
280
  <code>*.workers.dev</code> URL, and set
281
281
  <code>site.environments.production.url</code> in <code>site.json</code> to it.
282
282
  </span>
@@ -315,8 +315,8 @@
315
315
  ><span class="custom-check__indicator custom-check__indicator--check"></span
316
316
  ></span>
317
317
  <span>
318
- Preview it: <code>npx vitops setup domain --domain &lt;apex&gt; --dry</code> — this
319
- only plans and checks; it never applies the cutover itself.
318
+ Preview it: <code>vp exec vitops setup domain --domain &lt;apex&gt; --dry</code> —
319
+ this only plans and checks; it never applies the cutover itself.
320
320
  </span>
321
321
  </label>
322
322
  <label class="custom-check">
@@ -326,7 +326,7 @@
326
326
  ></span>
327
327
  <span>
328
328
  Uncomment the <code>routes</code> block in <code>wrangler.jsonc</code>, set your
329
- apex, and deploy: <code>pnpm deploy</code>.
329
+ apex, and deploy: <code>vpr deploy</code>.
330
330
  </span>
331
331
  </label>
332
332
  <label class="custom-check">
@@ -347,7 +347,8 @@
347
347
  ><span class="custom-check__indicator custom-check__indicator--check"></span
348
348
  ></span>
349
349
  <span>
350
- Re-run <code>npx vitops robots</code> and <code>npx vitops search notify</code>.
350
+ Re-run <code>vp exec vitops robots</code> and
351
+ <code>vp exec vitops search notify</code>.
351
352
  </span>
352
353
  </label>
353
354
  </div>
@@ -373,7 +374,7 @@
373
374
  <span class="custom-check__box"
374
375
  ><span class="custom-check__indicator custom-check__indicator--check"></span
375
376
  ></span>
376
- <span> Check it: <code>npx vitops validate site.json</code> </span>
377
+ <span> Check it: <code>vp exec vitops validate site.json</code> </span>
377
378
  </label>
378
379
  </div>
379
380
  </details>
@@ -398,8 +399,8 @@
398
399
  ><span class="custom-check__indicator custom-check__indicator--check"></span
399
400
  ></span>
400
401
  <span>
401
- Preview then run: <code>npx vitops setup search --dry</code>, then
402
- <code>npx vitops setup search</code>. This is a <strong>user OAuth</strong>
402
+ Preview then run: <code>vp exec vitops setup search --dry</code>, then
403
+ <code>vp exec vitops setup search</code>. This is a <strong>user OAuth</strong>
403
404
  action, deliberately — verifying a site makes the caller an owner, and that should
404
405
  be a person, not a deploy robot.
405
406
  </span>
@@ -410,8 +411,25 @@
410
411
  ><span class="custom-check__indicator custom-check__indicator--check"></span
411
412
  ></span>
412
413
  <span>
413
- Preview then run: <code>npx vitops setup ads --dry</code>, then
414
- <code>npx vitops setup ads</code>.
414
+ Preview then run: <code>vp exec vitops setup ads --dry</code>, then
415
+ <code>vp exec vitops setup ads</code>.
416
+ </span>
417
+ </label>
418
+ <label class="custom-check">
419
+ <input type="checkbox" data-sop="connections.turnstile" />
420
+ <span class="custom-check__box"
421
+ ><span class="custom-check__indicator custom-check__indicator--check"></span
422
+ ></span>
423
+ <span>
424
+ If the site has a contact form, create its spam check:
425
+ <code>vp exec vitops setup turnstile --dry</code>, then
426
+ <code>vp exec vitops setup turnstile</code>. It needs
427
+ <code>CLOUDFLARE_API_TOKEN</code> with
428
+ <strong>Account &gt; Turnstile &gt; Edit</strong> (plus
429
+ <code>CLOUDFLARE_ACCOUNT_ID</code>) and <code>wrangler login</code>. The public site
430
+ key goes into <code>site.security.turnstile.siteKey</code>; the secret is saved as
431
+ the Worker secret <code>VITOPS_TURNSTILE_SECRET</code> and never touches
432
+ <code>site.json</code>. It never edits or deletes an existing widget.
415
433
  </span>
416
434
  </label>
417
435
  <label class="custom-check">
@@ -453,10 +471,10 @@
453
471
  ></span>
454
472
  <span>
455
473
  Generate real documents from the filled-in config —
456
- <code>npx vitops legal --format portable-text</code> (EmDash; paste into the admin)
457
- or <code>npx vitops legal --format md</code> (static; commit the output). The seeded
458
- <code>/terms</code> and <code>/privacy</code> pages are deliberately short,
459
- obviously-unfinished placeholders.
474
+ <code>vp exec vitops legal --format portable-text</code> (EmDash; paste into the
475
+ admin) or <code>vp exec vitops legal --format md</code> (static; commit the output).
476
+ The seeded <code>/terms</code> and <code>/privacy</code> pages are deliberately
477
+ short, obviously-unfinished placeholders.
460
478
  </span>
461
479
  </label>
462
480
  <label class="custom-check">
@@ -484,7 +502,7 @@
484
502
  ></span>
485
503
  <span>
486
504
  Robots directive matches the live origin's intent (blocked while on workers.dev,
487
- open after cutover) — <code>npx vitops robots</code>.
505
+ open after cutover) — <code>vp exec vitops robots</code>.
488
506
  </span>
489
507
  </label>
490
508
  <label class="custom-check">
@@ -509,7 +527,7 @@
509
527
  ><span class="custom-check__indicator custom-check__indicator--check"></span
510
528
  ></span>
511
529
  <span>
512
- <code>npx vitops lint --src</code> and <code>pnpm typecheck</code> both clean.
530
+ <code>vp exec vitops lint --src</code> and <code>vpr typecheck</code> both clean.
513
531
  </span>
514
532
  </label>
515
533
  <label class="custom-check">
@@ -527,7 +545,7 @@
527
545
  <span class="custom-check__box"
528
546
  ><span class="custom-check__indicator custom-check__indicator--check"></span
529
547
  ></span>
530
- <span> <code>npx vitops search notify</code> run once live. </span>
548
+ <span> <code>vp exec vitops search notify</code> run once live. </span>
531
549
  </label>
532
550
  <label class="custom-check">
533
551
  <input type="checkbox" data-sop="launch.check" />
@@ -535,8 +553,8 @@
535
553
  ><span class="custom-check__indicator custom-check__indicator--check"></span
536
554
  ></span>
537
555
  <span>
538
- A day or two later: <code>npx vitops search notify --check</code> confirms Google
539
- actually indexed it.
556
+ A day or two later: <code>vp exec vitops search notify --check</code> confirms
557
+ Google actually indexed it.
540
558
  </span>
541
559
  </label>
542
560
  </div>
@@ -12,6 +12,9 @@ worker-configuration.d.ts
12
12
  # wrangler local state (local D1/R2 data lives here during `astro dev`)
13
13
  .wrangler/
14
14
 
15
+ # production content dump `scripts/pull-content.mjs` loads into the local build DB
16
+ .vitops/content.sql
17
+
15
18
  # node hosting target: SQLite db + local uploads
16
19
  data/
17
20
 
@@ -23,6 +23,15 @@ const organizationName = resolveOrganizationName(config) ?? 'My EmDash Site';
23
23
  // data migration, not a rewrite — see README "Hosting".
24
24
  const { adapter, database, storage } = await vitopsHosting();
25
25
 
26
+ // Favicon/PWA colours come from site.json rather than a second copy here that
27
+ // drifts when the palette changes: the browser-UI colour is the `brand-primary`
28
+ // role's hue seed. (The page background behind the maskable icon is a computed
29
+ // ramp step, not a config value, so `backgroundColor` below stays a literal.)
30
+ const colors = config.designSystem?.themes?.default?.colors ?? {};
31
+ const roleHue = (role) =>
32
+ typeof colors.roles?.[role] === 'string' ? colors.roles[role] : colors.roles?.[role]?.hue;
33
+ const brandColor = colors.palette?.[roleHue('brand-primary')]?.seed ?? '#2e9b73';
34
+
26
35
  export default defineConfig({
27
36
  output: 'server',
28
37
  adapter,
@@ -79,7 +88,7 @@ export default defineConfig({
79
88
  favicon: {
80
89
  source: 'src/assets/logo.svg',
81
90
  name: organizationName,
82
- themeColor: '#2e9b73',
91
+ themeColor: brandColor,
83
92
  backgroundColor: '#ffffff',
84
93
  },
85
94
  // inject: false — Layout.astro imports the generated stylesheet, so
@@ -91,6 +100,10 @@ export default defineConfig({
91
100
  // there instead of a bare design-system.json. legal/fonts/search/ads/
92
101
  // reviews all read the same file (see AGENTS.md's "Client onboarding").
93
102
  css: { format: 'tailwind', inject: false, input: 'site.json' },
103
+ // The /design-system admin page embeds public/vitops/design-system-editor.html in an
104
+ // iframe; this writes it, with its manifest, editor.js and editor.css, without loading
105
+ // the editor on public pages.
106
+ editorManifest: true,
94
107
  site: { input: 'site.json' },
95
108
  }),
96
109
  ],
@@ -5,10 +5,12 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "astro dev",
8
- "build": "astro build",
8
+ "build": "astro build && vitops emdash check-prerender",
9
9
  "preview": "astro preview",
10
- "deploy": "vitops email check && astro build && emdash migrate --wrangler-config wrangler.jsonc && wrangler deploy",
11
- "deploy:dev": "vitops email check && astro build && node scripts/build-dev-wrangler.mjs && emdash migrate --wrangler-config dist/server/wrangler.dev.json && wrangler deploy --config dist/server/wrangler.dev.json",
10
+ "pull-content": "node scripts/pull-content.mjs",
11
+ "prepare-local-db": "vitops emdash prepare-db",
12
+ "deploy": "vitops email check && node scripts/pull-content.mjs && vitops emdash prepare-db && astro build && vitops emdash check-prerender && emdash migrate --wrangler-config wrangler.jsonc && wrangler deploy",
13
+ "deploy:dev": "vitops email check && node scripts/pull-content.mjs && vitops emdash prepare-db && astro build && vitops emdash check-prerender && node scripts/build-dev-wrangler.mjs && emdash migrate --wrangler-config dist/server/wrangler.dev.json && wrangler deploy --config dist/server/wrangler.dev.json",
12
14
  "init:github": "vitops setup repo",
13
15
  "init:hosting": "vitops setup hosting",
14
16
  "setup": "vitops setup all",
@@ -21,13 +23,13 @@
21
23
  "dependencies": {
22
24
  "@astrojs/cloudflare": "^14.0.0",
23
25
  "@astrojs/react": "^7.1.2",
24
- "@emdash-cms/cloudflare": "^1.4.0",
26
+ "@emdash-cms/cloudflare": "latest",
25
27
  "@getvitops/astro": "latest",
26
28
  "@getvitops/core": "latest",
27
29
  "@getvitops/emdash": "latest",
28
30
  "@tailwindcss/vite": "^4.3.3",
29
31
  "astro": "^7.4.4",
30
- "emdash": "^1.4.0",
32
+ "emdash": "latest",
31
33
  "react": "^19.3.1",
32
34
  "react-dom": "^19.3.1",
33
35
  "tailwindcss": "^4.3.3"
@@ -1,9 +1,24 @@
1
- # Cloudflare Workers static assets apply this file to every asset response.
1
+ # Cloudflare static-assets response headers. They apply to files served from the
2
+ # static asset store — the prerendered pages — and NOT to responses the Worker
3
+ # renders itself (the EmDash admin at /_emdash/*, its API, anything left
4
+ # on-demand). See AGENTS.md "Content Security Policy".
2
5
  #
3
- # A baseline that cannot break a page. No Content-Security-Policy on purpose: <Head /> emits
4
- # inline scripts (the consent stub, the colour-scheme bootstrap), so a strict policy needs
5
- # per-site nonces or hashes — add one deliberately once you know which third-party tags you load.
6
+ # REPORT-ONLY on purpose: violations are logged to the browser console and
7
+ # nothing is blocked, so this cannot break a page. Read the console on the
8
+ # pages you ship, add what is legitimately needed, then rename the header to
9
+ # `Content-Security-Policy` to enforce it.
10
+ #
11
+ # The two sha256 values are the inline scripts <Head /> emits (the colour-scheme
12
+ # bootstrap and the consent-gate stub). They change only when
13
+ # @getvitops/astro changes those scripts; regenerate with
14
+ # vp exec node -e "import('@getvitops/astro').then(m => console.log(m.contentSecurityPolicy()))"
15
+ # `templates.test.ts` in the toolchain repo guards that they match.
16
+ #
17
+ # Expect reports for scripts this policy cannot name by hash: EmDash's own head
18
+ # tags, a server-island loader, an editor's HTML block, and third-party tags the
19
+ # consent gate activates (allow their origins in script-src/connect-src).
6
20
  /*
21
+ Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'sha256-CEiVlTo192T9Rug/nAl+ihvkqOQkYdr6Rq3oc+gR9JU=' 'sha256-z/UqsITVvNGOJSaOSvhN1rdtyYdMIxpQ/lMXM1q0g90=' https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-src https://challenges.cloudflare.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'
7
22
  X-Content-Type-Options: nosniff
8
23
  Referrer-Policy: strict-origin-when-cross-origin
9
24
  Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
@@ -0,0 +1,83 @@
1
+ // Pre-build step: load production's EmDash content into the LOCAL D1 the build
2
+ // prerenders from.
3
+ //
4
+ // Why this exists: `src/pages/index.astro` and `[...slug].astro` are prerendered
5
+ // (`prerender = true` + `getStaticPaths`), so their HTML is written during
6
+ // `astro build`. The Cloudflare adapter prerenders inside a local workerd with a
7
+ // LOCAL D1 (`.wrangler/state`), never the deployed database — so on a CI runner
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.md), so this
10
+ // copies production's D1 into the local one first.
11
+ //
12
+ // wrangler d1 export <db> --remote → .vitops/content.sql
13
+ // wrangler d1 execute <db> --local ← .vitops/content.sql (state cleared first)
14
+ //
15
+ // Read-only against production (an export). It only ever writes the local
16
+ // `.wrangler/state/v3/d1` directory, which is disposable build state.
17
+ //
18
+ // Fails CLOSED. A deploy that proceeds after a failed pull would publish an empty
19
+ // site over a good one, so any failure aborts the deploy — except one case that is
20
+ // not a failure: the remote database does not exist yet (the first deploy, where
21
+ // wrangler provisions it). Building from the seed is only right once the local database is
22
+ // migrated and seeded, which is `scripts/prepare-local-db.mjs`'s job, run straight after this.
23
+ //
24
+ // Skip with SKIP_PULL_CONTENT=1 (e.g. a deliberate seed-only build).
25
+
26
+ import { execFileSync, spawnSync } from 'node:child_process';
27
+ import { mkdirSync, readFileSync, rmSync } from 'node:fs';
28
+
29
+ if (process.env.SKIP_PULL_CONTENT) {
30
+ console.log('pull-content: SKIP_PULL_CONTENT set — prerendering from the local/seed database.');
31
+ process.exit(0);
32
+ }
33
+
34
+ /** wrangler.jsonc allows comments and trailing commas; JSON.parse does not. */
35
+ function parseJsonc(text) {
36
+ const out = text
37
+ .replace(/("(?:[^"\\]|\\.)*")|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, (m, str) => str ?? '')
38
+ .replace(/,(\s*[}\]])/g, '$1');
39
+ return JSON.parse(out);
40
+ }
41
+
42
+ const config = parseJsonc(readFileSync('wrangler.jsonc', 'utf8'));
43
+ const db = config.d1_databases?.[0]?.database_name;
44
+ if (!db) {
45
+ console.error('pull-content: wrangler.jsonc has no d1_databases[0].database_name.');
46
+ process.exit(1);
47
+ }
48
+
49
+ const dump = '.vitops/content.sql';
50
+ mkdirSync('.vitops', { recursive: true });
51
+
52
+ const wrangler = (args) =>
53
+ spawnSync('wrangler', args, { encoding: 'utf8', env: { ...process.env, CI: '1' } });
54
+
55
+ const exported = wrangler([
56
+ 'd1',
57
+ 'export',
58
+ db,
59
+ '--remote',
60
+ '--output',
61
+ dump,
62
+ '--skip-confirmation',
63
+ ]);
64
+ if (exported.status !== 0) {
65
+ const text = `${exported.stdout}\n${exported.stderr}`;
66
+ if (/not found|does not exist|no such database|couldn'?t find|could not find|7404/i.test(text)) {
67
+ console.warn(
68
+ `pull-content: remote database "${db}" does not exist yet (first deploy?) — ` +
69
+ 'prerendering from the seed.',
70
+ );
71
+ process.exit(0);
72
+ }
73
+ console.error(`pull-content: could not export "${db}" from production:\n${text}`);
74
+ console.error('Refusing to build — prerendering now would publish an empty site.');
75
+ process.exit(1);
76
+ }
77
+
78
+ // Replaying onto a populated local database collides on CREATE TABLE, so start clean.
79
+ rmSync('.wrangler/state/v3/d1', { recursive: true, force: true });
80
+ execFileSync('wrangler', ['d1', 'execute', db, '--local', '--file', dump, '--yes'], {
81
+ stdio: 'inherit',
82
+ });
83
+ console.log(`pull-content: loaded production "${db}" into the local build database.`);