@getvitops/create 0.9.0-alpha.0 → 0.9.0-alpha.2
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 +65 -0
- package/package.json +2 -2
- package/templates/emdash/.github/workflows/deploy-prod.yml +1 -1
- package/templates/emdash/AGENTS.md +149 -317
- package/templates/emdash/README.md +2 -2
- 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 +79 -15
- package/templates/emdash/site.json +59 -2
- package/templates/emdash/src/layouts/Layout.astro +5 -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 +7 -0
- package/templates/static/_gitignore +10 -0
- package/templates/static/package.json +3 -1
- package/templates/emdash/.agents/skills/custom-blocks/SKILL.md +0 -104
|
@@ -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"
|
|
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
|
{
|
|
@@ -81,6 +82,48 @@
|
|
|
81
82
|
]
|
|
82
83
|
}
|
|
83
84
|
],
|
|
85
|
+
"widgetAreas": [
|
|
86
|
+
{
|
|
87
|
+
"name": "announcement",
|
|
88
|
+
"label": "Announcement bar",
|
|
89
|
+
"description": "Shown above the header on every page. Empty until you add a widget.",
|
|
90
|
+
"widgets": []
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"name": "pre-footer",
|
|
94
|
+
"label": "Above the footer",
|
|
95
|
+
"description": "A call to action shown above the footer on every page (Widgets in the admin).",
|
|
96
|
+
"widgets": [
|
|
97
|
+
{
|
|
98
|
+
"type": "content",
|
|
99
|
+
"title": "Ready to talk?",
|
|
100
|
+
"content": [
|
|
101
|
+
{
|
|
102
|
+
"_type": "block",
|
|
103
|
+
"_key": "pf-text",
|
|
104
|
+
"style": "normal",
|
|
105
|
+
"markDefs": [],
|
|
106
|
+
"children": [
|
|
107
|
+
{
|
|
108
|
+
"_type": "span",
|
|
109
|
+
"_key": "pf-text-s",
|
|
110
|
+
"text": "Tell us what you need and we will reply within one business day.",
|
|
111
|
+
"marks": []
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"_type": "vitops_actionLink",
|
|
117
|
+
"_key": "pf-cta",
|
|
118
|
+
"label": "Contact us",
|
|
119
|
+
"href": "/contact",
|
|
120
|
+
"variant": "cta"
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
],
|
|
84
127
|
"content": {
|
|
85
128
|
"pages": [
|
|
86
129
|
{
|
|
@@ -119,10 +162,11 @@
|
|
|
119
162
|
]
|
|
120
163
|
},
|
|
121
164
|
{
|
|
122
|
-
"_type": "
|
|
165
|
+
"_type": "vitops_hero",
|
|
123
166
|
"_key": "demo-hero",
|
|
124
167
|
"eyebrow": "Vitops blocks",
|
|
125
168
|
"heading": "Build pages from design-system patterns",
|
|
169
|
+
"level": "2",
|
|
126
170
|
"body": "Every block below renders a framework pattern, so it follows your tokens and works without JavaScript.",
|
|
127
171
|
"primaryLabel": "Open the admin",
|
|
128
172
|
"primaryHref": "/_emdash/admin",
|
|
@@ -131,7 +175,7 @@
|
|
|
131
175
|
"layout": "centered"
|
|
132
176
|
},
|
|
133
177
|
{
|
|
134
|
-
"_type": "
|
|
178
|
+
"_type": "vitops_cards",
|
|
135
179
|
"_key": "demo-cards",
|
|
136
180
|
"columns": "3",
|
|
137
181
|
"cards": [
|
|
@@ -155,7 +199,7 @@
|
|
|
155
199
|
]
|
|
156
200
|
},
|
|
157
201
|
{
|
|
158
|
-
"_type": "
|
|
202
|
+
"_type": "vitops_accordion",
|
|
159
203
|
"_key": "demo-accordion",
|
|
160
204
|
"exclusive": true,
|
|
161
205
|
"items": [
|
|
@@ -173,7 +217,7 @@
|
|
|
173
217
|
]
|
|
174
218
|
},
|
|
175
219
|
{
|
|
176
|
-
"_type": "
|
|
220
|
+
"_type": "vitops_marquee",
|
|
177
221
|
"_key": "demo-marquee",
|
|
178
222
|
"pauseOnHover": true,
|
|
179
223
|
"reverse": false,
|
|
@@ -197,14 +241,14 @@
|
|
|
197
241
|
]
|
|
198
242
|
},
|
|
199
243
|
{
|
|
200
|
-
"_type": "
|
|
244
|
+
"_type": "vitops_banner",
|
|
201
245
|
"_key": "welcome-banner",
|
|
202
246
|
"message": "This banner is a Vitops block — dismiss it, then edit this page in the EmDash admin.",
|
|
203
247
|
"tone": "info",
|
|
204
248
|
"dismissible": true
|
|
205
249
|
},
|
|
206
250
|
{
|
|
207
|
-
"_type": "
|
|
251
|
+
"_type": "vitops_details",
|
|
208
252
|
"_key": "next-steps",
|
|
209
253
|
"summary": "Next steps",
|
|
210
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`.",
|
|
@@ -245,7 +289,7 @@
|
|
|
245
289
|
"title": "Terms & Conditions",
|
|
246
290
|
"content": [
|
|
247
291
|
{
|
|
248
|
-
"_type": "
|
|
292
|
+
"_type": "vitops_banner",
|
|
249
293
|
"_key": "terms-placeholder",
|
|
250
294
|
"message": "Placeholder text — replace with your own terms before launch. This is not legal advice.",
|
|
251
295
|
"tone": "warning",
|
|
@@ -346,7 +390,7 @@
|
|
|
346
390
|
"title": "Privacy Policy",
|
|
347
391
|
"content": [
|
|
348
392
|
{
|
|
349
|
-
"_type": "
|
|
393
|
+
"_type": "vitops_banner",
|
|
350
394
|
"_key": "privacy-placeholder",
|
|
351
395
|
"message": "Placeholder text — replace with your own privacy policy before launch. This is not legal advice.",
|
|
352
396
|
"tone": "warning",
|
|
@@ -461,7 +505,7 @@
|
|
|
461
505
|
"title": "Contact",
|
|
462
506
|
"content": [
|
|
463
507
|
{
|
|
464
|
-
"_type": "
|
|
508
|
+
"_type": "vitops_banner",
|
|
465
509
|
"_key": "contact-placeholder",
|
|
466
510
|
"message": "Placeholder text — replace with your contact details before launch.",
|
|
467
511
|
"tone": "warning",
|
|
@@ -482,15 +526,35 @@
|
|
|
482
526
|
]
|
|
483
527
|
},
|
|
484
528
|
{
|
|
485
|
-
"_type": "
|
|
529
|
+
"_type": "vitops_form",
|
|
486
530
|
"_key": "contact-form",
|
|
487
531
|
"heading": "Send us a message",
|
|
488
532
|
"level": "2",
|
|
489
533
|
"fields": [
|
|
490
|
-
{
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
534
|
+
{
|
|
535
|
+
"label": "Your name",
|
|
536
|
+
"name": "name",
|
|
537
|
+
"type": "text",
|
|
538
|
+
"required": true
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
"label": "Email",
|
|
542
|
+
"name": "email",
|
|
543
|
+
"type": "email",
|
|
544
|
+
"required": true
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
"label": "Phone",
|
|
548
|
+
"name": "phone",
|
|
549
|
+
"type": "tel",
|
|
550
|
+
"required": false
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
"label": "Message",
|
|
554
|
+
"name": "message",
|
|
555
|
+
"type": "textarea",
|
|
556
|
+
"required": true
|
|
557
|
+
}
|
|
494
558
|
],
|
|
495
559
|
"submitLabel": "Send message",
|
|
496
560
|
"successMessage": "Thanks. Your message has been sent and we will reply soon."
|
|
@@ -506,7 +570,7 @@
|
|
|
506
570
|
"title": "Cookie Notice",
|
|
507
571
|
"content": [
|
|
508
572
|
{
|
|
509
|
-
"_type": "
|
|
573
|
+
"_type": "vitops_banner",
|
|
510
574
|
"_key": "cookies-placeholder",
|
|
511
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.",
|
|
512
576
|
"tone": "warning",
|
|
@@ -23,8 +23,14 @@
|
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
"roles": {
|
|
26
|
-
"neutral": {
|
|
27
|
-
|
|
26
|
+
"neutral": {
|
|
27
|
+
"hue": "ink",
|
|
28
|
+
"kind": "surface"
|
|
29
|
+
},
|
|
30
|
+
"surface": {
|
|
31
|
+
"hue": "ink",
|
|
32
|
+
"kind": "surface"
|
|
33
|
+
},
|
|
28
34
|
"ui-primary": "brand",
|
|
29
35
|
"brand-primary": "brand",
|
|
30
36
|
"info": "sky",
|
|
@@ -221,6 +227,46 @@
|
|
|
221
227
|
"size": "var(--text-m)",
|
|
222
228
|
"weight": 400,
|
|
223
229
|
"line-height": "1.55"
|
|
230
|
+
},
|
|
231
|
+
"title": {
|
|
232
|
+
"family": "display",
|
|
233
|
+
"size": "var(--text-2xl)",
|
|
234
|
+
"weight": 600,
|
|
235
|
+
"line-height": "1.15",
|
|
236
|
+
"tracking": "-0.025em",
|
|
237
|
+
"text-wrap": "balance"
|
|
238
|
+
},
|
|
239
|
+
"lead": {
|
|
240
|
+
"family": "sans",
|
|
241
|
+
"size": "var(--text-l)",
|
|
242
|
+
"weight": 400,
|
|
243
|
+
"line-height": "1.5",
|
|
244
|
+
"text-wrap": "pretty"
|
|
245
|
+
},
|
|
246
|
+
"caption": {
|
|
247
|
+
"family": "sans",
|
|
248
|
+
"size": "var(--text-s)",
|
|
249
|
+
"weight": 400,
|
|
250
|
+
"line-height": "1.3",
|
|
251
|
+
"text-wrap": "pretty",
|
|
252
|
+
"color": "var(--color-text-neutral-muted)"
|
|
253
|
+
},
|
|
254
|
+
"eyebrow": {
|
|
255
|
+
"family": "sans",
|
|
256
|
+
"size": "var(--text-s)",
|
|
257
|
+
"weight": 600,
|
|
258
|
+
"line-height": "1.3",
|
|
259
|
+
"tracking": "0.08em",
|
|
260
|
+
"text-transform": "uppercase",
|
|
261
|
+
"text-wrap": "balance"
|
|
262
|
+
},
|
|
263
|
+
"footnote": {
|
|
264
|
+
"family": "sans",
|
|
265
|
+
"size": "var(--text-xs)",
|
|
266
|
+
"weight": 400,
|
|
267
|
+
"line-height": "1.4",
|
|
268
|
+
"text-wrap": "pretty",
|
|
269
|
+
"color": "var(--color-text-neutral-muted)"
|
|
224
270
|
}
|
|
225
271
|
},
|
|
226
272
|
"headings": {
|
|
@@ -263,6 +309,17 @@
|
|
|
263
309
|
},
|
|
264
310
|
"site": {
|
|
265
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
|
+
},
|
|
266
323
|
"locales": {
|
|
267
324
|
"en": {
|
|
268
325
|
"name": "English"
|
|
@@ -7,6 +7,7 @@ import SiteFooter from '@getvitops/astro/components/SiteFooter.astro';
|
|
|
7
7
|
import SiteNav from '@getvitops/astro/components/SiteNav.astro';
|
|
8
8
|
import { loadNavMenus } from '@getvitops/astro/components/sitenav.ts';
|
|
9
9
|
import { EmDashHead } from 'emdash/ui';
|
|
10
|
+
import { VitopsWidgetArea } from '@getvitops/emdash/astro';
|
|
10
11
|
import config from '../../site.json' with { type: 'json' };
|
|
11
12
|
// Design-system CSS, layout-scoped (vitops css.inject: false): only pages
|
|
12
13
|
// rendered through this layout get it — keeps it out of /_emdash/admin, while
|
|
@@ -72,6 +73,8 @@ const orgName = config.organization?.name || siteTitle;
|
|
|
72
73
|
<VitopsHead />
|
|
73
74
|
</head>
|
|
74
75
|
<body>
|
|
76
|
+
<!-- Widget areas (Widgets in the admin): the same content wherever they appear. -->
|
|
77
|
+
<VitopsWidgetArea name="announcement" class="centered" />
|
|
75
78
|
<header class="navbar-sticky centered">
|
|
76
79
|
<div class="navbar">
|
|
77
80
|
<a class="brand flex items-center" href="/" aria-label={`${siteTitle} home`}>
|
|
@@ -89,6 +92,8 @@ const orgName = config.organization?.name || siteTitle;
|
|
|
89
92
|
<slot />
|
|
90
93
|
</main>
|
|
91
94
|
|
|
95
|
+
<VitopsWidgetArea name="pre-footer" class="centered" />
|
|
96
|
+
|
|
92
97
|
<SiteFooter class="centered" sitemap={footerSitemap} legal={footerLegal} name={orgName} />
|
|
93
98
|
|
|
94
99
|
<style>
|
|
@@ -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
|
|
@@ -70,6 +70,13 @@ activates to `script-src` / `connect-src`, any embed to `frame-src`, and hash an
|
|
|
70
70
|
inline script you add yourself. Hashes rather than nonces because a static page has no
|
|
71
71
|
per-request response to carry one.
|
|
72
72
|
|
|
73
|
+
## Skills
|
|
74
|
+
|
|
75
|
+
`@getvitops/skills` ships agent skills that update with the package. `skills experimental_sync` (run
|
|
76
|
+
by `prepare`, so by `vp install`) links them into `.agents/skills/` and `.claude/skills/`; they are
|
|
77
|
+
gitignored. The set is written for EmDash sites, so `csp` is the one that applies here. A skill you
|
|
78
|
+
write yourself with the same name is never replaced. `setup` is this site's own.
|
|
79
|
+
|
|
73
80
|
## Commands
|
|
74
81
|
|
|
75
82
|
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"
|
|
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.
|