@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.
- package/CHANGELOG.md +103 -0
- package/package.json +2 -2
- package/templates/emdash/.agents/skills/custom-blocks/SKILL.md +104 -0
- package/templates/emdash/.agents/skills/setup/SKILL.md +4 -4
- package/templates/emdash/.claude/hooks/session-start.sh +4 -1
- package/templates/emdash/.github/workflows/deploy-prod.yml +12 -0
- package/templates/emdash/AGENTS.md +228 -28
- package/templates/emdash/README.md +48 -37
- package/templates/emdash/SETUP.html +45 -27
- package/templates/emdash/_gitignore +3 -0
- package/templates/emdash/astro.config.mjs +14 -1
- package/templates/emdash/package.json +7 -5
- package/templates/emdash/public/_headers +19 -4
- package/templates/emdash/scripts/pull-content.mjs +83 -0
- package/templates/emdash/seed/seed.json +188 -6
- package/templates/emdash/site.json +102 -3
- package/templates/emdash/src/cloudflare-workers.d.ts +9 -0
- package/templates/emdash/src/layouts/Layout.astro +27 -45
- package/templates/emdash/src/pages/404.astro +4 -3
- package/templates/emdash/src/pages/[...slug].astro +22 -12
- package/templates/emdash/src/pages/api/track.ts +158 -0
- package/templates/emdash/src/pages/index.astro +10 -9
- package/templates/static/.agents/skills/setup/SKILL.md +4 -4
- package/templates/static/AGENTS.md +68 -14
- package/templates/static/README.md +7 -7
- package/templates/static/SETUP.html +45 -27
- package/templates/static/public/_headers +16 -4
- package/templates/static/site.json +54 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
This is an EmDash CMS website (Astro
|
|
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 (`
|
|
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`, …). `
|
|
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 `
|
|
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
|
-
`
|
|
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
|
-
- **
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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 `
|
|
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
|
-
`
|
|
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
|
-
|
|
314
|
+
vp dlx skills add emdash-cms/skills --skill '*' -y
|
|
150
315
|
```
|
|
151
316
|
|
|
152
|
-
Refresh them with `
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
28
|
-
|
|
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,
|
|
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`),
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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 `
|
|
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: `
|
|
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
|
-
`
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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 —
|
|
182
|
-
| `src/pages/[...slug].astro` |
|
|
183
|
-
| `src/pages/404.astro` |
|
|
184
|
-
| `
|
|
185
|
-
| `scripts/
|
|
186
|
-
| `
|
|
187
|
-
|
|
|
188
|
-
|
|
|
189
|
-
| `.
|
|
190
|
-
| `.
|
|
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 |
|