@getvitops/create 0.9.0-alpha.1 → 0.9.0-alpha.3
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 +44 -0
- package/package.json +2 -2
- package/templates/emdash/.github/workflows/deploy-prod.yml +1 -1
- package/templates/emdash/AGENTS.md +162 -331
- package/templates/emdash/README.md +5 -5
- package/templates/emdash/_gitignore +13 -2
- package/templates/emdash/astro.config.mjs +2 -0
- package/templates/emdash/node_modules/.vitest-cache/CACHEDIR.TAG +3 -0
- package/templates/emdash/node_modules/.vitest-cache/_metadata.json +3 -0
- package/templates/emdash/package.json +6 -4
- package/templates/emdash/public/_headers +1 -1
- package/templates/emdash/scripts/pull-content.mjs +1 -1
- package/templates/emdash/seed/seed.json +14 -12
- package/templates/emdash/site.json +11 -0
- package/templates/emdash/src/pages/[...slug].astro +1 -1
- package/templates/emdash/src/pages/api/track.ts +2 -2
- package/templates/emdash/src/pages/index.astro +2 -2
- package/templates/static/AGENTS.md +24 -2
- package/templates/static/_gitignore +10 -0
- package/templates/static/package.json +3 -1
- package/templates/emdash/.agents/skills/custom-blocks/SKILL.md +0 -104
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# @getvitops/create
|
|
2
2
|
|
|
3
|
+
## 0.9.0-alpha.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 8543f73: `vitops agents` (also `vitops agents sync`) now keeps a short fenced block at the **top** of `AGENTS.md` instead of appending a long one. The block names the config, says to prefer the framework's classes, and links the skills linked under `.agents/skills` from installed `@getvitops/*` packages (with a one-line "use when …" from each), plus `vitops docs <topic>` for reference; the generate-command list and the topic list are gone. A later run replaces only that block in place and leaves the rest of the file alone; with no block, it is added at the top; a block from an older release is updated where it is. The command also links every skill an installed `@getvitops/skills` ships, so it works without the `skills` CLI. The templates run it from `prepare` and ship the block already in `AGENTS.md`.
|
|
8
|
+
|
|
9
|
+
## 0.9.0-alpha.2
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 0a47856: Build correctness and gaps from the second upstream round.
|
|
14
|
+
|
|
15
|
+
- **`@getvitops/emdash`: fixes a circular import that emptied the plugin block map in the static build.** `Section.astro` and `WidgetArea.astro` imported `emdash/ui` statically, which imports `virtual:emdash/block-components`, which is `@getvitops/emdash/astro` again. Depending on import order, every `vitops_*` block inside a section body was dropped from prerendered pages with only a `PortableText [components.type] is missing …` warning. Both now import it lazily, and a test fails if any renderer imports `emdash/ui` statically. No code change needed in sites.
|
|
16
|
+
- **`@getvitops/cli`: `vitops emdash build`** runs `astro build` and then `check-prerender`, and fails when the build reports a block type with no component (reported once per type). The template's `build`, `deploy` and `deploy:dev` scripts use it; existing sites replace `astro build && vitops emdash check-prerender` with `vitops emdash build`. `check-prerender` also warns (fails with `--strict`) on a page without exactly one `<h1>`.
|
|
17
|
+
- **`@getvitops/emdash`: `vitops_hero` and `vitops_section` heading level** accepts `h1`–`h6` and `p` (a heading that is styled but not in the outline). The hero defaults to `h1`; set the level to `2` on a page whose layout already renders an `h1` (the template's seed does).
|
|
18
|
+
- **`@getvitops/emdash`: `vitops_logos`** shows an entry's name as text (`.logos__name`) when it has no image. `@getvitops/core` adds the rule (all formats).
|
|
19
|
+
- **`@getvitops/emdash`: an empty content widget** (no text, no blocks) no longer renders its title, and an area whose widgets are all empty renders nothing.
|
|
20
|
+
- **`@getvitops/create`:** the template's `pages` collection gets `urlPattern: "/{slug}"` (the sitemap listed `/pages/<slug>`, which 404s; existing sites change it under Content Types), and `.gitignore` ignores only `src/styles/tailwind.css` and `tokens.json`, so site CSS kept in `src/styles/` is committed.
|
|
21
|
+
- **`@getvitops/emdash`: per-item colour.** The rows of `vitops_cards`, `vitops_stats`, `vitops_steps` and the hero's `cards` take their own `tone` / `color` (blank inherits the block's), so one block can hold a pine item and an amber one. Hero cards also take an `eyebrow` and a button (`ctaLabel` / `ctaHref`; with a button the card is no longer a stretched link).
|
|
22
|
+
- **`@getvitops/generator` / `@getvitops/emdash`: solid fills read.** The generator emits `--color-<hue>-<step>-on` for every step of every hue, the foreground for a fill at that step picked by the same APCA rule as `text-on-<role>` (css and tailwind formats, a plain `:root`, not `@theme`). A block fill chosen by `color` (hue and step) uses it instead of a fixed white..
|
|
23
|
+
- **`@getvitops/core`: `.counter__value.sr-only`** hides itself, so a build that never emitted the `.sr-only` utility no longer shows a counting figure twice (all formats).
|
|
24
|
+
- **`@getvitops/astro` / `@getvitops/core`: `SiteFooter` groups.** A top-level item of the `footer-sitemap` menu with children becomes a column (its label is the heading, its children the links, one level only); childless items stay in the flat list. Each column is a named `<nav>`.
|
|
25
|
+
- **`@getvitops/astro`: planned links and header buttons.** A menu item with `draft` in its CSS classes stays in the menu but is not rendered (at every level, in the header and the footer). `<SiteNav>` has an `actions` slot for header buttons that are not menu items, and an item with `cta` in its CSS classes renders as a button.
|
|
26
|
+
- **`@getvitops/cli`: `check-prerender` warnings** (fail with `--strict`): links to pages the build did not produce, a form with no delivery address (no `site.notifications.email`, location `email` or `site.crm`), and form fields missing from every `site.templates` form entry (the generated privacy policy reads those). It reads `./site.json`, or `--site=<path>`. `vitops setup … --dry-run` is accepted as `--dry`.
|
|
27
|
+
- **`@getvitops/emdash` / `@getvitops/astro`: "tick any number" form field** (`vitops_form` field kind `checkboxes`): a fieldset of checkboxes sharing one name. The conversion route joins repeated values (`IT, Marketing`) instead of keeping the last. The "required" toggle is not applied to the group (HTML cannot require one of several).
|
|
28
|
+
- **`@getvitops/core`: size hooks.** `.steps__title`, `.stats__figure` and `.card__title` read `--steps-title-size` (default one step below `heading`), `--stats-figure-size` and `--card-title-size`, set on the block through its `style` field. They set the `--heading-fs` / `--display-fs` properties the type-role utilities already read, so no extra class or prop is needed (all formats).
|
|
29
|
+
- **`@getvitops/create`:** the template's `site.json` lists its contact form under `site.templates`, so the privacy policy inventory and the new check agree.
|
|
30
|
+
- **`@getvitops/emdash`: `theme` on every block, on widget areas and on your own markup.** A `theme` select (`<role>` tinted, `<role>:solid` filled, populated from the site's roles by a new `themes/list` route) is a common field on every `vitops_*` block, applied to the block's own root with the role's computed readable text. `<VitopsWidgetArea theme="…">` takes the same, and `themeAttrs('surface')` gives `{ class, style }` for a layout `<body>`. `vitops_section`'s existing `theme` now goes through the same path.
|
|
31
|
+
- **`@getvitops/cli` / `@getvitops/emdash`: pages with a dropped block fail the build.** `vitops_section` and widget areas emit an invisible `<template data-vitops-unrendered="…">` for any body block with no renderer, and `check-prerender` fails the page naming the types (nested bodies only; `vitops emdash build` also scans the build log, which covers top-level bodies). A regression test imports the plugin entry first with a mock of the real virtual module, so the old import cycle fails it.
|
|
32
|
+
- **`@getvitops/emdash`: `formTemplateFromBlock(block)`** turns a saved `vitops_form` value into a `site.templates` form entry (names as submitted, a `checkboxes` field as a `checkbox` with options), to paste into `site.json` so the generated privacy policy lists what the form collects. The build cannot read the CMS database, so this is run by hand; `check-prerender` warns when fields are missing.
|
|
33
|
+
- **`@getvitops/utils`: `hasFormDelivery`** (`@getvitops/utils/notify`): the one test for "has this form anywhere to go", used by `check-prerender`.
|
|
34
|
+
- **`@getvitops/emdash`:** a `color` hue tint now flips with the colour scheme (`light-dark()`); hero option cards sit at `h2` under an `h1` hero; menu items also accept `hidden: true` (same as the `draft` class); `vitops docs blocks` documents `theme`, the per-row colours, the `--<role>-fs` type-size hook and the form checks.
|
|
35
|
+
- **`@getvitops/create`:** the template redirects `/home` to `/`, and the guides use `vp exec` instead of `vpx`.
|
|
36
|
+
|
|
37
|
+
### Patch Changes
|
|
38
|
+
|
|
39
|
+
- 0a47856: **Breaking: every block type is now `vitops_<name>`** (`vitops_cards`, `vitops_hero`, `vitops_actionLink`, …), where the Portable Text blocks were `vitops.<name>` and the section block type was already `vitops_section`. EmDash validates a block type's slug as `[a-z][a-z0-9_]*`, so `vitops.section` was never possible and the toolkit had two styles for one kind of thing. Underscore is the one form valid everywhere, so it is now the only one.
|
|
40
|
+
|
|
41
|
+
Migration: in stored content and in `seed/seed.json`, rename `_type` values from `vitops.<name>` to `vitops_<name>`; in `PortableText` `components`, rename the keys the same way; the `blocks: { include, exclude }` option takes full type names, so rename those too. `vt_*` names in a site are that site's own types and are unaffected. A search-and-replace of `vitops.(actionLink|imageCompare|copyButton|banner|details|carousel|gallery|cards|accordion|hero|marquee|logos|steps|stats|callout|entries|pricing|embed|form|reviews|nav|badges|widgetArea)` with `vitops_$1` covers it. `vitops emdash build` reports any block left on the old name as having no component.
|
|
42
|
+
|
|
43
|
+
- 0a47856: New package `@getvitops/skills`: agent skills for sites built with the toolchain (`custom-blocks`, `content-and-deploys`, `contact-form`, `csp`). The emdash template depends on it and runs `skills experimental_sync` on `prepare`, which links them into `.agents/skills/` and `.claude/skills/`, so they update with `vp update` instead of being copied at scaffold time. Existing sites: `vp add @getvitops/skills && vp add -D skills`, set `prepare` to `vp config && skills experimental_sync -y -a claude-code --no-frozen-lockfile`, gitignore the four linked names, and delete the site's own copies of those four skills (the sync never replaces a directory it did not create).
|
|
44
|
+
|
|
45
|
+
The emdash template's `AGENTS.md` is shortened to the always-relevant rules. Deploy and content mechanics (where each kind of data is the truth, prerendering, publish-triggered rebuilds, refreshing dev), the contact form, and the CSP move into those skills. Corrects the nesting guidance: a block type with a `portableText` field (such as `vitops_section`) holds other blocks, while `vitops_*` Portable Text blocks do not. Adds guidance for agents to record reusable toolkit gaps in `UPSTREAM.md`. The generated stylesheet is no longer described as always `tailwind.css`.
|
|
46
|
+
|
|
3
47
|
## 0.9.0-alpha.1
|
|
4
48
|
|
|
5
49
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getvitops/create",
|
|
3
|
-
"version": "0.9.0-alpha.
|
|
3
|
+
"version": "0.9.0-alpha.3",
|
|
4
4
|
"description": "Project templates for the Vitops design system — scaffold wired-up sites with `vp create @getvitops`.",
|
|
5
5
|
"homepage": "https://docs.vitops.ca",
|
|
6
6
|
"bugs": "https://github.com/getvitops/design-system/issues",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"access": "public"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@getvitops/generator": "8.3.0-alpha.
|
|
22
|
+
"@getvitops/generator": "8.3.0-alpha.3"
|
|
23
23
|
},
|
|
24
24
|
"engines": {
|
|
25
25
|
"node": ">=24"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
#
|
|
11
11
|
# Also runs on a `repository_dispatch` of type `emdash-publish`: pages are
|
|
12
12
|
# prerendered, so a publish in the EmDash admin only goes live after a rebuild,
|
|
13
|
-
# and an EmDash `content:afterPublish` hook POSTs that event (
|
|
13
|
+
# and an EmDash `content:afterPublish` hook POSTs that event (.agents/skills/content-and-deploys,
|
|
14
14
|
# "Publish-triggered rebuilds"). A dispatch runs this file from the DEFAULT branch
|
|
15
15
|
# (`dev`), so `ref` pins the build to `main` — production's code, not dev's. It
|
|
16
16
|
# still goes through the `production` environment, so it still waits for the
|
|
@@ -1,365 +1,196 @@
|
|
|
1
|
+
<!-- vitops:start -->
|
|
2
|
+
|
|
3
|
+
Built with the Vitops design system (`@getvitops/*`); tokens and site facts live in `site.json`.
|
|
4
|
+
Prefer its classes and patterns over hand-written CSS. Load the skill that matches the task:
|
|
5
|
+
|
|
6
|
+
- [contact-form](.agents/skills/contact-form/SKILL.md) — Use when adding or changing a form, fixing submissions that don't arrive, setting up noti…
|
|
7
|
+
- [content-and-deploys](.agents/skills/content-and-deploys/SKILL.md) — Use for any deploy or build problem, stale or missing content, "my publish isn't live", s…
|
|
8
|
+
- [csp](.agents/skills/csp/SKILL.md) — Use when changing `public/_headers`, adding an embed, tracking snippet or third-party scr…
|
|
9
|
+
- [custom-blocks](.agents/skills/custom-blocks/SKILL.md) — Use before creating, renaming or deleting any block type, any `src/components/blocks/*` r…
|
|
10
|
+
- [vitops-design-system](.agents/skills/vitops-design-system/SKILL.md) — Use when styling or authoring pages or components (choosing utility/pattern classes, colo…
|
|
11
|
+
|
|
12
|
+
Reference, rendered from this project: `vitops docs <topic>` (no topic lists them).
|
|
13
|
+
<!-- managed by `vitops agents sync` · @getvitops/cli@8.3.0-alpha.2 — edit outside this block -->
|
|
14
|
+
<!-- vitops:end -->
|
|
15
|
+
|
|
1
16
|
This is an EmDash CMS website (Astro on Cloudflare Workers, D1 + R2 — public pages
|
|
2
17
|
prerendered, the Worker serving the admin and API) styled by the Vitops design system.
|
|
3
18
|
|
|
4
19
|
## Ground rules
|
|
5
20
|
|
|
6
|
-
- `site.json` is the single source of truth
|
|
7
|
-
|
|
8
|
-
`
|
|
9
|
-
|
|
10
|
-
and
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
21
|
+
- `site.json` is the single source of truth: organization + site facts, and the design system
|
|
22
|
+
under `designSystem.themes.default`. Never edit the generated stylesheet
|
|
23
|
+
(`src/styles/tailwind.css` with the default `tailwind` format; `css.format` in
|
|
24
|
+
`astro.config.mjs` picks it). It is rebuilt from `site.json` on every dev/build and gitignored.
|
|
25
|
+
Only `src/styles/tailwind.css` and `tokens.json` are ignored: put your own CSS in a separate file
|
|
26
|
+
(for example `src/site.css`, imported from the layout) and commit it.
|
|
27
|
+
- Content lives in the EmDash database, not the repo. Read it with `getEmDashCollection` /
|
|
28
|
+
`getEmDashEntry` from `emdash`; render Portable Text with `<PortableText />` from `emdash/ui`
|
|
29
|
+
(the `vitops_*` blocks from `@getvitops/emdash` are merged in).
|
|
30
|
+
- **Where truth lives:** content (entries, media, menus) is **production's**, edited there; code,
|
|
31
|
+
`site.json` and schema are **git's** (`dev` → `main`). The dev database is a disposable copy
|
|
32
|
+
refreshed from prod, never promoted back. See `.agents/skills/content-and-deploys/SKILL.md`.
|
|
33
|
+
- `seed/seed.json` applies only to an empty database. To change a running site's content model, use
|
|
34
|
+
the admin (`/_emdash/admin`) or its MCP server (`/_emdash/api/mcp`).
|
|
35
|
+
- Hosting comes from `vitopsHosting()` in `astro.config.mjs` — never hard-code an adapter or
|
|
36
|
+
d1/r2/sqlite. Switch with `vitopsHosting({ target })` or the `HOSTING` env var.
|
|
37
|
+
- Keep the Worker cron trigger in `wrangler.jsonc`: it publishes content scheduled in the admin.
|
|
38
|
+
- `src/assets/placeholder/` is wireframe stand-in imagery; delete it once real assets land.
|
|
39
|
+
- Prefer framework classes (utilities, patterns, type roles like `font-heading`) over bespoke CSS.
|
|
40
|
+
`vp exec vitops docs classes` prints the full class reference.
|
|
41
|
+
- `SETUP.html` is the onboarding checklist (`.agents/skills/setup/`). Tick a box only for a step you
|
|
42
|
+
completed, never for a human-only one (the Cloudflare API token, domain cutover, legal documents).
|
|
43
|
+
|
|
44
|
+
## Building pages
|
|
45
|
+
|
|
46
|
+
- The editor's insert menu offers the `vitops_*` blocks. `vp exec vitops docs blocks` lists them
|
|
47
|
+
with every field; `vp exec vitops docs components` lists every pattern, which tiers provide it
|
|
48
|
+
and which block renders it.
|
|
49
|
+
- **Nesting.** A block type with a `portableText` field, such as `vitops_section`, holds other
|
|
50
|
+
blocks. A `vitops_*` block holds only scalar fields and repeaters (cards, gallery images,
|
|
51
|
+
accordion items, slides), so it can't. EmDash's built-in `columns` doesn't render `vitops_*`
|
|
52
|
+
children. For layout, nest in a section, use a `blocks` field, or hand-write the template with
|
|
53
|
+
`@getvitops/astro` components (`Subgrid`, `Cards`, `Carousel`, …).
|
|
54
|
+
- **Section, block or widget area?** Copy-and-edit content is an EmDash Section. The same content
|
|
55
|
+
everywhere (announcement bar, pre-footer call to action, sidebar, footer columns) is a **widget
|
|
56
|
+
area**: define it under Widgets in the admin and render it with `<VitopsWidgetArea name="…" />`
|
|
57
|
+
from `@getvitops/emdash/astro` in the layout, or in page content with the `vitops_widgetArea`
|
|
58
|
+
block. Page-specific content is blocks. An area can't hold another area, and prerendered pages
|
|
59
|
+
show an edit after the next deploy (like menus).
|
|
60
|
+
- **Escape hatches on every block:** `theme` (a backdrop from the site's colour roles, `surface` or
|
|
61
|
+
`brand-primary:solid`, with the readable text for it picked by the design system, so contrast is
|
|
62
|
+
not the editor's judgement), `anchor` (the `id`, for `#links`), `className` (extra classes; under
|
|
63
|
+
the tailwind format only ones the site already uses) and `style` (CSS custom properties only,
|
|
64
|
+
e.g. `--p-card: 2rem`; anything else is dropped). The same backdrop is available on a widget area
|
|
65
|
+
(`<VitopsWidgetArea theme="…">`) and on your own markup (`themeAttrs('surface')` from
|
|
66
|
+
`@getvitops/emdash/astro`, spread on the layout's `<body>`). How a role looks (flat, raised,
|
|
67
|
+
glass) is the site's own role styling in `site.json`. Set `--header-offset` to the sticky
|
|
68
|
+
header's height so anchors clear it.
|
|
69
|
+
- Override a block per site with `<PortableText components={{ type: { 'vitops_cards': MyCards } }} />`;
|
|
70
|
+
hide one with `vitopsEmdash({ blocks: { exclude: [...] } })`.
|
|
71
|
+
- **One `<h1>` per page.** The template's page routes render the title as an `<h1>`, and a hero's
|
|
72
|
+
heading level defaults to `h1`, so set a hero's level to `2` on those pages (or let the hero own
|
|
73
|
+
the `h1`). Use `p` for a display heading that is not part of the outline. `vitops emdash build`
|
|
74
|
+
warns on any page without exactly one `<h1>`.
|
|
75
|
+
- Labels: `badge` is static (status, count, category); `tag` may be editable or dismissable. Don't
|
|
76
|
+
invent `chip` or `pill`. The button class is `btn`.
|
|
77
|
+
- **Before adding a block type** or a `src/components/blocks/*` renderer, read
|
|
78
|
+
`.agents/skills/custom-blocks/SKILL.md`. Types can't be renamed or deleted once pages use them.
|
|
79
|
+
|
|
80
|
+
## Layout foundations — the answer, not a starting point
|
|
81
|
+
|
|
82
|
+
| About to write… | Write instead |
|
|
83
|
+
| ------------------------------------------------------- | --------------- |
|
|
84
|
+
| a `wrap`/`wrapper`/`container`/`inner` class | **`centered`** |
|
|
85
|
+
| `max-width` + `margin-inline: auto` | **`centered`** |
|
|
86
|
+
| `padding-block` on a section | **`region`** |
|
|
87
|
+
| margins between headings/paragraphs/lists | **`rhythm`** |
|
|
88
|
+
| `display: grid` + `grid-template-columns: repeat(n, …)` | **`subgrid`** |
|
|
89
|
+
| `repeat(auto-fit, minmax(…))` | **`grid-auto`** |
|
|
90
|
+
| `display: flex` + `gap` for a row of controls | **`cluster`** |
|
|
91
|
+
|
|
92
|
+
`centered` is a grid of _named tracks_: a child widens itself with `breakout`/`spotlight`/
|
|
93
|
+
`fullbleed` without the parent knowing, which a bespoke `.wrap` can't do.
|
|
94
|
+
|
|
95
|
+
- **More than one card means `subgrid`**, so headings and CTAs align across the set:
|
|
96
|
+
`<ul class="subgrid subgrid-cols-3" role="list">` with `<li class="card subgrid-card">`, or
|
|
97
|
+
`<Subgrid />` (renders the slot verbatim; author the `<li>`s).
|
|
98
|
+
- **A card that is a link:** `stretched-link` inside it is zero-JS but kills text selection;
|
|
99
|
+
`<Cards>` keeps selection and falls back to the same link with no JS. Never both. Never
|
|
100
|
+
`<li><a class="card">`: the `li` is the grid item, so the tranches don't align.
|
|
101
|
+
- `vpr lint:design` reports all of these and the pre-commit hook fails on them. Fix the class, not
|
|
102
|
+
the CSS.
|
|
103
|
+
|
|
104
|
+
## Rendering and the contact form
|
|
105
|
+
|
|
106
|
+
- Every public page is **prerendered**; the Worker handles `/_emdash/*` and `src/pages/api/track.ts`.
|
|
107
|
+
An admin publish goes live after a rebuild. Run `vpr prepare-local-db` before a first local
|
|
108
|
+
`vpr build`. Details, previews and server islands: `.agents/skills/content-and-deploys/SKILL.md`.
|
|
109
|
+
- The contact form posts to `/api/track`. Set `site.notifications.email` before launch or the route
|
|
110
|
+
refuses posts, and mirror form fields in `site.templates`. Details:
|
|
111
|
+
`.agents/skills/contact-form/SKILL.md`.
|
|
112
|
+
- `public/_headers` ships a Report-Only CSP. Read `.agents/skills/csp/SKILL.md` before enforcing it
|
|
113
|
+
or adding embeds.
|
|
114
|
+
- `vitops audit` needs optional packages and a browser, checked before it starts a server:
|
|
39
115
|
`vp add -D playwright @axe-core/playwright lighthouse && vp exec playwright install chromium`.
|
|
40
116
|
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).
|
|
48
|
-
- `seed/seed.json` only applies against an empty database: structure
|
|
49
|
-
(collections, menus, settings) on first boot; the `content` entries only
|
|
50
|
-
when the setup wizard's "include sample content" option is chosen. Changing
|
|
51
|
-
the seed does not migrate an existing site — use the admin
|
|
52
|
-
(`/_emdash/admin`) or the site's MCP server (`/_emdash/api/mcp`) to change
|
|
53
|
-
the content model of a running site.
|
|
54
|
-
- Prefer framework CSS classes (utilities, patterns, type roles like
|
|
55
|
-
`font-heading`) over bespoke CSS. Run `vp exec vitops agents` to append the
|
|
56
|
-
full generated class-vocabulary reference to this file.
|
|
57
|
-
- **Layout foundations — these are the answer, not a starting point.** Six
|
|
58
|
-
substitutions carry the structure of nearly every page:
|
|
59
|
-
|
|
60
|
-
| About to write… | Write instead |
|
|
61
|
-
| ------------------------------------------------------- | --------------- |
|
|
62
|
-
| a `wrap`/`wrapper`/`container`/`inner` class | **`centered`** |
|
|
63
|
-
| `max-width` + `margin-inline: auto` | **`centered`** |
|
|
64
|
-
| `padding-block` on a section | **`region`** |
|
|
65
|
-
| margins between headings/paragraphs/lists | **`rhythm`** |
|
|
66
|
-
| `display: grid` + `grid-template-columns: repeat(n, …)` | **`subgrid`** |
|
|
67
|
-
| `repeat(auto-fit, minmax(…))` | **`grid-auto`** |
|
|
68
|
-
| `display: flex` + `gap` for a row of controls | **`cluster`** |
|
|
69
|
-
|
|
70
|
-
`centered` is a grid of _named tracks_, not a max-width box — a child widens
|
|
71
|
-
itself with `breakout`/`spotlight`/`fullbleed` without the parent knowing, which
|
|
72
|
-
is the thing a bespoke `.wrap` can never do. **More than one card means
|
|
73
|
-
`subgrid`**: a plain grid aligns the outer boxes but not the tranches inside
|
|
74
|
-
them, so headings and CTAs land at different heights across the set. Write
|
|
75
|
-
`<ul class="subgrid subgrid-cols-3" role="list">` with
|
|
76
|
-
`<li class="card subgrid-card">` items, or `<Subgrid />` (which renders the slot
|
|
77
|
-
verbatim — author the `<li>`s yourself; it adds `role="list"` for you, and a
|
|
78
|
-
hand-written one needs it because a marker-less `<ul>` stops being announced as a
|
|
79
|
-
list in Safari).
|
|
80
|
-
|
|
81
|
-
**When the whole card is a link the card is still the item**, and pick by whether
|
|
82
|
-
the card's text must stay selectable: `stretched-link` on a link inside it is
|
|
83
|
-
zero-JS but kills text selection, while `<Cards>` (which wraps the list in
|
|
84
|
-
`<wc-cards>`) keeps selection and falls back to that same link with no JS. Never
|
|
85
|
-
both — the overlay wins and the JS never runs. Never `<li><a class="card">`: the
|
|
86
|
-
`li` is the grid item, so the anchor's tranches never align and it does not fill
|
|
87
|
-
the cell; it renders fine, which is why it keeps getting written.
|
|
88
|
-
|
|
89
|
-
`vpr lint:design` reports all of these, and the pre-commit hook fails on them
|
|
90
|
-
— the fix is the class, not a justification for the CSS.
|
|
91
|
-
|
|
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.
|
|
164
|
-
- The Worker cron trigger in `wrangler.jsonc` drives scheduled publishing —
|
|
165
|
-
don't remove it.
|
|
166
|
-
- Hosting (adapter + EmDash database/storage) comes from `vitopsHosting()`
|
|
167
|
-
(`@getvitops/emdash`) in astro.config.mjs — never hard-code an adapter or
|
|
168
|
-
d1/r2/sqlite there. Switch targets via `vitopsHosting({ target })` or the
|
|
169
|
-
`HOSTING` env var (see README "Hosting"); the `node` target needs
|
|
170
|
-
`@astrojs/node` + `better-sqlite3` installed first.
|
|
171
|
-
- **`SETUP.html` is the client-onboarding checklist.** When working it via the
|
|
172
|
-
`.agents/skills/setup/` skill, tick a box only for a step you actually
|
|
173
|
-
completed — never on the operator's behalf for a step marked human-only
|
|
174
|
-
(the Cloudflare API token, the domain cutover, publishing legal documents).
|
|
175
117
|
|
|
176
118
|
## Branch model
|
|
177
119
|
|
|
178
|
-
- Work on **`dev
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
-
|
|
209
|
-
|
|
210
|
-
- **
|
|
211
|
-
`
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
`.
|
|
217
|
-
|
|
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
|
-
|
|
305
|
-
## EmDash agent skills
|
|
306
|
-
|
|
307
|
-
The EmDash skills (`building-emdash-site`, `creating-plugins`, `emdash-cli`,
|
|
308
|
-
`upgrading-emdash` and the two WordPress porting skills) are installed per
|
|
309
|
-
project, not globally, so they match the `emdash` version this repo pins. If
|
|
310
|
-
`.agents/skills/building-emdash-site/` is missing, install them (the setup guide
|
|
311
|
-
and `SETUP.html` have the full step):
|
|
312
|
-
|
|
313
|
-
```bash
|
|
314
|
-
vp dlx skills add emdash-cms/skills --skill '*' -y
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
Refresh them with `vp dlx skills update` after bumping `emdash` — the
|
|
318
|
-
`upgrading-emdash` skill covers the rest of the upgrade.
|
|
120
|
+
- Work on **`dev`**; every push auto-deploys the isolated `-dev` worker.
|
|
121
|
+
- **`main`** is production and protected: never push or merge to it. It ships only through the
|
|
122
|
+
`promote` skill (`.agents/skills/promote/`), and only when the user explicitly asks.
|
|
123
|
+
- Sample legal pages (`/terms`, `/privacy`) are deliberately short, unfinished placeholders; don't
|
|
124
|
+
"improve" them into something that reads as a finished policy. To replace them, fill in
|
|
125
|
+
`site.json`'s `organization`/`site.legal`, run
|
|
126
|
+
`vp exec vitops legal --format portable-text --out ./legal`, and paste each document into the page
|
|
127
|
+
in the admin. It still needs review before launch.
|
|
128
|
+
|
|
129
|
+
## EmDash 1.x behaviours
|
|
130
|
+
|
|
131
|
+
- Passkeys are bound to the hostname they were created on. After a domain cutover use **Settings →
|
|
132
|
+
General → Change domain**, then **Continue on**.
|
|
133
|
+
- Link entries by `entry.data.slug`, never `entry.id` (multilingual sites prefix `id` with a locale).
|
|
134
|
+
- Submit `/sitemap.xml` to search engines, not a per-collection sitemap.
|
|
135
|
+
- Clear a stuck D1 migration lock with `vp exec emdash migrate --take-over-lock <id>` (needs
|
|
136
|
+
`--expected-target-fingerprint` non-interactively).
|
|
137
|
+
|
|
138
|
+
## Improving the toolkit
|
|
139
|
+
|
|
140
|
+
You are also a source of feedback for `@getvitops/*`. When you struggle with a framework built-in,
|
|
141
|
+
write a workaround, add bespoke CSS or a site-local block, or find a doc wrong or missing, and the
|
|
142
|
+
need is **not** unique to this site, record it in `UPSTREAM.md` at the repo root (create it if
|
|
143
|
+
absent). One entry each, with: what you tried, what failed or was missing, the workaround (file
|
|
144
|
+
path), the change you'd propose, and the package (`@getvitops/emdash`, `astro`, `core`, `cli`,
|
|
145
|
+
`create`, or docs). Mention new entries in your end-of-task summary. Don't log choices that are
|
|
146
|
+
specific to this site, and never patch `node_modules`.
|
|
147
|
+
|
|
148
|
+
## Docs and skills
|
|
149
|
+
|
|
150
|
+
- `.mcp.json` preconfigures the Astro and EmDash docs MCP servers; search them before relying on
|
|
151
|
+
pre-trained knowledge.
|
|
152
|
+
- **Skills.** `@getvitops/skills` ships `custom-blocks`, `content-and-deploys`, `contact-form` and
|
|
153
|
+
`csp`. `skills experimental_sync` (run by `prepare`, so by `vp install`) links them into
|
|
154
|
+
`.agents/skills/` and `.claude/skills/`; they are gitignored and update with `vp update`. A skill
|
|
155
|
+
you write yourself with the same name is never replaced. `promote` and `setup` are this site's own.
|
|
156
|
+
- The EmDash skills (`building-emdash-site`, `creating-plugins`, `emdash-cli`, `upgrading-emdash`,
|
|
157
|
+
two WordPress porting skills) are installed per project to match the pinned `emdash`. If
|
|
158
|
+
`.agents/skills/building-emdash-site/` is missing: `vp dlx skills add emdash-cms/skills --skill '*' -y`.
|
|
159
|
+
Refresh with `vp dlx skills update` after bumping `emdash`.
|
|
319
160
|
|
|
320
161
|
## Commands
|
|
321
162
|
|
|
322
|
-
This is a **Vite+** project: use `vp` (and
|
|
323
|
-
|
|
163
|
+
This is a **Vite+** project: use `vp` (and `vpr`), not `pnpm`/`npm`/`npx`. Scripts that wrap Astro
|
|
164
|
+
go through `vpr` (`vpr dev`, `vpr build`, `vpr preview`), because `vp dev`/`vp build` skip Astro.
|
|
165
|
+
`vpr dev` returns immediately (the server runs in the background): check it with
|
|
166
|
+
`vp exec astro dev status`, read `vp exec astro dev logs --follow`, stop with
|
|
167
|
+
`vp exec astro dev stop`; the site is at <http://localhost:4321>.
|
|
324
168
|
|
|
325
169
|
| To… | Run |
|
|
326
170
|
| ---------------------------------------- | ------------------------------------------------------- |
|
|
327
171
|
| install dependencies | `vp install` |
|
|
328
172
|
| add / remove / update a package | `vp add <pkg>` · `vp remove <pkg>` · `vp update` |
|
|
329
|
-
| run a `package.json` script | `vpr <script>`
|
|
173
|
+
| run a `package.json` script | `vpr <script>` |
|
|
330
174
|
| run a locally installed binary | `vp exec <bin>` (e.g. `vp exec vitops docs components`) |
|
|
331
175
|
| run a one-off package without installing | `vp dlx <pkg>` |
|
|
332
176
|
| format + lint + typecheck (files) | `vp check` |
|
|
333
177
|
| 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.
|
|
178
|
+
| design-lint the markup | `vpr lint:design` |
|
|
343
179
|
|
|
344
180
|
| Script | What it does |
|
|
345
181
|
| ----------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
346
182
|
| `vpr dev` | local dev with local D1/R2 emulation (no Cloudflare account) |
|
|
347
183
|
| `vpr deploy` | pull production content, build, `wrangler deploy` prod (provisions D1/R2 on first run) |
|
|
348
184
|
| `vpr deploy:dev` | same, for the isolated dev worker (needs `DEV_D1_ID`) |
|
|
349
|
-
| `vpr pull-content` | just the content pull
|
|
185
|
+
| `vpr pull-content` | just the content pull |
|
|
350
186
|
| `vpr prepare-local-db` | migrate and seed the local D1 the build prerenders from |
|
|
351
187
|
| `vpr init:github` | one-time: create + push the GitHub repo (main + dev, dev default). Only when the user asks. |
|
|
352
188
|
| `vpr init:hosting` | one-time: provision hosting (dev D1/R2 + repo secrets). Only when the user asks. |
|
|
353
189
|
| `vp exec vitops validate site.json` | schema-check the site config |
|
|
354
190
|
|
|
355
|
-
|
|
356
|
-
|
|
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`.
|
|
191
|
+
Shipping is not a package release: no changeset or version bump. Commit to `dev`, then promote.
|
|
192
|
+
(`vp` has no `vp change` command.)
|
|
360
193
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
`
|
|
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.
|
|
194
|
+
`vp create` names the package after the target directory (pass `--directory <client-slug>`).
|
|
195
|
+
`wrangler.jsonc`'s `name`, D1 `database_name` and R2 `bucket_name` are separate and still say
|
|
196
|
+
`my-emdash-site`; rename them to match before the first deploy.
|
|
@@ -51,7 +51,7 @@ get the prerendered `src/pages/404.astro` with a real 404 status.
|
|
|
51
51
|
|
|
52
52
|
**Pages are prerendered.** `vpr build` writes static HTML for every published
|
|
53
53
|
`pages` entry, so an edit in the admin goes live on the next rebuild rather than
|
|
54
|
-
per request — see
|
|
54
|
+
per request — see `.agents/skills/content-and-deploys` for how production content
|
|
55
55
|
reaches the build (`scripts/pull-content.mjs`) and for the publish-triggered rebuild.
|
|
56
56
|
In `vpr dev` the pages read your local database, so edits appear straight away.
|
|
57
57
|
|
|
@@ -147,8 +147,8 @@ and media in the storage backend (bucket/directory copy).
|
|
|
147
147
|
agents answer from current documentation. Your running site also exposes its
|
|
148
148
|
own content MCP server at `/_emdash/api/mcp` (authenticated).
|
|
149
149
|
- **Skills** (`.agents/skills/`): `promote` dispatches the dev → main
|
|
150
|
-
promotion workflow.
|
|
151
|
-
|
|
150
|
+
promotion workflow. The short block at the top of `AGENTS.md` (fenced by
|
|
151
|
+
`<!-- vitops:start -->`) lists the toolkit skills; `vp install` refreshes it (`vitops agents sync`).
|
|
152
152
|
- **EmDash skills** are installed per project so they match the pinned `emdash`
|
|
153
153
|
version: `vp dlx skills add emdash-cms/skills --skill '*' -y`,
|
|
154
154
|
then commit `.agents/skills/`, the `.claude/skills/` symlinks and `skills-lock.json`. Update with
|
|
@@ -173,7 +173,7 @@ Useful commands (via `@getvitops/cli`):
|
|
|
173
173
|
|
|
174
174
|
```bash
|
|
175
175
|
vp exec vitops validate site.json # check the config against the schema
|
|
176
|
-
vp exec vitops agents
|
|
176
|
+
vp exec vitops agents sync # refresh the managed block at the top of AGENTS.md and link the skills
|
|
177
177
|
vpr build:css # regenerate the CSS manually
|
|
178
178
|
```
|
|
179
179
|
|
|
@@ -189,7 +189,7 @@ vpr build:css # regenerate the CSS manually
|
|
|
189
189
|
| `src/pages/index.astro` | Home — prerendered from the `home` page entry |
|
|
190
190
|
| `src/pages/[...slug].astro` | Prerendered once per published `pages` entry (`getStaticPaths`) |
|
|
191
191
|
| `src/pages/404.astro` | Prerendered not-found page, served with a 404 status for any unmatched URL |
|
|
192
|
-
| `public/_headers` | Report-Only CSP for the static pages (see
|
|
192
|
+
| `public/_headers` | Report-Only CSP for the static pages (see `.agents/skills/csp`) |
|
|
193
193
|
| `scripts/pull-content.mjs` | Loads production's D1 into the local build DB before the prerender (`deploy`/`deploy:dev`) |
|
|
194
194
|
| `wrangler.jsonc` | Cloudflare bindings (D1/R2) + cron trigger |
|
|
195
195
|
| `scripts/build-dev-wrangler.mjs` | Rewrites the built wrangler config for the isolated dev worker |
|
|
@@ -18,8 +18,9 @@ worker-configuration.d.ts
|
|
|
18
18
|
# node hosting target: SQLite db + local uploads
|
|
19
19
|
data/
|
|
20
20
|
|
|
21
|
-
# generated by the vitops() integration on dev/build
|
|
22
|
-
src/styles/
|
|
21
|
+
# generated by the vitops() integration on dev/build (site CSS goes in src/site.css, not here)
|
|
22
|
+
src/styles/tailwind.css
|
|
23
|
+
src/styles/tokens.json
|
|
23
24
|
public/vitops/
|
|
24
25
|
|
|
25
26
|
# generated by `pnpm run sop` — styles SETUP.html for a file:// open
|
|
@@ -32,3 +33,13 @@ public/vitops/
|
|
|
32
33
|
|
|
33
34
|
# os / editor
|
|
34
35
|
.DS_Store
|
|
36
|
+
|
|
37
|
+
# agent skills linked from @getvitops/skills by `skills experimental_sync` (see skills-lock.json)
|
|
38
|
+
.agents/skills/custom-blocks
|
|
39
|
+
.agents/skills/content-and-deploys
|
|
40
|
+
.agents/skills/contact-form
|
|
41
|
+
.agents/skills/csp
|
|
42
|
+
.claude/skills/custom-blocks
|
|
43
|
+
.claude/skills/content-and-deploys
|
|
44
|
+
.claude/skills/contact-form
|
|
45
|
+
.claude/skills/csp
|
|
@@ -35,6 +35,8 @@ const brandColor = colors.palette?.[roleHue('brand-primary')]?.seed ?? '#2e9b73'
|
|
|
35
35
|
export default defineConfig({
|
|
36
36
|
output: 'server',
|
|
37
37
|
adapter,
|
|
38
|
+
// The seed's `pages` collection serves entries at `/{slug}` (urlPattern), and `home` lives at `/`.
|
|
39
|
+
redirects: { '/home': '/' },
|
|
38
40
|
image: {
|
|
39
41
|
layout: 'constrained',
|
|
40
42
|
responsiveStyles: true,
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "astro dev",
|
|
8
|
-
"build": "
|
|
8
|
+
"build": "vitops emdash build",
|
|
9
9
|
"preview": "astro preview",
|
|
10
10
|
"pull-content": "node scripts/pull-content.mjs",
|
|
11
11
|
"prepare-local-db": "vitops emdash prepare-db",
|
|
12
|
-
"deploy": "vitops email check && node scripts/pull-content.mjs && vitops emdash prepare-db &&
|
|
13
|
-
"deploy:dev": "vitops email check && node scripts/pull-content.mjs && vitops emdash prepare-db &&
|
|
12
|
+
"deploy": "vitops email check && node scripts/pull-content.mjs && vitops emdash prepare-db && vitops emdash build && emdash migrate --wrangler-config wrangler.jsonc && wrangler deploy",
|
|
13
|
+
"deploy:dev": "vitops email check && node scripts/pull-content.mjs && vitops emdash prepare-db && vitops emdash build && node scripts/build-dev-wrangler.mjs && emdash migrate --wrangler-config dist/server/wrangler.dev.json && wrangler deploy --config dist/server/wrangler.dev.json",
|
|
14
14
|
"init:github": "vitops setup repo",
|
|
15
15
|
"init:hosting": "vitops setup hosting",
|
|
16
16
|
"setup": "vitops setup all",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"build:css": "vitops generate -i site.json -f tailwind -o src/styles",
|
|
19
19
|
"lint:design": "vitops lint -i site.json -f tailwind --strict",
|
|
20
20
|
"sop": "vitops generate -i site.json -f css -o .vitops/sop",
|
|
21
|
-
"prepare": "vp config"
|
|
21
|
+
"prepare": "vp config && skills experimental_sync -y -a claude-code --no-frozen-lockfile && vitops agents sync -i site.json"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@astrojs/cloudflare": "^14.0.0",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"@getvitops/astro": "latest",
|
|
28
28
|
"@getvitops/core": "latest",
|
|
29
29
|
"@getvitops/emdash": "latest",
|
|
30
|
+
"@getvitops/skills": "latest",
|
|
30
31
|
"@tailwindcss/vite": "^4.3.3",
|
|
31
32
|
"astro": "^7.4.4",
|
|
32
33
|
"emdash": "latest",
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
"@cloudflare/workers-types": "^4.20260305.1",
|
|
40
41
|
"@getvitops/cli": "latest",
|
|
41
42
|
"@iconify-json/fa7-solid": "^1.2.5",
|
|
43
|
+
"skills": "^1.7.6",
|
|
42
44
|
"typescript": "^6.0.3",
|
|
43
45
|
"vite": "latest",
|
|
44
46
|
"vite-plus": "latest",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Cloudflare static-assets response headers. They apply to files served from the
|
|
2
2
|
# static asset store — the prerendered pages — and NOT to responses the Worker
|
|
3
3
|
# renders itself (the EmDash admin at /_emdash/*, its API, anything left
|
|
4
|
-
# on-demand). See
|
|
4
|
+
# on-demand). See .agents/skills/csp.
|
|
5
5
|
#
|
|
6
6
|
# REPORT-ONLY on purpose: violations are logged to the browser console and
|
|
7
7
|
# nothing is blocked, so this cannot break a page. Read the console on the
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// `astro build`. The Cloudflare adapter prerenders inside a local workerd with a
|
|
7
7
|
// LOCAL D1 (`.wrangler/state`), never the deployed database — so on a CI runner
|
|
8
8
|
// the build would see an empty, auto-seeded database and bake "No home page yet"
|
|
9
|
-
// into every page. Production is the truth for content (see
|
|
9
|
+
// into every page. Production is the truth for content (see .agents/skills/content-and-deploys), so this
|
|
10
10
|
// copies production's D1 into the local one first.
|
|
11
11
|
//
|
|
12
12
|
// wrangler d1 export <db> --remote → .vitops/content.sql
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"slug": "pages",
|
|
16
16
|
"label": "Pages",
|
|
17
17
|
"labelSingular": "Page",
|
|
18
|
+
"urlPattern": "/{slug}",
|
|
18
19
|
"supports": ["drafts", "revisions", "seo"],
|
|
19
20
|
"fields": [
|
|
20
21
|
{
|
|
@@ -112,7 +113,7 @@
|
|
|
112
113
|
]
|
|
113
114
|
},
|
|
114
115
|
{
|
|
115
|
-
"_type": "
|
|
116
|
+
"_type": "vitops_actionLink",
|
|
116
117
|
"_key": "pf-cta",
|
|
117
118
|
"label": "Contact us",
|
|
118
119
|
"href": "/contact",
|
|
@@ -161,10 +162,11 @@
|
|
|
161
162
|
]
|
|
162
163
|
},
|
|
163
164
|
{
|
|
164
|
-
"_type": "
|
|
165
|
+
"_type": "vitops_hero",
|
|
165
166
|
"_key": "demo-hero",
|
|
166
167
|
"eyebrow": "Vitops blocks",
|
|
167
168
|
"heading": "Build pages from design-system patterns",
|
|
169
|
+
"level": "2",
|
|
168
170
|
"body": "Every block below renders a framework pattern, so it follows your tokens and works without JavaScript.",
|
|
169
171
|
"primaryLabel": "Open the admin",
|
|
170
172
|
"primaryHref": "/_emdash/admin",
|
|
@@ -173,7 +175,7 @@
|
|
|
173
175
|
"layout": "centered"
|
|
174
176
|
},
|
|
175
177
|
{
|
|
176
|
-
"_type": "
|
|
178
|
+
"_type": "vitops_cards",
|
|
177
179
|
"_key": "demo-cards",
|
|
178
180
|
"columns": "3",
|
|
179
181
|
"cards": [
|
|
@@ -197,7 +199,7 @@
|
|
|
197
199
|
]
|
|
198
200
|
},
|
|
199
201
|
{
|
|
200
|
-
"_type": "
|
|
202
|
+
"_type": "vitops_accordion",
|
|
201
203
|
"_key": "demo-accordion",
|
|
202
204
|
"exclusive": true,
|
|
203
205
|
"items": [
|
|
@@ -215,7 +217,7 @@
|
|
|
215
217
|
]
|
|
216
218
|
},
|
|
217
219
|
{
|
|
218
|
-
"_type": "
|
|
220
|
+
"_type": "vitops_marquee",
|
|
219
221
|
"_key": "demo-marquee",
|
|
220
222
|
"pauseOnHover": true,
|
|
221
223
|
"reverse": false,
|
|
@@ -239,14 +241,14 @@
|
|
|
239
241
|
]
|
|
240
242
|
},
|
|
241
243
|
{
|
|
242
|
-
"_type": "
|
|
244
|
+
"_type": "vitops_banner",
|
|
243
245
|
"_key": "welcome-banner",
|
|
244
246
|
"message": "This banner is a Vitops block — dismiss it, then edit this page in the EmDash admin.",
|
|
245
247
|
"tone": "info",
|
|
246
248
|
"dismissible": true
|
|
247
249
|
},
|
|
248
250
|
{
|
|
249
|
-
"_type": "
|
|
251
|
+
"_type": "vitops_details",
|
|
250
252
|
"_key": "next-steps",
|
|
251
253
|
"summary": "Next steps",
|
|
252
254
|
"body": "Adjust site.json (under designSystem.themes.default) to your brand (colors, type, patterns), replace src/assets/logo.svg, then deploy with `pnpm deploy`.",
|
|
@@ -287,7 +289,7 @@
|
|
|
287
289
|
"title": "Terms & Conditions",
|
|
288
290
|
"content": [
|
|
289
291
|
{
|
|
290
|
-
"_type": "
|
|
292
|
+
"_type": "vitops_banner",
|
|
291
293
|
"_key": "terms-placeholder",
|
|
292
294
|
"message": "Placeholder text — replace with your own terms before launch. This is not legal advice.",
|
|
293
295
|
"tone": "warning",
|
|
@@ -388,7 +390,7 @@
|
|
|
388
390
|
"title": "Privacy Policy",
|
|
389
391
|
"content": [
|
|
390
392
|
{
|
|
391
|
-
"_type": "
|
|
393
|
+
"_type": "vitops_banner",
|
|
392
394
|
"_key": "privacy-placeholder",
|
|
393
395
|
"message": "Placeholder text — replace with your own privacy policy before launch. This is not legal advice.",
|
|
394
396
|
"tone": "warning",
|
|
@@ -503,7 +505,7 @@
|
|
|
503
505
|
"title": "Contact",
|
|
504
506
|
"content": [
|
|
505
507
|
{
|
|
506
|
-
"_type": "
|
|
508
|
+
"_type": "vitops_banner",
|
|
507
509
|
"_key": "contact-placeholder",
|
|
508
510
|
"message": "Placeholder text — replace with your contact details before launch.",
|
|
509
511
|
"tone": "warning",
|
|
@@ -524,7 +526,7 @@
|
|
|
524
526
|
]
|
|
525
527
|
},
|
|
526
528
|
{
|
|
527
|
-
"_type": "
|
|
529
|
+
"_type": "vitops_form",
|
|
528
530
|
"_key": "contact-form",
|
|
529
531
|
"heading": "Send us a message",
|
|
530
532
|
"level": "2",
|
|
@@ -568,7 +570,7 @@
|
|
|
568
570
|
"title": "Cookie Notice",
|
|
569
571
|
"content": [
|
|
570
572
|
{
|
|
571
|
-
"_type": "
|
|
573
|
+
"_type": "vitops_banner",
|
|
572
574
|
"_key": "cookies-placeholder",
|
|
573
575
|
"message": "Placeholder text — replace with your own cookie notice before launch. This is not legal advice. `vitops legal` can generate a draft from site.json.",
|
|
574
576
|
"tone": "warning",
|
|
@@ -309,6 +309,17 @@
|
|
|
309
309
|
},
|
|
310
310
|
"site": {
|
|
311
311
|
"defaultLocale": "en",
|
|
312
|
+
"templates": {
|
|
313
|
+
"contact": {
|
|
314
|
+
"type": "form",
|
|
315
|
+
"fields": [
|
|
316
|
+
{ "name": "name", "type": "text", "required": true },
|
|
317
|
+
{ "name": "email", "type": "email", "required": true },
|
|
318
|
+
{ "name": "tel", "type": "tel" },
|
|
319
|
+
{ "name": "message", "type": "textarea", "required": true }
|
|
320
|
+
]
|
|
321
|
+
}
|
|
322
|
+
},
|
|
312
323
|
"locales": {
|
|
313
324
|
"en": {
|
|
314
325
|
"name": "English"
|
|
@@ -8,7 +8,7 @@ import Layout from '../layouts/Layout.astro';
|
|
|
8
8
|
// the content database — `scripts/pull-content.mjs` loads production's into the
|
|
9
9
|
// local build database first (it is part of `deploy`/`deploy:dev`) — so an edit
|
|
10
10
|
// in the admin goes live on the next rebuild, which a publish webhook triggers
|
|
11
|
-
// (see
|
|
11
|
+
// (see .agents/skills/content-and-deploys).
|
|
12
12
|
//
|
|
13
13
|
// There is no `Astro.rewrite('/404')` here and no on-demand fallback: a URL that
|
|
14
14
|
// isn't in the list below simply has no route, so Astro serves the prerendered
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The site's one on-demand endpoint: contact-form posts (the `
|
|
2
|
+
* The site's one on-demand endpoint: contact-form posts (the `vitops_form` block) and the
|
|
3
3
|
* conversion beacons (`<Tracking />`, `tel:`/`mailto:` clicks) all arrive here.
|
|
4
4
|
*
|
|
5
|
-
* Every public page is prerendered (see
|
|
5
|
+
* Every public page is prerendered (see .agents/skills/content-and-deploys); this route sets
|
|
6
6
|
* `prerender = false` because it has to run on each request. It is the one exception, and the
|
|
7
7
|
* Cloudflare adapter handles it in the Worker beside the EmDash admin.
|
|
8
8
|
*
|
|
@@ -5,8 +5,8 @@ import Layout from '../layouts/Layout.astro';
|
|
|
5
5
|
|
|
6
6
|
// Prerendered, like every page in this template: written to static HTML at build
|
|
7
7
|
// time from the `home` entry (seed/seed.json → pages/home). Edit it in the admin;
|
|
8
|
-
// a publish triggers a rebuild that bakes the change in (
|
|
9
|
-
// freshness"). The Vitops blocks from @getvitops/emdash render automatically —
|
|
8
|
+
// a publish triggers a rebuild that bakes the change in (.agents/skills/content-and-deploys,
|
|
9
|
+
// "Rendering and freshness"). The Vitops blocks from @getvitops/emdash render automatically —
|
|
10
10
|
// EmDash merges the plugin's block components into <PortableText>.
|
|
11
11
|
//
|
|
12
12
|
// Anything on a page that must be fresh per request (a stock count, a signed-in
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
<!-- vitops:start -->
|
|
2
|
+
|
|
3
|
+
Built with the Vitops design system (`@getvitops/*`); tokens and site facts live in `site.json`.
|
|
4
|
+
Prefer its classes and patterns over hand-written CSS. Load the skill that matches the task:
|
|
5
|
+
|
|
6
|
+
- [contact-form](.agents/skills/contact-form/SKILL.md) — Use when adding or changing a form, fixing submissions that don't arrive, setting up noti…
|
|
7
|
+
- [content-and-deploys](.agents/skills/content-and-deploys/SKILL.md) — Use for any deploy or build problem, stale or missing content, "my publish isn't live", s…
|
|
8
|
+
- [csp](.agents/skills/csp/SKILL.md) — Use when changing `public/_headers`, adding an embed, tracking snippet or third-party scr…
|
|
9
|
+
- [custom-blocks](.agents/skills/custom-blocks/SKILL.md) — Use before creating, renaming or deleting any block type, any `src/components/blocks/*` r…
|
|
10
|
+
- [vitops-design-system](.agents/skills/vitops-design-system/SKILL.md) — Use when styling or authoring pages or components (choosing utility/pattern classes, colo…
|
|
11
|
+
|
|
12
|
+
Reference, rendered from this project: `vitops docs <topic>` (no topic lists them).
|
|
13
|
+
<!-- managed by `vitops agents sync` · @getvitops/cli@8.3.0-alpha.2 — edit outside this block -->
|
|
14
|
+
<!-- vitops:end -->
|
|
15
|
+
|
|
1
16
|
This is a static Astro website styled by the Vitops design system, deployed
|
|
2
17
|
as Cloudflare Workers static assets (no server runtime, no database).
|
|
3
18
|
|
|
@@ -14,8 +29,8 @@ as Cloudflare Workers static assets (no server runtime, no database).
|
|
|
14
29
|
portrait, landscape; SVG + PNG) for layout work before real imagery
|
|
15
30
|
exists. Delete the directory once real assets land.
|
|
16
31
|
- Prefer framework CSS classes (utilities, patterns, type roles like
|
|
17
|
-
`font-heading`) over bespoke CSS.
|
|
18
|
-
full
|
|
32
|
+
`font-heading`) over bespoke CSS. `vp exec vitops docs classes` prints the
|
|
33
|
+
full class vocabulary.
|
|
19
34
|
- **Layout foundations — these are the answer, not a starting point.** Six
|
|
20
35
|
substitutions carry the structure of nearly every page:
|
|
21
36
|
| About to write… | Write instead |
|
|
@@ -70,6 +85,13 @@ activates to `script-src` / `connect-src`, any embed to `frame-src`, and hash an
|
|
|
70
85
|
inline script you add yourself. Hashes rather than nonces because a static page has no
|
|
71
86
|
per-request response to carry one.
|
|
72
87
|
|
|
88
|
+
## Skills
|
|
89
|
+
|
|
90
|
+
`@getvitops/skills` ships agent skills that update with the package. `skills experimental_sync` (run
|
|
91
|
+
by `prepare`, so by `vp install`) links them into `.agents/skills/` and `.claude/skills/`; they are
|
|
92
|
+
gitignored. The set is written for EmDash sites, so `csp` is the one that applies here. A skill you
|
|
93
|
+
write yourself with the same name is never replaced. `setup` is this site's own.
|
|
94
|
+
|
|
73
95
|
## Commands
|
|
74
96
|
|
|
75
97
|
This is a **Vite+** project: use `vp` (and its `vpr` shorthand), not `pnpm`/`npm`/`npx`.
|
|
@@ -25,3 +25,13 @@ public/vitops/
|
|
|
25
25
|
|
|
26
26
|
# os / editor
|
|
27
27
|
.DS_Store
|
|
28
|
+
|
|
29
|
+
# agent skills linked from @getvitops/skills by `skills experimental_sync` (see skills-lock.json)
|
|
30
|
+
.agents/skills/custom-blocks
|
|
31
|
+
.agents/skills/content-and-deploys
|
|
32
|
+
.agents/skills/contact-form
|
|
33
|
+
.agents/skills/csp
|
|
34
|
+
.claude/skills/custom-blocks
|
|
35
|
+
.claude/skills/content-and-deploys
|
|
36
|
+
.claude/skills/contact-form
|
|
37
|
+
.claude/skills/csp
|
|
@@ -16,15 +16,17 @@
|
|
|
16
16
|
"build:css": "vitops generate -i site.json -f css -o src/styles",
|
|
17
17
|
"lint:design": "vitops lint -i site.json -f css --strict",
|
|
18
18
|
"sop": "vitops generate -i site.json -f css -o .vitops/sop",
|
|
19
|
-
"prepare": "vp config"
|
|
19
|
+
"prepare": "vp config && skills experimental_sync -y -a claude-code --no-frozen-lockfile && vitops agents sync -i site.json"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@getvitops/astro": "latest",
|
|
23
|
+
"@getvitops/skills": "latest",
|
|
23
24
|
"astro": "^7.4.4"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"@astrojs/check": "^0.9.12",
|
|
27
28
|
"@getvitops/cli": "latest",
|
|
29
|
+
"skills": "^1.7.6",
|
|
28
30
|
"typescript": "^6.0.3",
|
|
29
31
|
"vite": "latest",
|
|
30
32
|
"vite-plus": "latest",
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: custom-blocks
|
|
3
|
-
description: Decide whether a page section needs a new EmDash block type, and if so how to name, build, version and retire it. Use before creating, renaming or deleting any block type, any `src/components/blocks/*` renderer, or when asked to "add a block", "make an editable section" or "add a component the editors can place".
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Custom blocks
|
|
7
|
-
|
|
8
|
-
Pages are composed from **blocks** so non-technical editors can rearrange and edit
|
|
9
|
-
them in the CMS. A new block type is the most expensive way to get a section onto a
|
|
10
|
-
page: it adds a content-model entry, a renderer, a field schema and a migration
|
|
11
|
-
burden, and **a block type cannot be renamed or deleted** once pages use it (see
|
|
12
|
-
"Lifecycle"). So work down this list and stop at the first rung that fits.
|
|
13
|
-
|
|
14
|
-
## 1. Decision rule
|
|
15
|
-
|
|
16
|
-
1. **Compose what exists.** The `vitops.*` blocks from `@getvitops/emdash` (cards,
|
|
17
|
-
carousel, gallery, accordion, details, banner, hero, marquee, image-compare,
|
|
18
|
-
copy button, action link, …) resolve inside any block type's `portableText` field,
|
|
19
|
-
so "a section with a heading, intro and then a row of cards" is a section block
|
|
20
|
-
whose body holds `vitops.cards` — not a new "cards section" type. Check what is
|
|
21
|
-
installed first: `vp exec vitops docs components` lists every block the toolchain
|
|
22
|
-
ships and which tier provides it.
|
|
23
|
-
2. **Extend an existing block** if one is close. An extra option (a `variant`
|
|
24
|
-
select, an optional slot) on a block that already exists is a version bump on that
|
|
25
|
-
block, not a new type. Prefer this when the new thing is a variation of the same
|
|
26
|
-
content shape.
|
|
27
|
-
3. **Site-local block type** only when the content shape is specific to this site
|
|
28
|
-
(a bespoke dashboard mock, an A-versus-B argument with hard-coded figures). Keep
|
|
29
|
-
the renderer in `src/components/blocks/` and the type name prefixed for the site
|
|
30
|
-
(see "Naming"). Code-owned widgets expose only a caption field; the numbers live in
|
|
31
|
-
code, not in the CMS.
|
|
32
|
-
4. **Upstream it** when a second site would plausibly want it. Don't build a
|
|
33
|
-
generic block here and quietly fork it: raise it against the toolchain
|
|
34
|
-
(`getvitops/design-system`) with the shape you used, so every site gets it. Until
|
|
35
|
-
it ships, keep the local version and mark it `local (watch)` in a comment, so it
|
|
36
|
-
is easy to retire when the real one lands.
|
|
37
|
-
|
|
38
|
-
A form, a section container, a stats row, a logo grid, a CTA band and a steps list
|
|
39
|
-
are all _generic_ — expect to find them upstream or to upstream them, not to keep
|
|
40
|
-
them local.
|
|
41
|
-
|
|
42
|
-
## 2. Naming: check the framework's vocabulary first
|
|
43
|
-
|
|
44
|
-
Block types are permanent, so the name has to be right the first time. Before you
|
|
45
|
-
name one:
|
|
46
|
-
|
|
47
|
-
- Run `vp exec vitops docs components` and `vp exec vitops docs classes`. If the
|
|
48
|
-
framework already has a pattern by that name, **use its meaning**: `badge` is a
|
|
49
|
-
_static_ label; `tag` is a label that may be static or dynamic (editable,
|
|
50
|
-
dismissable). Never `chip` or `pill`. A block called `chips` that renders badges
|
|
51
|
-
cost one site a new type, a content migration and a permanently retired type.
|
|
52
|
-
- Prefix site-local types with a short site code (`acme_section`), never `vitops.`
|
|
53
|
-
— that namespace belongs to the toolchain.
|
|
54
|
-
- Name the thing, not its look: `section`, `steps`, `logos`; not `green-box`.
|
|
55
|
-
|
|
56
|
-
## 3. Field and renderer conventions
|
|
57
|
-
|
|
58
|
-
- **Colour: `tone` plus optional `color`.** `tone` takes a **role** name from
|
|
59
|
-
`site.json`'s `colors.roles` (preferred), falling back to a semantic name
|
|
60
|
-
(`info` `success` `warning` `danger`); `color` is the escape hatch for a palette
|
|
61
|
-
hue/step and should be rare — a site that sets it on every item is telling you a
|
|
62
|
-
role is missing. A select field can only list static options, so either list the
|
|
63
|
-
roles you actually use or take a validated string. Resolve it in one shared helper
|
|
64
|
-
(`src/components/blocks/tone.ts`) rather than per block.
|
|
65
|
-
- **Class names cannot be built from field values.** Tailwind only emits classes it
|
|
66
|
-
can read in source, so `` `bg-${tone}` `` produces nothing. Resolve a tone to CSS
|
|
67
|
-
custom properties (or a lookup table of whole class names) instead.
|
|
68
|
-
- **Links are `url` fields**, not `string`: EmDash's `url` type accepts site-relative
|
|
69
|
-
paths. (`string` is a mistake that needs a breaking version to undo.)
|
|
70
|
-
- **Repeater sub-fields are scalar-only** (no `portableText`, no links inside text),
|
|
71
|
-
so a checklist is one newline-separated `text` field. Don't fight it; rich item
|
|
72
|
-
bodies need EmDash to lift that limit.
|
|
73
|
-
- **Blocks don't nest.** A "section containing stats" is the section's own body
|
|
74
|
-
holding `vitops.*` blocks, or a flat sequence of sibling blocks; per-section
|
|
75
|
-
backdrops don't carry across siblings.
|
|
76
|
-
- **Reach for framework classes** (`centered`, `region`, `rhythm`, `subgrid`,
|
|
77
|
-
`cluster`, `font-*`, `card`, `badge`, `tag`) in the renderer; run
|
|
78
|
-
`vpr lint:design` — hand-written layout CSS in a block is the signal that the
|
|
79
|
-
framework is missing a pattern worth upstreaming.
|
|
80
|
-
- A card title is a heading: render a real `<h2>`–`<h4>` with a level prop, not a
|
|
81
|
-
styled `<p>`.
|
|
82
|
-
|
|
83
|
-
## 4. Lifecycle
|
|
84
|
-
|
|
85
|
-
- **Create** with the schema tools (the `schema_*` actions on the site's MCP server,
|
|
86
|
-
`/_emdash/api/mcp`) or the admin; commit the matching renderer and the
|
|
87
|
-
`emdash export-seed` output so a fresh environment gets the same model.
|
|
88
|
-
- **A breaking change is a new version**, not an edit: add the version, migrate the
|
|
89
|
-
entries that use the old one, then activate it. Two things the tooling does not say:
|
|
90
|
-
`schema_activate_block_type_version`'s `expectedFingerprint` is the fingerprint of
|
|
91
|
-
the **currently active** version, not the one you are activating (a mismatch is a
|
|
92
|
-
`CONFLICT`), and `schema_update_block_type` **replaces the whole `fields` list**, so
|
|
93
|
-
send every field, not just the changed one.
|
|
94
|
-
- **Migrating content:** `emdash content update` has no `--migrate-blocks` flag; the
|
|
95
|
-
REST route `PUT /_emdash/api/content/<collection>/<id>` with
|
|
96
|
-
`{ data, _rev, migrateBlocks: true }` does (it takes Portable Text arrays; the CLI
|
|
97
|
-
and MCP take Markdown). Updating a published entry stages a **draft** — a separate
|
|
98
|
-
`content publish` is needed before the live page changes, and (because pages are
|
|
99
|
-
prerendered) a rebuild after that.
|
|
100
|
-
- **Retiring is not deleting.** A type that was renamed or superseded stays in the
|
|
101
|
-
database and the seed permanently; keep its renderer mapping old values so existing
|
|
102
|
-
content still renders, mark it retired, and stop offering it to editors.
|
|
103
|
-
- **Typed fields:** `emdash types` does not type `blocks` fields (they come out as
|
|
104
|
-
`unknown[]`); hand-write a value type per renderer until EmDash generates it.
|