@getvitops/create 0.9.0-alpha.4 → 0.9.0-alpha.5

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 CHANGED
@@ -1,5 +1,42 @@
1
1
  # @getvitops/create
2
2
 
3
+ ## 0.9.0-alpha.5
4
+
5
+ ### Minor Changes
6
+
7
+ - 6b5ea17: **Leaner scaffolded `AGENTS.md`, and skills named by scope.** The EmDash template's `AGENTS.md` went from 212 to about 70 lines and the static one from 141 to about 50: only always-true rules stay (source of truth, generated CSS, branches, `vp`/`vpr`, where to look), and task-specific guidance moved into skills loaded when the task matches.
8
+
9
+ - **Breaking, `@getvitops/skills`: skills are renamed by scope.** `custom-blocks` → `emdash-custom-blocks`, `content-and-deploys` → `emdash-content-and-deploys`, `contact-form` → `emdash-contact-form`, `csp` → `vitops-csp`, `see-the-page` → `vitops-see-the-page`, `ads` → `vitops-ads`. `emdash-*` skills only apply to an EmDash site; `vitops-*` apply anywhere, and both prefixes are reserved for toolkit skills. New: `emdash-building-pages` (blocks, nesting, page opener, widget areas, escape hatches, layout primitives, `site.chrome`) and `vitops-consent` (split out of `vitops-design-system`, whose description and body now cover styling and tokens only). Existing sites: run `vp install` so `skills experimental_sync` relinks, and replace the per-skill `.agents/skills/<name>` / `.claude/skills/<name>` lines in `.gitignore` with `.agents/skills/vitops-*`, `.agents/skills/emdash-*` and the same two under `.claude/skills/`. A site's own skill must not use those prefixes.
10
+ - **`vitops agents sync`:** the block lists only skills that apply (a project without `emdash` in its dependencies gets no `emdash-*` ones, and they are not linked), summaries are cut at a clause rather than with an ellipsis, the pointer says `vp exec vitops docs`, and the version stamp is gone so the file no longer changes on every CLI bump.
11
+ - **`@getvitops/create`:** both templates ship the trimmed `AGENTS.md`; the project-naming steps moved into the `setup` skill and EmDash 1.x behaviours, legal-page replacement and the audit prerequisites into `emdash-content-and-deploys`. Existing sites: nothing to change beyond the skill rename; delete the sections you no longer need.
12
+
13
+ - 7069f93: **Agents now look at rendered pages with `playwright-cli`.** Both templates add `playwright` as a devDependency, run `playwright cli install --skills` from `prepare` (writing the upstream command reference to `.claude/skills/playwright-cli/`), ship `.playwright/cli.config.json`, allow `pnpm exec playwright cli`, and deny `mcp__claude-in-chrome__*` and `mcp__playwright__*` in `.claude/settings.json`. `@getvitops/skills` adds a short `see-the-page` skill (serve first, snapshot before screenshot, dark mode, mobile, no-JS) that defers to the upstream one. Existing sites: add `playwright` as a devDependency, extend `prepare`, and copy the settings. `vitops audit` now needs only `@axe-core/playwright lighthouse` added.
14
+ - 4d94c8b: **Added: `site.chrome` — a configurable masthead and nav behaviour.** Non-breaking.
15
+
16
+ - `site.chrome.masthead` (new `@getvitops/config` schema): `position` (`static|sticky|fixed`), `behavior` (`always|hide-on-scroll`), `surface` (`solid|glass|transparent|transparent-until-scrolled`), `tone` (a `colors.roles` key), `layout` (`full|inset|floating`), `width`, `elevation` (`shadow`, `border`, `onScroll`), `height` and `inset`. `height` and `inset` take fluid lengths (`calc()`, `clamp()`, `min()`, `max()`, with `cqi`/`vw`/… units), checked against a closed grammar because they are written into CSS. `validateConfig` rejects a `tone` or `shadow` the design system does not define (only when the default theme states them directly, not via `extends`).
17
+ - `site.chrome.nav`: `compact` (`drawer|dropdown`), `drawerSide`, `breakpoint`/`mobileBreakpoint`/`megaBreakpoint`, `depth`, `megaDepth`.
18
+ - `<Masthead config={site}>` (`@getvitops/astro`, slots `brand`/default/`actions`) and `resolveMasthead()` / `resolveNavOptions()`. A prop overrides one field per layout. The key is `chrome`/`masthead` on purpose: `<header>` is valid in any section and `banner` already means the announcement bar and the landmark role.
19
+ - `<SiteNav>` gains `compact="dropdown"` (a columnar panel anchored under the toggle), `drawerSide` (previously hardcoded to the end edge in Astro), `megaDepth` and `megaPanel`. `.sitenav--mega` now styles second-level items as column headings and has an optional featured column.
20
+ - `@getvitops/emdash`: `VitopsMegaPanel` fills that column from the widget area `mega-<item url slug>` (or a `mega-area:<name>` class on the menu item). No area means no column.
21
+ - `@getvitops/create` (emdash template): the layout renders through `<Masthead>`; `site.json` ships a sticky glass masthead with a shadow once scrolled. The hand-written `.navbar { block-size: 64px }` is gone (use `masthead.height`).
22
+ - CSS (`patterns/masthead.css`, `patterns/sitenav.css`): **all formats**, including tailwind, where the patterns are inlined into `@layer components`. Hide-on-scroll and the `*-on-scroll` states are `scroll-state()` container queries that only subtract from a settled (solid, elevated, visible) bar, so a browser without them (Firefox today) shows that bar rather than a transparent one. A sticky or fixed masthead sets `--header-offset` / `--top-offset` so anchors land below it.
23
+ - `@getvitops/generator`: new `TIERS.masthead`; `TIERS.sitenav` documents the new props.
24
+
25
+ ### Patch Changes
26
+
27
+ - bb61a35: **Call tracking numbers: show an ad visitor a number that attributes the phone call.** Two additive config blocks, no CSS, no breaking change — all formats unaffected.
28
+
29
+ - **`site.ads.google.callTracking`** — Google Ads _website call conversions_. `{ label, number, cache? }` makes the consent-gated Google tag fetch a Google forwarding number (Canada supported) and swap it in for visitors who arrived from a Google ad. Google replaces text only, so the capture script also rewrites the matching `tel:` href. Needs `pixelId` and `site.tracking.enabled`; rejected on any other platform (only Google can put a number on the website — Microsoft's exist only inside the ad, in the US/UK/AU).
30
+ - **`site.tracking.phoneNumbers`** — per-platform numbers for every other platform: the real `number`, `sources` keyed by `site.ads` platform (plus `default-paid`), and an informational `provider`. You provision the numbers with a call-tracking service (Twilio, CallRail, …). The landing URL decides first, then the `_ac` cookie, so a visitor who declines `marketing` sees a tracking number on the landing page only. An organic visitor is never swapped, and the server-rendered HTML always carries the real number.
31
+ - The `tel:` tap beacon now carries which number was shown (`phoneSource`), and the conversion notification says so.
32
+ - `vitops ads lint` flags a call-tracking provider that is not in `site.legal.privacyPolicy.processors`, and notes that `callTracking` plus `conversionLabel.call` can count one call twice (make one secondary in Google Ads).
33
+ - `@getvitops/utils/tracking` exports the pure `selectPhoneNumber()` and helpers; the capture script mirrors it and `tracking.test.ts` pins the two.
34
+ - `@getvitops/skills` gains a **`vitops-ads`** skill (overview plus a `call-tracking.md` topic). The templates gitignore synced skills by prefix (`vitops-*`, `emdash-*`), so it needs no `.gitignore` line of its own.
35
+
36
+ `@getvitops/core`, `@getvitops/vite` and `@getvitops/emdash` — no consumer-facing change in these packages.
37
+
38
+ - 74a87ed: **Scaffolded sites no longer exempt third-party tooling from the 3-day release cooldown.** The templates floated `vite-plus` and `emdash` on `latest` and exempted them (plus `@voidzero-dev/*`, `oxlint`, `oxfmt`) from `minimumReleaseAge`. An exempt package's exact-pinned dependencies are not exempt, so `vp create` then failed with `ERR_PNPM_NO_MATURE_MATCHING_VERSION` whenever vite-plus had just pinned a fresh `@oxc-project/*`. Now only `@getvitops/*` is exempt; `vite-plus` and `vite` (the `vite-plus-core` alias) are `^1.5.1` (the security fix for 1.5.0) and `emdash` and `@emdash-cms/cloudflare` are `^1.4.0`, all ranges, so pnpm resolves the newest version that has cleared the cooldown. Existing sites: in `pnpm-workspace.yaml` reduce `minimumReleaseAgeExclude` to `'@getvitops/*'` and change the `catalog` entries to `^1.5.1`; in `package.json` change those four dependencies from `latest` to those ranges (`vite` as `npm:@voidzero-dev/vite-plus-core@^1.5.1`).
39
+
3
40
  ## 0.9.0-alpha.4
4
41
 
5
42
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getvitops/create",
3
- "version": "0.9.0-alpha.4",
3
+ "version": "0.9.0-alpha.5",
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.4"
22
+ "@getvitops/generator": "8.3.0-alpha.5"
23
23
  },
24
24
  "engines": {
25
25
  "node": ">=24"
@@ -59,3 +59,10 @@ report that verbatim rather than guessing at a fix. A step reported as
59
59
  "blocked" is not a failure to retry; it is waiting on one of the hard stops
60
60
  above, or on a `site.json` field the user still needs to fill in (stage 5,
61
61
  Configuration).
62
+
63
+ ## Naming the project
64
+
65
+ `vp create` takes the package name from the target directory and invents a random slug when none
66
+ is given, so pass one: `vp create @getvitops:<template> --directory <client-slug>`. `wrangler.jsonc`'s
67
+ `name` (and on EmDash the D1 `database_name` and R2 `bucket_name`) are separate and still say what
68
+ the template shipped; rename them to match before the first deploy.
@@ -1,6 +1,12 @@
1
1
  {
2
2
  "permissions": {
3
- "allow": ["mcp__astro-docs__search_astro_docs", "mcp__emdash-docs__search_docs"]
3
+ "allow": [
4
+ "mcp__astro-docs__search_astro_docs",
5
+ "mcp__emdash-docs__search_docs",
6
+ "Bash(pnpm exec playwright cli:*)",
7
+ "Bash(npx --no-install playwright cli:*)"
8
+ ],
9
+ "deny": ["mcp__claude-in-chrome__*", "mcp__playwright__*"]
4
10
  },
5
11
  "hooks": {
6
12
  "SessionStart": [
@@ -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 (.agents/skills/content-and-deploys,
13
+ # and an EmDash `content:afterPublish` hook POSTs that event (.agents/skills/emdash-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
@@ -0,0 +1,7 @@
1
+ {
2
+ "browser": {
3
+ "browserName": "chromium",
4
+ "launchOptions": { "headless": true }
5
+ },
6
+ "outputDir": ".playwright-cli"
7
+ }
@@ -3,198 +3,72 @@
3
3
  Built with the Vitops design system (`@getvitops/*`); tokens and site facts live in `site.json`.
4
4
  Prefer its classes and patterns over hand-written CSS. Load the skill that matches the task:
5
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 -->
6
+ - [emdash-building-pages](.agents/skills/emdash-building-pages/SKILL.md) — Use when authoring or changing a page's layout or content on this EmDash site — composing `vitops_*` blocks
7
+ - [emdash-contact-form](.agents/skills/emdash-contact-form/SKILL.md) — Use when adding or changing a form, fixing submissions that don't arrive, setting up notifications, CRM or Turnstile
8
+ - [emdash-content-and-deploys](.agents/skills/emdash-content-and-deploys/SKILL.md) — Use for any deploy or build problem, stale or missing content, "my publish isn't live", scheduled posts, previews
9
+ - [emdash-custom-blocks](.agents/skills/emdash-custom-blocks/SKILL.md) — Use before creating, renaming or deleting any block type, any `src/components/blocks/*` renderer
10
+ - [vitops-ads](.agents/skills/vitops-ads/SKILL.md) — Use when adding or changing a Google/Meta/LinkedIn/TikTok/Microsoft/Reddit/Pinterest/Snapchat pixel
11
+ - [vitops-consent](.agents/skills/vitops-consent/SKILL.md) — Use before gating, adding or changing any non-essential third-party tag, cookie, embed
12
+ - [vitops-csp](.agents/skills/vitops-csp/SKILL.md) — Use when changing `public/_headers`, adding an embed, tracking snippet or third-party script, turning the CSP on
13
+ - [vitops-design-system](.agents/skills/vitops-design-system/SKILL.md) — Use when styling or authoring pages or components (choosing utility/pattern classes, colours, spacing, typography)
14
+ - [vitops-see-the-page](.agents/skills/vitops-see-the-page/SKILL.md) — How to look at a rendered page of this site
15
+
16
+ Reference, rendered from this project: `vp exec vitops docs [topic]` (no topic lists them).
14
17
  <!-- vitops:end -->
15
18
 
16
- This is an EmDash CMS website (Astro on Cloudflare Workers, D1 + R2 — public pages
17
- prerendered, the Worker serving the admin and API) styled by the Vitops design system.
19
+ This is an EmDash CMS website (Astro on Cloudflare Workers, D1 + R2: public pages prerendered, the
20
+ Worker serving the admin and API) styled by the Vitops design system.
18
21
 
19
22
  ## Ground rules
20
23
 
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.
24
+ - `site.json` is the single source of truth: organization + site facts, and the design system under
25
+ `designSystem.themes.default`. Never edit the generated stylesheet (`src/styles/tailwind.css`,
26
+ rebuilt from `site.json` on every dev/build, gitignored); put your own CSS in a separate file
27
+ (e.g. `src/site.css`, imported from the layout) and commit it.
27
28
  - 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).
29
+ `getEmDashEntry` from `emdash`; render Portable Text with `<PortableText />` from `emdash/ui`.
30
30
  - **Where truth lives:** content (entries, media, menus) is **production's**, edited there; code,
31
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`.
32
+ refreshed from prod, never promoted back.
33
33
  - `seed/seed.json` applies only to an empty database. To change a running site's content model, use
34
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.
35
+ - Hosting comes from `vitopsHosting()` in `astro.config.mjs`; never hard-code an adapter or
36
+ d1/r2/sqlite. Keep the Worker cron trigger in `wrangler.jsonc`: it publishes scheduled content.
39
37
  - 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
- - **Page opener.** Use `vitops_section` at heading level 1 with its button fields (`cta_label`,
55
- `secondary_label`) and `stack: rhythm`; don't nest a `vitops_hero` or another section wrapper in a
56
- section body, which renders two layers of `section > .centered`. `vitops_hero` is for a hero
57
- that stands alone (it carries option cards).
58
- - **Section, block or widget area?** Copy-and-edit content is an EmDash Section. The same content
59
- everywhere (announcement bar, pre-footer call to action, sidebar, footer columns) is a **widget
60
- area**: define it under Widgets in the admin and render it with `<VitopsWidgetArea name="…" />`
61
- from `@getvitops/emdash/astro` in the layout, or in page content with the `vitops_widgetArea`
62
- block. Page-specific content is blocks. An area can't hold another area, and prerendered pages
63
- show an edit after the next deploy (like menus).
64
- - **Escape hatches on every block:** `theme` (a backdrop from the site's colour roles, `surface` or
65
- `brand-primary:solid`, with the readable text for it picked by the design system, so contrast is
66
- not the editor's judgement), `anchor` (the `id`, for `#links`), `className` (extra classes; under
67
- the tailwind format only ones the site already uses) and `style` (CSS custom properties only,
68
- e.g. `--p-card: 2rem`; anything else is dropped). The same backdrop is available on a widget area
69
- (`<VitopsWidgetArea theme="…">`) and on your own markup (`themeAttrs('surface')` from
70
- `@getvitops/emdash/astro`, spread on the layout's `<body>`). How a role looks (flat, raised,
71
- glass) is the site's own role styling in `site.json`. Set `--header-offset` to the sticky
72
- header's height so anchors clear it.
73
- - Override a block per site with `<PortableText components={{ type: { 'vitops_cards': MyCards } }} />`;
74
- hide one with `vitopsEmdash({ blocks: { exclude: [...] } })`.
75
- - **One `<h1>` per page.** The template's page routes render the title as an `<h1>`, and a hero's
76
- heading level defaults to `h1`, so set a hero's level to `2` on those pages (or let the hero own
77
- the `h1`). Use `p` for a display heading that is not part of the outline. `vitops emdash build`
78
- warns on any page without exactly one `<h1>`.
79
- - Labels: `badge` is static (status, count, category); `tag` may be editable or dismissable. Don't
80
- invent `chip` or `pill`. The button class is `btn`.
81
- - **Before adding a block type** or a `src/components/blocks/*` renderer, read
82
- `.agents/skills/custom-blocks/SKILL.md`. Types can't be renamed or deleted once pages use them.
83
-
84
- ## Layout foundations — the answer, not a starting point
85
-
86
- | About to write… | Write instead |
87
- | ------------------------------------------------------- | --------------- |
88
- | a `wrap`/`wrapper`/`container`/`inner` class | **`centered`** |
89
- | `max-width` + `margin-inline: auto` | **`centered`** |
90
- | `padding-block` on a section | **`region`** |
91
- | margins between headings/paragraphs/lists | **`rhythm`** |
92
- | `display: grid` + `grid-template-columns: repeat(n, …)` | **`subgrid`** |
93
- | `repeat(auto-fit, minmax(…))` | **`grid-auto`** |
94
- | `display: flex` + `gap` for a row of controls | **`cluster`** |
95
-
96
- `centered` is a grid of _named tracks_: a child widens itself with `breakout`/`spotlight`/
97
- `fullbleed` without the parent knowing, which a bespoke `.wrap` can't do.
98
-
99
- - **More than one card means `subgrid`**, so headings and CTAs align across the set:
100
- `<ul class="subgrid subgrid-cols-3" role="list">` with `<li class="card subgrid-card">`, or
101
- `<Subgrid />` (renders the slot verbatim; author the `<li>`s).
102
- - **A card that is a link:** `stretched-link` inside it is zero-JS but kills text selection;
103
- `<Cards>` keeps selection and falls back to the same link with no JS. Never both. Never
104
- `<li><a class="card">`: the `li` is the grid item, so the tranches don't align.
105
- - `vpr lint:design` reports all of these and the pre-commit hook fails on them. Fix the class, not
106
- the CSS.
107
-
108
- ## Rendering and the contact form
109
-
110
- - Every public page is **prerendered**; the Worker handles `/_emdash/*` and `src/pages/api/track.ts`.
111
- An admin publish goes live after a rebuild. Run `vpr prepare-local-db` before a first local
112
- `vpr build`. Details, previews and server islands: `.agents/skills/content-and-deploys/SKILL.md`.
113
- - The contact form posts to `/api/track`. Set `site.notifications.email` before launch or the route
114
- refuses posts, and mirror form fields in `site.templates`. Details:
115
- `.agents/skills/contact-form/SKILL.md`.
116
- - `public/_headers` ships a Report-Only CSP. Read `.agents/skills/csp/SKILL.md` before enforcing it
117
- or adding embeds.
118
- - `vitops audit` needs optional packages and a browser, checked before it starts a server:
119
- `vp add -D playwright @axe-core/playwright lighthouse && vp exec playwright install chromium`.
120
- Its page list is the built sitemap, then the seed's menus.
121
-
122
- ## Branch model
123
-
124
- - Work on **`dev`**; every push auto-deploys the isolated `-dev` worker.
125
- - **`main`** is production and protected: never push or merge to it. It ships only through the
126
- `promote` skill (`.agents/skills/promote/`), and only when the user explicitly asks.
38
+ `vpr lint:design` fails the pre-commit hook on unresolvable classes and layout primitives you
39
+ hand-rolled (`centered`, `region`, `rhythm`, `subgrid`, `grid-auto`, `cluster` exist for those).
40
+ - `src/assets/placeholder/` is wireframe stand-in imagery; delete it once real assets land.
127
41
  - Sample legal pages (`/terms`, `/privacy`) are deliberately short, unfinished placeholders; don't
128
- "improve" them into something that reads as a finished policy. To replace them, fill in
129
- `site.json`'s `organization`/`site.legal`, run
130
- `vp exec vitops legal --format portable-text --out ./legal`, and paste each document into the page
131
- in the admin. It still needs review before launch.
132
-
133
- ## EmDash 1.x behaviours
134
-
135
- - Passkeys are bound to the hostname they were created on. After a domain cutover use **Settings →
136
- General → Change domain**, then **Continue on**.
137
- - Link entries by `entry.data.slug`, never `entry.id` (multilingual sites prefix `id` with a locale).
138
- - Submit `/sitemap.xml` to search engines, not a per-collection sitemap.
139
- - Clear a stuck D1 migration lock with `vp exec emdash migrate --take-over-lock <id>` (needs
140
- `--expected-target-fingerprint` non-interactively).
141
-
142
- ## Improving the toolkit
143
-
144
- You are also a source of feedback for `@getvitops/*`. When you struggle with a framework built-in,
145
- write a workaround, add bespoke CSS or a site-local block, or find a doc wrong or missing, and the
146
- need is **not** unique to this site, record it in `UPSTREAM.md` at the repo root (create it if
147
- absent). One entry each, with: what you tried, what failed or was missing, the workaround (file
148
- path), the change you'd propose, and the package (`@getvitops/emdash`, `astro`, `core`, `cli`,
149
- `create`, or docs). Mention new entries in your end-of-task summary. Don't log choices that are
150
- specific to this site, and never patch `node_modules`.
42
+ "improve" them into something that reads as a finished policy.
43
+ - `SETUP.html` is the onboarding checklist (the `setup` skill). Tick a box only for a step you
44
+ completed, never for a human-only one (the Cloudflare API token, domain cutover, legal documents).
151
45
 
152
- ## Docs and skills
46
+ ## Branches
153
47
 
154
- - `.mcp.json` preconfigures the Astro and EmDash docs MCP servers; search them before relying on
155
- pre-trained knowledge.
156
- - **Skills.** `@getvitops/skills` ships `custom-blocks`, `content-and-deploys`, `contact-form` and
157
- `csp`. `skills experimental_sync` (run by `prepare`, so by `vp install`) links them into
158
- `.agents/skills/` and `.claude/skills/`; they are gitignored and update with `vp update`. A skill
159
- you write yourself with the same name is never replaced. `promote` and `setup` are this site's own.
160
- - The EmDash skills (`building-emdash-site`, `creating-plugins`, `emdash-cli`, `upgrading-emdash`,
161
- two WordPress porting skills) are installed per project to match the pinned `emdash`. If
162
- `.agents/skills/building-emdash-site/` is missing: `vp dlx skills add emdash-cms/skills --skill '*' -y`.
163
- Refresh with `vp dlx skills update` after bumping `emdash`.
48
+ Work on **`dev`**; every push auto-deploys the isolated `-dev` worker. **`main`** is production and
49
+ protected: never push or merge to it. It ships only through the `promote` skill, and only when the
50
+ user explicitly asks. Shipping is not a package release: no changeset or version bump.
164
51
 
165
52
  ## Commands
166
53
 
167
- This is a **Vite+** project: use `vp` (and `vpr`), not `pnpm`/`npm`/`npx`. Scripts that wrap Astro
168
- go through `vpr` (`vpr dev`, `vpr build`, `vpr preview`), because `vp dev`/`vp build` skip Astro.
54
+ This is a **Vite+** project: use `vp` (and `vpr` for scripts), not `pnpm`/`npm`/`npx`. Scripts that
55
+ wrap Astro go through `vpr` (`vpr dev`, `vpr build`), because `vp dev`/`vp build` skip Astro.
169
56
  `vpr dev` returns immediately (the server runs in the background): check it with
170
57
  `vp exec astro dev status`, read `vp exec astro dev logs --follow`, stop with
171
- `vp exec astro dev stop`; the site is at <http://localhost:4321>.
172
-
173
- | To… | Run |
174
- | ---------------------------------------- | ------------------------------------------------------- |
175
- | install dependencies | `vp install` |
176
- | add / remove / update a package | `vp add <pkg>` · `vp remove <pkg>` · `vp update` |
177
- | run a `package.json` script | `vpr <script>` |
178
- | run a locally installed binary | `vp exec <bin>` (e.g. `vp exec vitops docs components`) |
179
- | run a one-off package without installing | `vp dlx <pkg>` |
180
- | format + lint + typecheck (files) | `vp check` |
181
- | typecheck Astro (`.astro` files) | `vpr typecheck` |
182
- | design-lint the markup | `vpr lint:design` |
58
+ `vp exec astro dev stop`; the site is at <http://localhost:4321>. `package.json` lists the scripts;
59
+ `vp check` and `vpr typecheck` verify a change. `vpr init:github` and `vpr init:hosting` are one-time
60
+ setup, only when the user asks.
183
61
 
184
- | Script | What it does |
185
- | ----------------------------------- | ------------------------------------------------------------------------------------------- |
186
- | `vpr dev` | local dev with local D1/R2 emulation (no Cloudflare account) |
187
- | `vpr deploy` | pull production content, build, `wrangler deploy` prod (provisions D1/R2 on first run) |
188
- | `vpr deploy:dev` | same, for the isolated dev worker (needs `DEV_D1_ID`) |
189
- | `vpr pull-content` | just the content pull |
190
- | `vpr prepare-local-db` | migrate and seed the local D1 the build prerenders from |
191
- | `vpr init:github` | one-time: create + push the GitHub repo (main + dev, dev default). Only when the user asks. |
192
- | `vpr init:hosting` | one-time: provision hosting (dev D1/R2 + repo secrets). Only when the user asks. |
193
- | `vp exec vitops validate site.json` | schema-check the site config |
62
+ ## Where to look
194
63
 
195
- Shipping is not a package release: no changeset or version bump. Commit to `dev`, then promote.
196
- (`vp` has no `vp change` command.)
197
-
198
- `vp create` names the package after the target directory (pass `--directory <client-slug>`).
199
- `wrangler.jsonc`'s `name`, D1 `database_name` and R2 `bucket_name` are separate and still say
200
- `my-emdash-site`; rename them to match before the first deploy.
64
+ - **Skills** are listed in the block at the top; load the one matching the task before starting.
65
+ - **Reference** for this project's own tokens, classes, blocks and config: `vp exec vitops docs`.
66
+ - `.mcp.json` preconfigures the Astro and EmDash docs MCP servers; search them before relying on
67
+ pre-trained knowledge. The EmDash skills (`building-emdash-site`, `emdash-cli`, …) are installed
68
+ per project to match the pinned `emdash`; if `.agents/skills/building-emdash-site/` is missing:
69
+ `vp dlx skills add emdash-cms/skills --skill '*' -y`.
70
+ - **Improving the toolkit:** when you work around a framework built-in, add bespoke CSS or a
71
+ site-local block, or find a doc wrong or missing, and the need is **not** unique to this site,
72
+ record it in `UPSTREAM.md` at the repo root: what you tried, what failed or was missing, the
73
+ workaround (file path), the change you'd propose, and the package. Mention new entries in your
74
+ end-of-task summary. Never patch `node_modules`.
@@ -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 `.agents/skills/content-and-deploys` for how production content
54
+ per request — see `.agents/skills/emdash-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
 
@@ -179,23 +179,23 @@ vpr build:css # regenerate the CSS manually
179
179
 
180
180
  ## Project map
181
181
 
182
- | Path | Purpose |
183
- | -------------------------------- | -------------------------------------------------------------------------------------------------- |
184
- | `site.json` | Org + site config; design tokens/patterns at `designSystem.themes.default` → generated CSS |
185
- | `seed/seed.json` | First-boot content model + starter content |
186
- | `src/worker.ts` | Worker entry (fetch + EmDash scheduled handler) |
187
- | `src/live.config.ts` | Astro live content collections wiring for EmDash |
188
- | `src/layouts/Layout.astro` | Site chrome: EmDash + Vitops head tags, nav from the CMS menu |
189
- | `src/pages/index.astro` | Home — prerendered from the `home` page entry |
190
- | `src/pages/[...slug].astro` | Prerendered once per published `pages` entry (`getStaticPaths`) |
191
- | `src/pages/404.astro` | Prerendered not-found page, served with a 404 status for any unmatched URL |
192
- | `public/_headers` | Report-Only CSP for the static pages (see `.agents/skills/csp`) |
193
- | `scripts/pull-content.mjs` | Loads production's D1 into the local build DB before the prerender (`deploy`/`deploy:dev`) |
194
- | `wrangler.jsonc` | Cloudflare bindings (D1/R2) + cron trigger |
195
- | `scripts/build-dev-wrangler.mjs` | Rewrites the built wrangler config for the isolated dev worker |
196
- | `SETUP.html` | Client-onboarding checklist — run `vpr sop` first (see "One-time setup") |
197
- | `.github/workflows/` | deploy-dev (push to `dev`), deploy-prod (push to `main`, or an `emdash-publish` dispatch), promote |
198
- | `.agents/skills/promote/` | Agent skill: dispatch the dev → main promotion |
199
- | `.agents/skills/custom-blocks/` | Agent skill: when (and how) to add a block type — compose, extend, local, upstream |
200
- | `.mcp.json` | Astro + EmDash docs MCP servers for coding agents |
201
- | `.claude/hooks/` | Claude Code web: Node 24 + pnpm setup on session start |
182
+ | Path | Purpose |
183
+ | -------------------------------------- | -------------------------------------------------------------------------------------------------- |
184
+ | `site.json` | Org + site config; design tokens/patterns at `designSystem.themes.default` → generated CSS |
185
+ | `seed/seed.json` | First-boot content model + starter content |
186
+ | `src/worker.ts` | Worker entry (fetch + EmDash scheduled handler) |
187
+ | `src/live.config.ts` | Astro live content collections wiring for EmDash |
188
+ | `src/layouts/Layout.astro` | Site chrome: EmDash + Vitops head tags, nav from the CMS menu |
189
+ | `src/pages/index.astro` | Home — prerendered from the `home` page entry |
190
+ | `src/pages/[...slug].astro` | Prerendered once per published `pages` entry (`getStaticPaths`) |
191
+ | `src/pages/404.astro` | Prerendered not-found page, served with a 404 status for any unmatched URL |
192
+ | `public/_headers` | Report-Only CSP for the static pages (see `.agents/skills/vitops-csp`) |
193
+ | `scripts/pull-content.mjs` | Loads production's D1 into the local build DB before the prerender (`deploy`/`deploy:dev`) |
194
+ | `wrangler.jsonc` | Cloudflare bindings (D1/R2) + cron trigger |
195
+ | `scripts/build-dev-wrangler.mjs` | Rewrites the built wrangler config for the isolated dev worker |
196
+ | `SETUP.html` | Client-onboarding checklist — run `vpr sop` first (see "One-time setup") |
197
+ | `.github/workflows/` | deploy-dev (push to `dev`), deploy-prod (push to `main`, or an `emdash-publish` dispatch), promote |
198
+ | `.agents/skills/promote/` | Agent skill: dispatch the dev → main promotion |
199
+ | `.agents/skills/emdash-custom-blocks/` | Agent skill: when (and how) to add a block type — compose, extend, local, upstream |
200
+ | `.mcp.json` | Astro + EmDash docs MCP servers for coding agents |
201
+ | `.claude/hooks/` | Claude Code web: Node 24 + pnpm setup on session start |
@@ -35,14 +35,14 @@ public/vitops/
35
35
  .DS_Store
36
36
 
37
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
38
+ # (the `vitops-` and `emdash-` prefixes are reserved for toolkit skills)
39
+ .agents/skills/vitops-*
40
+ .agents/skills/emdash-*
41
+ .claude/skills/vitops-*
42
+ .claude/skills/emdash-*
46
43
  # `skills experimental_sync` also leaves a duplicate `pnpm-@getvitops+skills-*` link per skill
47
44
  .agents/skills/pnpm-*
48
45
  .claude/skills/pnpm-*
46
+ # playwright-cli: skill regenerated by `prepare`; output is per-session scratch
47
+ .claude/skills/playwright-cli
48
+ .playwright-cli/
@@ -18,19 +18,19 @@
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 && skills experimental_sync -y -a claude-code --no-frozen-lockfile && vitops agents sync -i site.json"
21
+ "prepare": "vp config && skills experimental_sync -y -a claude-code --no-frozen-lockfile && playwright cli install --skills && vitops agents sync -i site.json"
22
22
  },
23
23
  "dependencies": {
24
24
  "@astrojs/cloudflare": "^14.0.0",
25
25
  "@astrojs/react": "^7.1.2",
26
- "@emdash-cms/cloudflare": "latest",
26
+ "@emdash-cms/cloudflare": "^1.4.0",
27
27
  "@getvitops/astro": "latest",
28
28
  "@getvitops/core": "latest",
29
29
  "@getvitops/emdash": "latest",
30
30
  "@getvitops/skills": "latest",
31
31
  "@tailwindcss/vite": "^4.3.3",
32
32
  "astro": "^7.4.4",
33
- "emdash": "latest",
33
+ "emdash": "^1.4.0",
34
34
  "react": "^19.3.1",
35
35
  "react-dom": "^19.3.1",
36
36
  "tailwindcss": "^4.3.3"
@@ -40,10 +40,11 @@
40
40
  "@cloudflare/workers-types": "^4.20260305.1",
41
41
  "@getvitops/cli": "latest",
42
42
  "@iconify-json/fa7-solid": "^1.2.5",
43
+ "playwright": "^1.62.1",
43
44
  "skills": "^1.7.6",
44
45
  "typescript": "^6.0.3",
45
- "vite": "latest",
46
- "vite-plus": "latest",
46
+ "vite": "npm:@voidzero-dev/vite-plus-core@^1.5.1",
47
+ "vite-plus": "^1.5.1",
47
48
  "wrangler": "^4.111.0"
48
49
  },
49
50
  "devEngines": {
@@ -6,25 +6,18 @@ allowBuilds:
6
6
  better-sqlite3: false
7
7
  sharp: false
8
8
 
9
- # Supply-chain hardening: cooldown on brand-new releases (toolchain exempt)
10
- # and no non-registry transitive sources.
9
+ # Supply-chain hardening: nothing installs within 3 days of its publish, and no non-registry
10
+ # transitive sources. Only our own lockstep `@getvitops/*` is exempt. Do NOT exempt a third-party
11
+ # toolchain: an exempt package's exact pins are not exempt (vite-plus pins `@oxc-project/*`
12
+ # exactly), so the install fails whenever one is fresher than the window. A security patch may be
13
+ # admitted with an exact `name@x.y.z` line and a dated reason; remove it once it matures.
11
14
  minimumReleaseAge: 4320
12
15
  minimumReleaseAgeExclude:
13
16
  - '@getvitops/*'
14
- - emdash
15
- - '@emdash-cms/*'
16
- - vite-plus
17
- - '@voidzero-dev/*'
18
- - oxlint
19
- - '@oxlint/*'
20
- - oxlint-tsgolint
21
- - '@oxlint-tsgolint/*'
22
- - oxfmt
23
- - '@oxfmt/*'
24
17
  blockExoticSubdeps: true
25
18
  catalog:
26
- vite: npm:@voidzero-dev/vite-plus-core@latest
27
- vite-plus: latest
19
+ vite: npm:@voidzero-dev/vite-plus-core@^1.5.1
20
+ vite-plus: ^1.5.1
28
21
  overrides:
29
22
  vite: 'catalog:'
30
23
  peerDependencyRules:
@@ -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 .agents/skills/csp.
4
+ # on-demand). See .agents/skills/vitops-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 .agents/skills/content-and-deploys), so this
9
+ // into every page. Production is the truth for content (see .agents/skills/emdash-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
@@ -414,6 +414,17 @@
414
414
  },
415
415
  "site": {
416
416
  "defaultLocale": "en",
417
+ "chrome": {
418
+ "masthead": {
419
+ "position": "sticky",
420
+ "behavior": "always",
421
+ "surface": "glass",
422
+ "layout": "full",
423
+ "elevation": { "shadow": "sm", "onScroll": true },
424
+ "height": "4rem"
425
+ },
426
+ "nav": { "compact": "drawer", "drawerSide": "end" }
427
+ },
417
428
  "templates": {
418
429
  "contact": {
419
430
  "type": "form",
@@ -5,9 +5,11 @@ import { getMenu, getSiteSettings } from 'emdash';
5
5
  import { createPublicPageContext } from 'emdash/page';
6
6
  import SiteFooter from '@getvitops/astro/components/SiteFooter.astro';
7
7
  import SiteNav from '@getvitops/astro/components/SiteNav.astro';
8
+ import Masthead from '@getvitops/astro/components/Masthead.astro';
9
+ import { resolveNavOptions } from '@getvitops/astro/components/masthead.ts';
8
10
  import { loadNavMenus } from '@getvitops/astro/components/sitenav.ts';
9
11
  import { EmDashHead } from 'emdash/ui';
10
- import { VitopsWidgetArea } from '@getvitops/emdash/astro';
12
+ import { VitopsMegaPanel, VitopsWidgetArea } from '@getvitops/emdash/astro';
11
13
  import config from '../../site.json' with { type: 'json' };
12
14
  // Design-system CSS, layout-scoped (vitops css.inject: false): only pages
13
15
  // rendered through this layout get it — keeps it out of /_emdash/admin, while
@@ -75,18 +77,19 @@ const orgName = config.organization?.name || siteTitle;
75
77
  <body>
76
78
  <!-- Widget areas (Widgets in the admin): the same content wherever they appear. -->
77
79
  <VitopsWidgetArea name="announcement" class="centered" />
78
- <header class="navbar-sticky centered">
79
- <div class="navbar">
80
- <a class="brand flex items-center" href="/" aria-label={`${siteTitle} home`}>
81
- <span class="brand-mark" set:html={logo} aria-hidden="true" />
82
- <span class="font-heading">{siteTitle}</span>
83
- </a>
84
- <div class="header-end flex items-center">
85
- <SiteNav {...nav} />
86
- <wc-color-scheme-toggle></wc-color-scheme-toggle>
87
- </div>
80
+ <!-- The masthead's look and the nav's behaviour come from `site.chrome` in site.json
81
+ (position, hide-on-scroll, glass, inset, shadow, drawer vs dropdown). A prop here
82
+ overrides one field for this layout. -->
83
+ <Masthead config={config}>
84
+ <a slot="brand" class="brand flex items-center" href="/" aria-label={`${siteTitle} home`}>
85
+ <span class="brand-mark" set:html={logo} aria-hidden="true" />
86
+ <span class="font-heading">{siteTitle}</span>
87
+ </a>
88
+ <div class="header-end flex items-center">
89
+ <SiteNav {...nav} {...resolveNavOptions(config)} megaPanel={VitopsMegaPanel} />
90
+ <wc-color-scheme-toggle></wc-color-scheme-toggle>
88
91
  </div>
89
- </header>
92
+ </Masthead>
90
93
 
91
94
  <main class="centered rhythm">
92
95
  <slot />
@@ -109,10 +112,6 @@ const orgName = config.organization?.name || siteTitle;
109
112
  block-size: 1.75rem;
110
113
  }
111
114
 
112
- .navbar {
113
- block-size: 64px;
114
- }
115
-
116
115
  .header-end {
117
116
  gap: var(--space-s, 0.75rem);
118
117
  }
@@ -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 .agents/skills/content-and-deploys).
11
+ // (see .agents/skills/emdash-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
@@ -2,7 +2,7 @@
2
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 .agents/skills/content-and-deploys); this route sets
5
+ * Every public page is prerendered (see .agents/skills/emdash-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,7 +5,7 @@ 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 (.agents/skills/content-and-deploys,
8
+ // a publish triggers a rebuild that bakes the change in (.agents/skills/emdash-content-and-deploys,
9
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
  //
@@ -59,3 +59,10 @@ report that verbatim rather than guessing at a fix. A step reported as
59
59
  "blocked" is not a failure to retry; it is waiting on one of the hard stops
60
60
  above, or on a `site.json` field the user still needs to fill in (stage 5,
61
61
  Configuration).
62
+
63
+ ## Naming the project
64
+
65
+ `vp create` takes the package name from the target directory and invents a random slug when none
66
+ is given, so pass one: `vp create @getvitops:<template> --directory <client-slug>`. `wrangler.jsonc`'s
67
+ `name` (and on EmDash the D1 `database_name` and R2 `bucket_name`) are separate and still say what
68
+ the template shipped; rename them to match before the first deploy.
@@ -0,0 +1,6 @@
1
+ {
2
+ "permissions": {
3
+ "allow": ["Bash(pnpm exec playwright cli:*)", "Bash(npx --no-install playwright cli:*)"],
4
+ "deny": ["mcp__claude-in-chrome__*", "mcp__playwright__*"]
5
+ }
6
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "browser": {
3
+ "browserName": "chromium",
4
+ "launchOptions": { "headless": true }
5
+ },
6
+ "outputDir": ".playwright-cli"
7
+ }
@@ -3,138 +3,50 @@
3
3
  Built with the Vitops design system (`@getvitops/*`); tokens and site facts live in `site.json`.
4
4
  Prefer its classes and patterns over hand-written CSS. Load the skill that matches the task:
5
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…
6
+ - [vitops-ads](.agents/skills/vitops-ads/SKILL.md) — Use when adding or changing a Google/Meta/LinkedIn/TikTok/Microsoft/Reddit/Pinterest/Snapchat pixel
7
+ - [vitops-consent](.agents/skills/vitops-consent/SKILL.md) — Use before gating, adding or changing any non-essential third-party tag, cookie, embed
8
+ - [vitops-csp](.agents/skills/vitops-csp/SKILL.md) — Use when changing `public/_headers`, adding an embed, tracking snippet or third-party script, turning the CSP on
9
+ - [vitops-design-system](.agents/skills/vitops-design-system/SKILL.md) — Use when styling or authoring pages or components (choosing utility/pattern classes, colours, spacing, typography)
10
+ - [vitops-see-the-page](.agents/skills/vitops-see-the-page/SKILL.md) — How to look at a rendered page of this site
11
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 -->
12
+ Reference, rendered from this project: `vp exec vitops docs [topic]` (no topic lists them).
14
13
  <!-- vitops:end -->
15
14
 
16
- This is a static Astro website styled by the Vitops design system, deployed
17
- as Cloudflare Workers static assets (no server runtime, no database).
15
+ This is a static Astro website styled by the Vitops design system, deployed as Cloudflare Workers
16
+ static assets (no server runtime, no database).
18
17
 
19
18
  ## Ground rules
20
19
 
21
- - `site.json` is the single source of truth — organization + site facts, and
22
- the design system (colors, type, spacing, patterns, animations) under
23
- `designSystem.themes.default`. Never edit `src/styles/` by hand — it is
24
- generated from `site.json` by the `vitops()` integration on every dev/build
25
- and is gitignored. (There is no separate `design-system.json`: a scaffolded
26
- config can't merely reference a sibling file, so the token set lives inside
27
- `site.json`.)
28
- - `src/assets/placeholder/` holds neutral wireframe stand-ins (square,
29
- portrait, landscape; SVG + PNG) for layout work before real imagery
30
- exists. Delete the directory once real assets land.
31
- - Prefer framework CSS classes (utilities, patterns, type roles like
32
- `font-heading`) over bespoke CSS. `vp exec vitops docs classes` prints the
33
- full class vocabulary.
34
- - **Layout foundations — these are the answer, not a starting point.** Six
35
- substitutions carry the structure of nearly every page:
36
- | About to write… | Write instead |
37
- | ------------------------------------------------------- | --------------- |
38
- | a `wrap`/`wrapper`/`container`/`inner` class | **`centered`** |
39
- | `max-width` + `margin-inline: auto` | **`centered`** |
40
- | `padding-block` on a section | **`region`** |
41
- | margins between headings/paragraphs/lists | **`rhythm`** |
42
- | `display: grid` + `grid-template-columns: repeat(n, …)` | **`subgrid`** |
43
- | `repeat(auto-fit, minmax(…))` | **`grid-auto`** |
44
- | `display: flex` + `gap` for a row of controls | **`cluster`** |
45
- - Every page is prerendered by default (`output: 'static'`) — there is no
46
- server to fall back to, so nothing here can read request-time data
47
- (cookies, headers, query params) at render time. If a page genuinely needs
48
- that, it needs an adapter and a different template — this one stays pure
49
- static on purpose.
50
- - **`SETUP.html` is the client-onboarding checklist.** When working it via the
51
- `.agents/skills/setup/` skill, tick a box only for a step you actually
52
- completed — never on the operator's behalf for a step marked human-only
53
- (the Cloudflare API token, the domain cutover, publishing legal documents).
54
-
55
- ## Branch model
56
-
57
- - Work on **`dev`** (the integration branch). Every push auto-deploys the
58
- isolated `-dev` worker (`dev.<domain>` with a custom domain).
59
- - **`main`** is production and branch-protected — never push or merge to it
60
- directly. Production ships only via the manual **promote** flow:
61
- `.github/workflows/promote.yml`, dispatched manually, opens a `dev → main`
62
- PR with auto-merge; merging deploys prod. Only run it when the user
63
- explicitly asks to promote/ship to production.
64
-
65
- ## Labels: badge and tag
66
-
67
- Two label patterns, both enabled in `site.json`'s `patterns.items`: **`badge`** is a
68
- static label (a status, count or category) and **`tag`** is a label that may be
69
- static _or_ dynamic (editable, dismissable, in a `tag-list`). Don't invent a third
70
- name — `chip` and `pill` are not part of this vocabulary. The button pattern's class
71
- is `btn` (`<a class="btn">` is styled, not just a bare `<button>`).
72
-
73
- ## Content Security Policy
74
-
75
- `public/_headers` ships a **`Content-Security-Policy-Report-Only`** header for every
76
- page (they are all static assets): violations are logged to the browser console and
77
- nothing is blocked, so it can't break a page. Its `script-src` is `'self'` plus the
78
- SHA-256 of each inline script `<Head />` emits (the colour-scheme bootstrap and the
79
- consent-gate stub), with no `'unsafe-inline'`. `contentSecurityPolicy()` and
80
- `inlineScriptHashes()` from `@getvitops/astro` produce the same strings.
81
-
82
- Before enforcing it (rename the header to `Content-Security-Policy`), browse the pages
83
- with the console open: add the origins of any third-party tag the consent gate
84
- activates to `script-src` / `connect-src`, any embed to `frame-src`, and hash any
85
- inline script you add yourself. Hashes rather than nonces because a static page has no
86
- per-request response to carry one.
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.
20
+ - `site.json` is the single source of truth: organization + site facts, and the design system under
21
+ `designSystem.themes.default`. Never edit `src/styles/`: it is generated from `site.json` on every
22
+ dev/build and gitignored.
23
+ - Every page is prerendered (`output: 'static'`): nothing can read request-time data (cookies,
24
+ headers, query params). A page that needs that needs an adapter and a different template.
25
+ - Prefer framework classes (utilities, patterns, type roles like `font-heading`) over bespoke CSS.
26
+ `vpr lint:design` fails the pre-commit hook on unresolvable classes and layout primitives you
27
+ hand-rolled (`centered`, `region`, `rhythm`, `subgrid`, `grid-auto`, `cluster` exist for those).
28
+ - `src/assets/placeholder/` is wireframe stand-in imagery; delete it once real assets land.
29
+ - `SETUP.html` is the onboarding checklist (the `setup` skill). Tick a box only for a step you
30
+ completed, never for a human-only one (the Cloudflare API token, domain cutover, legal documents).
31
+
32
+ ## Branches
33
+
34
+ Work on **`dev`**; every push auto-deploys the isolated `-dev` worker. **`main`** is production and
35
+ protected: never push or merge to it directly; production ships only through the manual `promote`
36
+ workflow (`.github/workflows/promote.yml`), only when the user explicitly asks. Shipping is not a
37
+ package release: no changeset or version bump.
94
38
 
95
39
  ## Commands
96
40
 
97
- This is a **Vite+** project: use `vp` (and its `vpr` shorthand), not `pnpm`/`npm`/`npx`.
98
- The package manager underneath is pnpm, but `vp` picks it up for you.
99
-
100
- | To… | Run |
101
- | ---------------------------------------- | ---------------------------------------------------- |
102
- | install dependencies | `vp install` |
103
- | add / remove / update a package | `vp add <pkg>` · `vp remove <pkg>` · `vp update` |
104
- | run a `package.json` script | `vpr <script>` (alias for `vp run <script>`) |
105
- | run a locally installed binary | `vp exec <bin>` (e.g. `vp exec vitops docs classes`) |
106
- | run a one-off package without installing | `vp dlx <pkg>` |
107
- | format + lint + typecheck (files) | `vp check` |
108
- | typecheck Astro (`.astro` files) | `vpr typecheck` |
109
-
110
- **Scripts that wrap Astro must go through `vpr`.** `vp dev` / `vp build` run Vite
111
- directly and skip Astro, so use `vpr dev`, `vpr build`, `vpr preview`.
112
-
113
- **`vpr dev` returns immediately.** `astro dev` now starts the server in the
114
- background. Check it with `vp exec astro dev status`, read its output with
115
- `vp exec astro dev logs --follow`, and stop it with `vp exec astro dev stop`; the
116
- site is at <http://localhost:4321>. Don't wait on it as if it were a foreground
117
- process.
118
-
119
- | Script | What it does |
120
- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
121
- | `vpr dev` | local dev |
122
- | `vpr build:css` | regenerate the framework CSS from `site.json` |
123
- | `vpr deploy` | build + `wrangler deploy` to prod |
124
- | `vpr deploy:dev` | build + deploy the isolated dev worker |
125
- | `vpr init:github` | one-time: create + push the GitHub repo (main + dev, dev default, branch protection + environment policy). Only when the user asks. |
126
- | `vpr init:hosting` | one-time: read the Cloudflare account id and set it as a repo secret. Only when the user asks. |
127
- | `vp exec vitops validate site.json` | schema-check the site config |
128
- | `vp exec vitops lint -i site.json -f css` | find framework classes in source that resolve to nothing under this format |
41
+ This is a **Vite+** project: use `vp` (and `vpr` for scripts), not `pnpm`/`npm`/`npx`. Scripts that
42
+ wrap Astro go through `vpr` (`vpr dev`, `vpr build`), because `vp dev`/`vp build` skip Astro.
43
+ `vpr dev` returns immediately (the server runs in the background): check it with
44
+ `vp exec astro dev status`, read `vp exec astro dev logs --follow`, stop with
45
+ `vp exec astro dev stop`; the site is at <http://localhost:4321>. `package.json` lists the scripts;
46
+ `vp check` and `vpr typecheck` verify a change. `vpr init:github` and `vpr init:hosting` are one-time
47
+ setup, only when the user asks.
129
48
 
130
- **Release flow.** A site is not a published package, so there is no changeset or
131
- version bump: ship by committing to `dev` (auto-deploys the dev worker) and then
132
- running the **promote** workflow. `vp` has no `vp change` / `vp pm change` command
133
- (checked against vp 1.5.0); for a _package_ it does have `vp pm version`,
134
- `vp pm publish` and `vp pm stage`.
49
+ ## Where to look
135
50
 
136
- **Naming the project.** `vp create` takes the package name from the target
137
- directory and invents a random slug when none is given, so pass one:
138
- `vp create @getvitops:static --directory <client-slug>`. `wrangler.jsonc`'s `name`
139
- is separate and still says what the template shipped — rename it to match before
140
- the first deploy.
51
+ - **Skills** are listed in the block at the top; load the one matching the task before starting.
52
+ - **Reference** for this project's own tokens, classes and config: `vp exec vitops docs`.
@@ -27,11 +27,11 @@ public/vitops/
27
27
  .DS_Store
28
28
 
29
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
30
+ # (the `vitops-` and `emdash-` prefixes are reserved for toolkit skills)
31
+ .agents/skills/vitops-*
32
+ .agents/skills/emdash-*
33
+ .claude/skills/vitops-*
34
+ .claude/skills/emdash-*
35
+ # playwright-cli: skill regenerated by `prepare`; output is per-session scratch
36
+ .claude/skills/playwright-cli
37
+ .playwright-cli/
@@ -16,7 +16,7 @@
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 && skills experimental_sync -y -a claude-code --no-frozen-lockfile && vitops agents sync -i site.json"
19
+ "prepare": "vp config && skills experimental_sync -y -a claude-code --no-frozen-lockfile && playwright cli install --skills && vitops agents sync -i site.json"
20
20
  },
21
21
  "dependencies": {
22
22
  "@getvitops/astro": "latest",
@@ -26,10 +26,11 @@
26
26
  "devDependencies": {
27
27
  "@astrojs/check": "^0.9.12",
28
28
  "@getvitops/cli": "latest",
29
+ "playwright": "^1.62.1",
29
30
  "skills": "^1.7.6",
30
31
  "typescript": "^6.0.3",
31
- "vite": "latest",
32
- "vite-plus": "latest",
32
+ "vite": "npm:@voidzero-dev/vite-plus-core@^1.5.1",
33
+ "vite-plus": "^1.5.1",
33
34
  "wrangler": "^4.111.0"
34
35
  },
35
36
  "devEngines": {
@@ -5,17 +5,12 @@ allowBuilds:
5
5
  workerd: true
6
6
  sharp: false
7
7
 
8
- # Supply-chain hardening: nothing installs within 3 days of its publish
9
- # (toolchain exempt), and no non-registry transitive sources.
8
+ # Supply-chain hardening: nothing installs within 3 days of its publish, and no non-registry
9
+ # transitive sources. Only our own lockstep `@getvitops/*` is exempt. Do NOT exempt a third-party
10
+ # toolchain: an exempt package's exact pins are not exempt (vite-plus pins `@oxc-project/*`
11
+ # exactly), so the install fails whenever one is fresher than the window. A security patch may be
12
+ # admitted with an exact `name@x.y.z` line and a dated reason; remove it once it matures.
10
13
  minimumReleaseAge: 4320
11
14
  minimumReleaseAgeExclude:
12
15
  - '@getvitops/*'
13
- - vite-plus
14
- - '@voidzero-dev/*'
15
- - oxlint
16
- - '@oxlint/*'
17
- - oxlint-tsgolint
18
- - '@oxlint-tsgolint/*'
19
- - oxfmt
20
- - '@oxfmt/*'
21
16
  blockExoticSubdeps: true