@getvitops/skills 0.0.0-stage → 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +41 -2
- package/package.json +20 -3
- package/skills/contact-form/SKILL.md +35 -0
- package/skills/content-and-deploys/SKILL.md +103 -0
- package/skills/csp/SKILL.md +35 -0
- package/skills/custom-blocks/SKILL.md +106 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# @getvitops/skills
|
|
2
|
+
|
|
3
|
+
## 0.1.0-alpha.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 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).
|
|
8
|
+
|
|
9
|
+
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`.
|
package/README.md
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @getvitops/skills
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Agent skills for sites built with the Vitops toolchain. A skill is a `skills/<name>/SKILL.md` an
|
|
4
|
+
agent loads when its `description` matches the task. They are knowledge about the toolkit, so they
|
|
5
|
+
ship here and update with the package instead of being copied into each site at scaffold time.
|
|
6
|
+
|
|
7
|
+
## Use
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
vp add @getvitops/skills
|
|
11
|
+
vp add -D skills
|
|
12
|
+
vp exec skills experimental_sync -y
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`skills experimental_sync` links each skill into `.agents/skills/` (and `.claude/skills/` when
|
|
16
|
+
Claude Code is selected), records them in `skills-lock.json`, and relinks on every run, so
|
|
17
|
+
`vp update` refreshes them. Add it to `prepare` to run it after every install:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"scripts": {
|
|
22
|
+
"prepare": "vp config && skills experimental_sync -y -a claude-code --no-frozen-lockfile"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`--no-frozen-lockfile` matters on CI: `skills` defaults to a frozen lockfile there, so without it a fresh clone, or any `@getvitops/skills` update, fails the install until `skills-lock.json` is regenerated.
|
|
28
|
+
|
|
29
|
+
`experimental_sync` is a preview command and may change; pin the `skills` version.
|
|
30
|
+
|
|
31
|
+
A skill the site installed or wrote itself is never replaced, so a site can override one by name.
|
|
32
|
+
|
|
33
|
+
## Skills
|
|
34
|
+
|
|
35
|
+
| Skill | Use it for |
|
|
36
|
+
| --------------------- | ------------------------------------------------------------------------------ |
|
|
37
|
+
| `custom-blocks` | Deciding whether a page section needs a new EmDash block type, and building it |
|
|
38
|
+
| `content-and-deploys` | Where content vs code is the truth, prerendering, publish-triggered rebuilds |
|
|
39
|
+
| `contact-form` | The `/api/track` route, no-JS behaviour, spam protection, privacy inventory |
|
|
40
|
+
| `csp` | The Report-Only CSP and what to check before enforcing it |
|
|
41
|
+
|
|
42
|
+
The `vitops-design-system` skill still ships with `@getvitops/cli` and is linked by `vitops agents`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getvitops/skills",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Agent skills for sites built with the Vitops toolchain. Installed as a dependency and linked into a project with `skills experimental_sync`, so they update with the package.",
|
|
5
|
+
"homepage": "https://docs.vitops.ca",
|
|
6
|
+
"bugs": "https://github.com/getvitops/design-system/issues",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/getvitops/design-system.git",
|
|
11
|
+
"directory": "packages/skills"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"skills",
|
|
15
|
+
"CHANGELOG.md"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=24"
|
|
22
|
+
}
|
|
6
23
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: contact-form
|
|
3
|
+
description: How the contact form works on this site — the on-demand `/api/track` route, no-JS behaviour, where submissions go, spam protection and Turnstile, and the privacy-policy inventory. Use when adding or changing a form, fixing submissions that don't arrive, setting up notifications, CRM or Turnstile, or touching `src/pages/api/track.ts`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Contact form
|
|
7
|
+
|
|
8
|
+
The `vitops_form` block posts a plain `<form method="post">` to `src/pages/api/track.ts`
|
|
9
|
+
(`prerender = false`, built with `createConversionRoute()`). It is the one on-demand public route,
|
|
10
|
+
and it also answers the conversion beacons.
|
|
11
|
+
|
|
12
|
+
- **No JavaScript needed.** The route answers a form post with a 303 back to the page plus
|
|
13
|
+
`?sent=1#form-sent` or `?error=1#form-error`, and the block's two notices are revealed by that
|
|
14
|
+
fragment. Keep one form per page.
|
|
15
|
+
- **Where submissions go:** `site.notifications.email` (or the primary location's `email`) and
|
|
16
|
+
`site.crm` when set. **With neither, the route refuses the post** rather than accept a message
|
|
17
|
+
nobody receives, so set `site.notifications.email` before launch and run `vitops email setup` for
|
|
18
|
+
its sender domain. A failed CRM write never fails the request; it is stated in the notification
|
|
19
|
+
email, which is the durability floor.
|
|
20
|
+
- **Spam protection:** a honeypot field, the route's origin / size / per-IP rate checks, and
|
|
21
|
+
Cloudflare Turnstile once `vitops setup turnstile` has put the public key in
|
|
22
|
+
`site.security.turnstile.siteKey` and saved the Worker secret `VITOPS_TURNSTILE_SECRET` (never in
|
|
23
|
+
`site.json`). With a key set the route fails closed if the secret is missing. `TURNSTILE_REQUIRED`
|
|
24
|
+
in the route (default `true`) makes a token-less post a refusal, which means the form needs
|
|
25
|
+
JavaScript; set it `false` to keep a no-JS submit working and accept that a bot can then skip the
|
|
26
|
+
token. Turnstile is category `necessary` (it protects the form being sent), loads only when a
|
|
27
|
+
visitor first uses the form, and its origin is allowed in the Report-Only CSP in
|
|
28
|
+
`public/_headers`.
|
|
29
|
+
- **Privacy policy inventory.** `vitops legal` derives "what personal information we collect" from
|
|
30
|
+
`site.templates` entries of type `form`, not from the editor's block, so a form added in the admin
|
|
31
|
+
is invisible to it. When you add or change form fields, mirror them in a `site.templates` form
|
|
32
|
+
entry in `site.json`, or the generated policy under-reports.
|
|
33
|
+
- A field named `phone` submits as `tel` (the route reserves `phone` and `event`).
|
|
34
|
+
- The per-IP rate limit is per Worker isolate; add a KV-backed `RateLimitStore` if you need a
|
|
35
|
+
fleet-wide one.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: content-and-deploys
|
|
3
|
+
description: How content, builds and deploys fit together on this EmDash site — where each kind of data is the source of truth, why pages are prerendered, how a publish reaches the live site, and refreshing the dev database. Use for any deploy or build problem, stale or missing content, "my publish isn't live", scheduled posts, previews, 404s, server islands, or refreshing dev from prod.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Content and deploys
|
|
7
|
+
|
|
8
|
+
## Where the truth lives
|
|
9
|
+
|
|
10
|
+
Truth runs a different direction per kind of data.
|
|
11
|
+
|
|
12
|
+
| Data | Truth | Moves by |
|
|
13
|
+
| ----------------------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------- |
|
|
14
|
+
| Code, Astro pages, `site.json`, design system, seed, schema | git (`dev`) | promote `dev → main` |
|
|
15
|
+
| Content: entries, media, menus, settings | **production** D1/R2 | editors work there; drafts and "Publish changes" are the staging step |
|
|
16
|
+
| Dev database | disposable copy of production | `scripts/refresh-dev.mjs` / the `site-refresh-dev` workflow, one way only |
|
|
17
|
+
|
|
18
|
+
The dev site exists to rehearse code and schema changes against real content. Never promote it
|
|
19
|
+
back. The refresh refuses any target not named `*-dev`, and drops every table in its target. It
|
|
20
|
+
copies no sessions (they are in KV). Set `EMDASH_PREVIEW_SECRET` on the dev worker, or prod preview
|
|
21
|
+
links would open drafts there. The dev worker's cron trigger is dropped, so scheduled publishing
|
|
22
|
+
can't be exercised on dev.
|
|
23
|
+
|
|
24
|
+
## Rendering and freshness: everything is prerendered
|
|
25
|
+
|
|
26
|
+
`output: 'server'` is still required (the admin, media/API routes and scheduled publishing need a
|
|
27
|
+
Worker), but every public page — `index.astro`, `[...slug].astro`, `404.astro` — has
|
|
28
|
+
`export const prerender = true`, and `[...slug].astro` lists the published `pages` entries in
|
|
29
|
+
`getStaticPaths()`. The HTML is written at build time and served as a static asset; the Worker only
|
|
30
|
+
handles `/_emdash/*` and anything deliberately left on-demand. A route with no `prerender` export is
|
|
31
|
+
re-rendered on every request, so don't leave it off by accident.
|
|
32
|
+
|
|
33
|
+
- **Where build-time content comes from.** The Cloudflare adapter prerenders against a _local_ D1
|
|
34
|
+
(`.wrangler/state`), never the deployed one — on a CI runner that is an empty database, which
|
|
35
|
+
would bake "No home page yet" into every page. `scripts/pull-content.mjs` (the first step of
|
|
36
|
+
`vpr deploy` and `vpr deploy:dev`) exports production's D1 and loads it locally first. It fails
|
|
37
|
+
closed: a failed export aborts the deploy rather than publishing an empty site (the one exception
|
|
38
|
+
is a database that doesn't exist yet, the first deploy, which builds from the seed).
|
|
39
|
+
`SKIP_PULL_CONTENT=1` skips it.
|
|
40
|
+
- **The local database must be migrated before the build.** The template runs
|
|
41
|
+
`migrations: { runtime: 'check' }`, so a build against a local database with pending migrations
|
|
42
|
+
prerenders EmDash's one-line "Database migrations are required" response into every page — and
|
|
43
|
+
still succeeds. A fresh runner, a first deploy, a `SKIP_PULL_CONTENT=1` build and an **upgrade**
|
|
44
|
+
deploy (the pull restores production's older schema, and `emdash migrate` only runs after the
|
|
45
|
+
build) would all publish that. `vitops emdash prepare-db` (`vpr prepare-local-db`, run after the
|
|
46
|
+
pull by `vpr deploy`, `vpr deploy:dev` and the reusable workflow) starts `astro dev` once, calls
|
|
47
|
+
EmDash's `dev-bypass` setup (which migrates and applies the seed's sample content) and stops it;
|
|
48
|
+
`vitops emdash check-prerender` (part of `vpr build`) then fails the build if any prerendered page
|
|
49
|
+
is not a real HTML page with a title. Run `vpr prepare-local-db` by hand before a first local
|
|
50
|
+
`vpr build`. It only writes the disposable `.wrangler/state`; the dev administrator it creates is
|
|
51
|
+
never deployed.
|
|
52
|
+
- **Freshness is a rebuild.** A page reflects production as of the last build, so a publish in the
|
|
53
|
+
admin has to trigger one (see "Publish-triggered rebuilds").
|
|
54
|
+
- **Scheduled publishes.** The Worker cron trigger in `wrangler.jsonc` (every minute) is what makes
|
|
55
|
+
content scheduled in the admin publish, and what runs plugin cron and maintenance. Keep it. A
|
|
56
|
+
scheduled publish changes the database but not the live HTML until a rebuild; whether it fires
|
|
57
|
+
the publish hook below is unverified, so check before relying on it.
|
|
58
|
+
- **Drafts and previews.** A prerendered page cannot show an unpublished draft, so an admin
|
|
59
|
+
"Preview" link shows the last published build. If previews matter, add a dedicated on-demand
|
|
60
|
+
route for them rather than removing `prerender` from the public ones.
|
|
61
|
+
- **Dynamic parts are server islands.** Anything that must be fresh per request goes in its own
|
|
62
|
+
component rendered with `server:defer` inside the prerendered page; the surrounding HTML stays
|
|
63
|
+
static. Keep islands rare — each is a Worker request after load.
|
|
64
|
+
- **Unknown URLs.** There is no `Astro.rewrite('/404')`: a slug not listed by `getStaticPaths()` has
|
|
65
|
+
no route, so Astro serves the prerendered `404.html` with a real 404 status. Rewriting an
|
|
66
|
+
on-demand route to a prerendered one fails (500), so don't reintroduce it.
|
|
67
|
+
|
|
68
|
+
## Publish-triggered rebuilds
|
|
69
|
+
|
|
70
|
+
`.github/workflows/deploy-prod.yml` also runs on a `repository_dispatch` event of type
|
|
71
|
+
`emdash-publish` (it pins `ref` to `main`, because a dispatch runs from the default branch, `dev`).
|
|
72
|
+
Wire an EmDash plugin hook to send it:
|
|
73
|
+
|
|
74
|
+
```jsonc
|
|
75
|
+
// emdash-plugin.jsonc
|
|
76
|
+
"capabilities": ["content:read", "network:request"],
|
|
77
|
+
"allowedHosts": ["api.github.com"]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// src/plugin.ts
|
|
82
|
+
hooks: {
|
|
83
|
+
'content:afterPublish': async (_event, ctx) => {
|
|
84
|
+
const token = await ctx.settings.get<string>('githubToken'); // an encrypted plugin setting
|
|
85
|
+
if (!token || !ctx.http) return;
|
|
86
|
+
await ctx.http.fetch(`https://api.github.com/repos/${OWNER}/${REPO}/dispatches`, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: { authorization: `Bearer ${token}`, accept: 'application/vnd.github+json' },
|
|
89
|
+
body: JSON.stringify({ event_type: 'emdash-publish' }),
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The token is a fine-grained GitHub token with `Contents: write` on this repo only, stored as the
|
|
96
|
+
plugin's encrypted setting (a sandboxed plugin can't read Worker environment variables), never in
|
|
97
|
+
the repo. The hook name, capabilities and `ctx.http` / `ctx.settings` calls follow EmDash's plugin
|
|
98
|
+
reference but this exact plugin has not been run against a live site, so treat it as a starting
|
|
99
|
+
point. Two costs: the run goes through the `production` environment, so it **waits for the
|
|
100
|
+
reviewers' approval** like any prod deploy (relax that rule if editors' publishes should go live
|
|
101
|
+
unattended), and bursts of publishes queue behind the `deploy-prod` concurrency group rather than
|
|
102
|
+
cancelling each other. Without the hook, a manual run of **Deploy prod** (or a push to `main`) does
|
|
103
|
+
the same job.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: csp
|
|
3
|
+
description: The Content Security Policy this site ships (Report-Only) and what to check before enforcing it. Use when changing `public/_headers`, adding an embed, tracking snippet or third-party script, turning the CSP on, or seeing CSP reports in the console.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Content Security Policy
|
|
7
|
+
|
|
8
|
+
`public/_headers` ships a **`Content-Security-Policy-Report-Only`** header: it logs violations to
|
|
9
|
+
the browser console and blocks nothing, so it can't break a page. Its `script-src` is `'self'` plus
|
|
10
|
+
the SHA-256 of each inline script `<Head />` emits (the colour-scheme bootstrap and the
|
|
11
|
+
consent-gate stub) — no `'unsafe-inline'`. Hashes, not nonces, because prerendered pages have no
|
|
12
|
+
per-request response to put a nonce in. `contentSecurityPolicy()` and `inlineScriptHashes()` from
|
|
13
|
+
`@getvitops/astro` produce the same strings (`templates.test.ts` in the toolchain keeps the file in
|
|
14
|
+
step).
|
|
15
|
+
|
|
16
|
+
Know what it does not cover before you flip it to enforcing (rename the header to
|
|
17
|
+
`Content-Security-Policy`):
|
|
18
|
+
|
|
19
|
+
- **`_headers` applies to static assets only.** Worker-rendered responses — the EmDash admin
|
|
20
|
+
(`/_emdash/*`), its API, anything left on-demand — don't get it. Setting a header there means
|
|
21
|
+
doing it in code (e.g. Astro middleware calling `contentSecurityPolicy()`), and the admin's own
|
|
22
|
+
inline scripts and editor HTML would need allowing; leave the admin out of an enforced policy.
|
|
23
|
+
- **Editor-authored HTML blocks are the hard limit.** Any block that lets an editor paste raw HTML
|
|
24
|
+
or a `<script>` (an embed, a tracking snippet) puts arbitrary inline script on the page, which a
|
|
25
|
+
hash policy cannot name ahead of time. HTML blocks created in the admin also run JavaScript in a
|
|
26
|
+
sandboxed iframe, and Astro's `security.csp` blocks their styles and scripts, so don't turn that
|
|
27
|
+
on without allowing for it. Enforcing a strict policy means removing such blocks or moving embeds
|
|
28
|
+
to a consent-gated `data-consent` tag whose origin you allow explicitly.
|
|
29
|
+
- **Third-party tags** the consent gate activates need their origins in `script-src` /
|
|
30
|
+
`connect-src`; anything with `frame` needs `frame-src`.
|
|
31
|
+
- **A server island** (`server:defer`) adds an inline loader script and a request to the Worker;
|
|
32
|
+
expect a report for the loader until you hash it.
|
|
33
|
+
|
|
34
|
+
Run the site, browse the pages you ship with the console open, fix or allow what is reported, and
|
|
35
|
+
only then enforce.
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
- **Nesting needs a `portableText` field.** A `vitops_*` Portable Text block holds
|
|
74
|
+
only scalars and repeaters, so it can't contain blocks. A block _type_ with a
|
|
75
|
+
`portableText` field can (`vitops_section`'s `body` holds any blocks), so "a
|
|
76
|
+
section containing stats" is a section whose body holds `vitops_stats`. A flat
|
|
77
|
+
sequence of siblings works too, but per-section backdrops don't carry across them.
|
|
78
|
+
- **Reach for framework classes** (`centered`, `region`, `rhythm`, `subgrid`,
|
|
79
|
+
`cluster`, `font-*`, `card`, `badge`, `tag`) in the renderer; run
|
|
80
|
+
`vpr lint:design` — hand-written layout CSS in a block is the signal that the
|
|
81
|
+
framework is missing a pattern worth upstreaming.
|
|
82
|
+
- A card title is a heading: render a real `<h2>`–`<h4>` with a level prop, not a
|
|
83
|
+
styled `<p>`.
|
|
84
|
+
|
|
85
|
+
## 4. Lifecycle
|
|
86
|
+
|
|
87
|
+
- **Create** with the schema tools (the `schema_*` actions on the site's MCP server,
|
|
88
|
+
`/_emdash/api/mcp`) or the admin; commit the matching renderer and the
|
|
89
|
+
`emdash export-seed` output so a fresh environment gets the same model.
|
|
90
|
+
- **A breaking change is a new version**, not an edit: add the version, migrate the
|
|
91
|
+
entries that use the old one, then activate it. Two things the tooling does not say:
|
|
92
|
+
`schema_activate_block_type_version`'s `expectedFingerprint` is the fingerprint of
|
|
93
|
+
the **currently active** version, not the one you are activating (a mismatch is a
|
|
94
|
+
`CONFLICT`), and `schema_update_block_type` **replaces the whole `fields` list**, so
|
|
95
|
+
send every field, not just the changed one.
|
|
96
|
+
- **Migrating content:** `emdash content update` has no `--migrate-blocks` flag; the
|
|
97
|
+
REST route `PUT /_emdash/api/content/<collection>/<id>` with
|
|
98
|
+
`{ data, _rev, migrateBlocks: true }` does (it takes Portable Text arrays; the CLI
|
|
99
|
+
and MCP take Markdown). Updating a published entry stages a **draft** — a separate
|
|
100
|
+
`content publish` is needed before the live page changes, and (because pages are
|
|
101
|
+
prerendered) a rebuild after that.
|
|
102
|
+
- **Retiring is not deleting.** A type that was renamed or superseded stays in the
|
|
103
|
+
database and the seed permanently; keep its renderer mapping old values so existing
|
|
104
|
+
content still renders, mark it retired, and stop offering it to editors.
|
|
105
|
+
- **Typed fields:** `emdash types` does not type `blocks` fields (they come out as
|
|
106
|
+
`unknown[]`); hand-write a value type per renderer until EmDash generates it.
|