@getvitops/create 0.8.2 → 0.9.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@
7
7
  <!--
8
8
  Opened straight from disk (file://) before anything is deployed, which is
9
9
  when most of these steps happen — so it links a relative, pre-generated
10
- stylesheet rather than the site's own build output. Run `pnpm run sop`
10
+ stylesheet rather than the site's own build output. Run `vpr sop`
11
11
  first (it's a one-line `vitops generate -f css` into .vitops/sop/); the
12
12
  banner below explains this if the stylesheet 404s, and the markup stays
13
13
  legible either way — every item here is a real checkbox in a real
@@ -21,7 +21,7 @@
21
21
  -->
22
22
  <link id="sop-stylesheet" rel="stylesheet" href=".vitops/sop/styles.css" />
23
23
  <style>
24
- /* Minimal fallback so the page is still legible before `pnpm run sop`
24
+ /* Minimal fallback so the page is still legible before `vpr sop`
25
25
  has run once — intentionally not framework CSS, since it only ever
26
26
  applies in the narrow window before the real stylesheet exists. */
27
27
  body {
@@ -42,7 +42,7 @@
42
42
  <header class="region rhythm">
43
43
  <h1>Client onboarding checklist</h1>
44
44
  <p id="no-stylesheet-notice" class="banner" hidden>
45
- This page isn't styled yet — run <code>pnpm run sop</code> once (it generates
45
+ This page isn't styled yet — run <code>vpr sop</code> once (it generates
46
46
  <code>.vitops/sop/styles.css</code>), then reload.
47
47
  </p>
48
48
  <p>
@@ -90,7 +90,7 @@
90
90
  <span class="custom-check__box"
91
91
  ><span class="custom-check__indicator custom-check__indicator--check"></span
92
92
  ></span>
93
- <span> Preview it: <code>npx vitops setup repo --dry</code> </span>
93
+ <span> Preview it: <code>vp exec vitops setup repo --dry</code> </span>
94
94
  </label>
95
95
  <label class="custom-check">
96
96
  <input type="checkbox" data-sop="repo.run" />
@@ -98,7 +98,7 @@
98
98
  ><span class="custom-check__indicator custom-check__indicator--check"></span
99
99
  ></span>
100
100
  <span>
101
- Run it: <code>npx vitops setup repo</code> — creates the GitHub repo, pushes
101
+ Run it: <code>vp exec vitops setup repo</code> — creates the GitHub repo, pushes
102
102
  <code>main</code> + <code>dev</code>, makes <code>dev</code> default, and requests
103
103
  branch protection + the <code>production</code>/<code>dev</code>
104
104
  environments.
@@ -126,7 +126,7 @@
126
126
  <span class="custom-check__box"
127
127
  ><span class="custom-check__indicator custom-check__indicator--check"></span
128
128
  ></span>
129
- <span> Preview it: <code>npx vitops setup hosting --dry</code> </span>
129
+ <span> Preview it: <code>vp exec vitops setup hosting --dry</code> </span>
130
130
  </label>
131
131
  <label class="custom-check">
132
132
  <input type="checkbox" data-sop="hosting.run" />
@@ -134,10 +134,10 @@
134
134
  ><span class="custom-check__indicator custom-check__indicator--check"></span
135
135
  ></span>
136
136
  <span>
137
- Run it: <code>npx vitops setup hosting</code> — provisions the isolated dev D1
137
+ Run it: <code>vp exec vitops setup hosting</code> — provisions the isolated dev D1
138
138
  database + R2 bucket (EmDash template) and sets the <code>DEV_D1_ID</code> /
139
139
  <code>CLOUDFLARE_ACCOUNT_ID</code> repo secrets. Requires
140
- <code>npx wrangler login</code> first.
140
+ <code>vp exec wrangler login</code> first.
141
141
  </span>
142
142
  </label>
143
143
  <label class="custom-check">
@@ -276,7 +276,7 @@
276
276
  ><span class="custom-check__indicator custom-check__indicator--check"></span
277
277
  ></span>
278
278
  <span>
279
- Deploy once (<code>pnpm deploy</code>), note the printed
279
+ Deploy once (<code>vpr deploy</code>), note the printed
280
280
  <code>*.workers.dev</code> URL, and set
281
281
  <code>site.environments.production.url</code> in <code>site.json</code> to it.
282
282
  </span>
@@ -315,8 +315,8 @@
315
315
  ><span class="custom-check__indicator custom-check__indicator--check"></span
316
316
  ></span>
317
317
  <span>
318
- Preview it: <code>npx vitops setup domain --domain &lt;apex&gt; --dry</code> — this
319
- only plans and checks; it never applies the cutover itself.
318
+ Preview it: <code>vp exec vitops setup domain --domain &lt;apex&gt; --dry</code> —
319
+ this only plans and checks; it never applies the cutover itself.
320
320
  </span>
321
321
  </label>
322
322
  <label class="custom-check">
@@ -326,7 +326,7 @@
326
326
  ></span>
327
327
  <span>
328
328
  Uncomment the <code>routes</code> block in <code>wrangler.jsonc</code>, set your
329
- apex, and deploy: <code>pnpm deploy</code>.
329
+ apex, and deploy: <code>vpr deploy</code>.
330
330
  </span>
331
331
  </label>
332
332
  <label class="custom-check">
@@ -347,7 +347,8 @@
347
347
  ><span class="custom-check__indicator custom-check__indicator--check"></span
348
348
  ></span>
349
349
  <span>
350
- Re-run <code>npx vitops robots</code> and <code>npx vitops search notify</code>.
350
+ Re-run <code>vp exec vitops robots</code> and
351
+ <code>vp exec vitops search notify</code>.
351
352
  </span>
352
353
  </label>
353
354
  </div>
@@ -373,7 +374,7 @@
373
374
  <span class="custom-check__box"
374
375
  ><span class="custom-check__indicator custom-check__indicator--check"></span
375
376
  ></span>
376
- <span> Check it: <code>npx vitops validate site.json</code> </span>
377
+ <span> Check it: <code>vp exec vitops validate site.json</code> </span>
377
378
  </label>
378
379
  </div>
379
380
  </details>
@@ -398,8 +399,8 @@
398
399
  ><span class="custom-check__indicator custom-check__indicator--check"></span
399
400
  ></span>
400
401
  <span>
401
- Preview then run: <code>npx vitops setup search --dry</code>, then
402
- <code>npx vitops setup search</code>. This is a <strong>user OAuth</strong>
402
+ Preview then run: <code>vp exec vitops setup search --dry</code>, then
403
+ <code>vp exec vitops setup search</code>. This is a <strong>user OAuth</strong>
403
404
  action, deliberately — verifying a site makes the caller an owner, and that should
404
405
  be a person, not a deploy robot.
405
406
  </span>
@@ -410,8 +411,25 @@
410
411
  ><span class="custom-check__indicator custom-check__indicator--check"></span
411
412
  ></span>
412
413
  <span>
413
- Preview then run: <code>npx vitops setup ads --dry</code>, then
414
- <code>npx vitops setup ads</code>.
414
+ Preview then run: <code>vp exec vitops setup ads --dry</code>, then
415
+ <code>vp exec vitops setup ads</code>.
416
+ </span>
417
+ </label>
418
+ <label class="custom-check">
419
+ <input type="checkbox" data-sop="connections.turnstile" />
420
+ <span class="custom-check__box"
421
+ ><span class="custom-check__indicator custom-check__indicator--check"></span
422
+ ></span>
423
+ <span>
424
+ If the site has a contact form, create its spam check:
425
+ <code>vp exec vitops setup turnstile --dry</code>, then
426
+ <code>vp exec vitops setup turnstile</code>. It needs
427
+ <code>CLOUDFLARE_API_TOKEN</code> with
428
+ <strong>Account &gt; Turnstile &gt; Edit</strong> (plus
429
+ <code>CLOUDFLARE_ACCOUNT_ID</code>) and <code>wrangler login</code>. The public site
430
+ key goes into <code>site.security.turnstile.siteKey</code>; the secret is saved as
431
+ the Worker secret <code>VITOPS_TURNSTILE_SECRET</code> and never touches
432
+ <code>site.json</code>. It never edits or deletes an existing widget.
415
433
  </span>
416
434
  </label>
417
435
  <label class="custom-check">
@@ -453,10 +471,10 @@
453
471
  ></span>
454
472
  <span>
455
473
  Generate real documents from the filled-in config —
456
- <code>npx vitops legal --format portable-text</code> (EmDash; paste into the admin)
457
- or <code>npx vitops legal --format md</code> (static; commit the output). The seeded
458
- <code>/terms</code> and <code>/privacy</code> pages are deliberately short,
459
- obviously-unfinished placeholders.
474
+ <code>vp exec vitops legal --format portable-text</code> (EmDash; paste into the
475
+ admin) or <code>vp exec vitops legal --format md</code> (static; commit the output).
476
+ The seeded <code>/terms</code> and <code>/privacy</code> pages are deliberately
477
+ short, obviously-unfinished placeholders.
460
478
  </span>
461
479
  </label>
462
480
  <label class="custom-check">
@@ -484,7 +502,7 @@
484
502
  ></span>
485
503
  <span>
486
504
  Robots directive matches the live origin's intent (blocked while on workers.dev,
487
- open after cutover) — <code>npx vitops robots</code>.
505
+ open after cutover) — <code>vp exec vitops robots</code>.
488
506
  </span>
489
507
  </label>
490
508
  <label class="custom-check">
@@ -509,7 +527,7 @@
509
527
  ><span class="custom-check__indicator custom-check__indicator--check"></span
510
528
  ></span>
511
529
  <span>
512
- <code>npx vitops lint --src</code> and <code>pnpm typecheck</code> both clean.
530
+ <code>vp exec vitops lint --src</code> and <code>vpr typecheck</code> both clean.
513
531
  </span>
514
532
  </label>
515
533
  <label class="custom-check">
@@ -527,7 +545,7 @@
527
545
  <span class="custom-check__box"
528
546
  ><span class="custom-check__indicator custom-check__indicator--check"></span
529
547
  ></span>
530
- <span> <code>npx vitops search notify</code> run once live. </span>
548
+ <span> <code>vp exec vitops search notify</code> run once live. </span>
531
549
  </label>
532
550
  <label class="custom-check">
533
551
  <input type="checkbox" data-sop="launch.check" />
@@ -535,8 +553,8 @@
535
553
  ><span class="custom-check__indicator custom-check__indicator--check"></span
536
554
  ></span>
537
555
  <span>
538
- A day or two later: <code>npx vitops search notify --check</code> confirms Google
539
- actually indexed it.
556
+ A day or two later: <code>vp exec vitops search notify --check</code> confirms
557
+ Google actually indexed it.
540
558
  </span>
541
559
  </label>
542
560
  </div>
@@ -12,6 +12,9 @@ worker-configuration.d.ts
12
12
  # wrangler local state (local D1/R2 data lives here during `astro dev`)
13
13
  .wrangler/
14
14
 
15
+ # production content dump `scripts/pull-content.mjs` loads into the local build DB
16
+ .vitops/content.sql
17
+
15
18
  # node hosting target: SQLite db + local uploads
16
19
  data/
17
20
 
@@ -23,6 +23,15 @@ const organizationName = resolveOrganizationName(config) ?? 'My EmDash Site';
23
23
  // data migration, not a rewrite — see README "Hosting".
24
24
  const { adapter, database, storage } = await vitopsHosting();
25
25
 
26
+ // Favicon/PWA colours come from site.json rather than a second copy here that
27
+ // drifts when the palette changes: the browser-UI colour is the `brand-primary`
28
+ // role's hue seed. (The page background behind the maskable icon is a computed
29
+ // ramp step, not a config value, so `backgroundColor` below stays a literal.)
30
+ const colors = config.designSystem?.themes?.default?.colors ?? {};
31
+ const roleHue = (role) =>
32
+ typeof colors.roles?.[role] === 'string' ? colors.roles[role] : colors.roles?.[role]?.hue;
33
+ const brandColor = colors.palette?.[roleHue('brand-primary')]?.seed ?? '#2e9b73';
34
+
26
35
  export default defineConfig({
27
36
  output: 'server',
28
37
  adapter,
@@ -79,7 +88,7 @@ export default defineConfig({
79
88
  favicon: {
80
89
  source: 'src/assets/logo.svg',
81
90
  name: organizationName,
82
- themeColor: '#2e9b73',
91
+ themeColor: brandColor,
83
92
  backgroundColor: '#ffffff',
84
93
  },
85
94
  // inject: false — Layout.astro imports the generated stylesheet, so
@@ -91,6 +100,10 @@ export default defineConfig({
91
100
  // there instead of a bare design-system.json. legal/fonts/search/ads/
92
101
  // reviews all read the same file (see AGENTS.md's "Client onboarding").
93
102
  css: { format: 'tailwind', inject: false, input: 'site.json' },
103
+ // The /design-system admin page embeds public/vitops/design-system-editor.html in an
104
+ // iframe; this writes it, with its manifest, editor.js and editor.css, without loading
105
+ // the editor on public pages.
106
+ editorManifest: true,
94
107
  site: { input: 'site.json' },
95
108
  }),
96
109
  ],
@@ -5,10 +5,12 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "astro dev",
8
- "build": "astro build",
8
+ "build": "astro build && vitops emdash check-prerender",
9
9
  "preview": "astro preview",
10
- "deploy": "vitops email check && astro build && emdash migrate --wrangler-config wrangler.jsonc && wrangler deploy",
11
- "deploy:dev": "vitops email check && astro build && node scripts/build-dev-wrangler.mjs && emdash migrate --wrangler-config dist/server/wrangler.dev.json && wrangler deploy --config dist/server/wrangler.dev.json",
10
+ "pull-content": "node scripts/pull-content.mjs",
11
+ "prepare-local-db": "vitops emdash prepare-db",
12
+ "deploy": "vitops email check && node scripts/pull-content.mjs && vitops emdash prepare-db && astro build && vitops emdash check-prerender && emdash migrate --wrangler-config wrangler.jsonc && wrangler deploy",
13
+ "deploy:dev": "vitops email check && node scripts/pull-content.mjs && vitops emdash prepare-db && astro build && vitops emdash check-prerender && node scripts/build-dev-wrangler.mjs && emdash migrate --wrangler-config dist/server/wrangler.dev.json && wrangler deploy --config dist/server/wrangler.dev.json",
12
14
  "init:github": "vitops setup repo",
13
15
  "init:hosting": "vitops setup hosting",
14
16
  "setup": "vitops setup all",
@@ -21,13 +23,13 @@
21
23
  "dependencies": {
22
24
  "@astrojs/cloudflare": "^14.0.0",
23
25
  "@astrojs/react": "^7.1.2",
24
- "@emdash-cms/cloudflare": "^1.4.0",
26
+ "@emdash-cms/cloudflare": "latest",
25
27
  "@getvitops/astro": "latest",
26
28
  "@getvitops/core": "latest",
27
29
  "@getvitops/emdash": "latest",
28
30
  "@tailwindcss/vite": "^4.3.3",
29
31
  "astro": "^7.4.4",
30
- "emdash": "^1.4.0",
32
+ "emdash": "latest",
31
33
  "react": "^19.3.1",
32
34
  "react-dom": "^19.3.1",
33
35
  "tailwindcss": "^4.3.3"
@@ -1,9 +1,24 @@
1
- # Cloudflare Workers static assets apply this file to every asset response.
1
+ # Cloudflare static-assets response headers. They apply to files served from the
2
+ # static asset store — the prerendered pages — and NOT to responses the Worker
3
+ # renders itself (the EmDash admin at /_emdash/*, its API, anything left
4
+ # on-demand). See AGENTS.md "Content Security Policy".
2
5
  #
3
- # A baseline that cannot break a page. No Content-Security-Policy on purpose: <Head /> emits
4
- # inline scripts (the consent stub, the colour-scheme bootstrap), so a strict policy needs
5
- # per-site nonces or hashes — add one deliberately once you know which third-party tags you load.
6
+ # REPORT-ONLY on purpose: violations are logged to the browser console and
7
+ # nothing is blocked, so this cannot break a page. Read the console on the
8
+ # pages you ship, add what is legitimately needed, then rename the header to
9
+ # `Content-Security-Policy` to enforce it.
10
+ #
11
+ # The two sha256 values are the inline scripts <Head /> emits (the colour-scheme
12
+ # bootstrap and the consent-gate stub). They change only when
13
+ # @getvitops/astro changes those scripts; regenerate with
14
+ # vp exec node -e "import('@getvitops/astro').then(m => console.log(m.contentSecurityPolicy()))"
15
+ # `templates.test.ts` in the toolchain repo guards that they match.
16
+ #
17
+ # Expect reports for scripts this policy cannot name by hash: EmDash's own head
18
+ # tags, a server-island loader, an editor's HTML block, and third-party tags the
19
+ # consent gate activates (allow their origins in script-src/connect-src).
6
20
  /*
21
+ Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'sha256-CEiVlTo192T9Rug/nAl+ihvkqOQkYdr6Rq3oc+gR9JU=' 'sha256-z/UqsITVvNGOJSaOSvhN1rdtyYdMIxpQ/lMXM1q0g90=' https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-src https://challenges.cloudflare.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'
7
22
  X-Content-Type-Options: nosniff
8
23
  Referrer-Policy: strict-origin-when-cross-origin
9
24
  Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
@@ -0,0 +1,83 @@
1
+ // Pre-build step: load production's EmDash content into the LOCAL D1 the build
2
+ // prerenders from.
3
+ //
4
+ // Why this exists: `src/pages/index.astro` and `[...slug].astro` are prerendered
5
+ // (`prerender = true` + `getStaticPaths`), so their HTML is written during
6
+ // `astro build`. The Cloudflare adapter prerenders inside a local workerd with a
7
+ // LOCAL D1 (`.wrangler/state`), never the deployed database — so on a CI runner
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.md), so this
10
+ // copies production's D1 into the local one first.
11
+ //
12
+ // wrangler d1 export <db> --remote → .vitops/content.sql
13
+ // wrangler d1 execute <db> --local ← .vitops/content.sql (state cleared first)
14
+ //
15
+ // Read-only against production (an export). It only ever writes the local
16
+ // `.wrangler/state/v3/d1` directory, which is disposable build state.
17
+ //
18
+ // Fails CLOSED. A deploy that proceeds after a failed pull would publish an empty
19
+ // site over a good one, so any failure aborts the deploy — except one case that is
20
+ // not a failure: the remote database does not exist yet (the first deploy, where
21
+ // wrangler provisions it). Building from the seed is only right once the local database is
22
+ // migrated and seeded, which is `scripts/prepare-local-db.mjs`'s job, run straight after this.
23
+ //
24
+ // Skip with SKIP_PULL_CONTENT=1 (e.g. a deliberate seed-only build).
25
+
26
+ import { execFileSync, spawnSync } from 'node:child_process';
27
+ import { mkdirSync, readFileSync, rmSync } from 'node:fs';
28
+
29
+ if (process.env.SKIP_PULL_CONTENT) {
30
+ console.log('pull-content: SKIP_PULL_CONTENT set — prerendering from the local/seed database.');
31
+ process.exit(0);
32
+ }
33
+
34
+ /** wrangler.jsonc allows comments and trailing commas; JSON.parse does not. */
35
+ function parseJsonc(text) {
36
+ const out = text
37
+ .replace(/("(?:[^"\\]|\\.)*")|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, (m, str) => str ?? '')
38
+ .replace(/,(\s*[}\]])/g, '$1');
39
+ return JSON.parse(out);
40
+ }
41
+
42
+ const config = parseJsonc(readFileSync('wrangler.jsonc', 'utf8'));
43
+ const db = config.d1_databases?.[0]?.database_name;
44
+ if (!db) {
45
+ console.error('pull-content: wrangler.jsonc has no d1_databases[0].database_name.');
46
+ process.exit(1);
47
+ }
48
+
49
+ const dump = '.vitops/content.sql';
50
+ mkdirSync('.vitops', { recursive: true });
51
+
52
+ const wrangler = (args) =>
53
+ spawnSync('wrangler', args, { encoding: 'utf8', env: { ...process.env, CI: '1' } });
54
+
55
+ const exported = wrangler([
56
+ 'd1',
57
+ 'export',
58
+ db,
59
+ '--remote',
60
+ '--output',
61
+ dump,
62
+ '--skip-confirmation',
63
+ ]);
64
+ if (exported.status !== 0) {
65
+ const text = `${exported.stdout}\n${exported.stderr}`;
66
+ if (/not found|does not exist|no such database|couldn'?t find|could not find|7404/i.test(text)) {
67
+ console.warn(
68
+ `pull-content: remote database "${db}" does not exist yet (first deploy?) — ` +
69
+ 'prerendering from the seed.',
70
+ );
71
+ process.exit(0);
72
+ }
73
+ console.error(`pull-content: could not export "${db}" from production:\n${text}`);
74
+ console.error('Refusing to build — prerendering now would publish an empty site.');
75
+ process.exit(1);
76
+ }
77
+
78
+ // Replaying onto a populated local database collides on CREATE TABLE, so start clean.
79
+ rmSync('.wrangler/state/v3/d1', { recursive: true, force: true });
80
+ execFileSync('wrangler', ['d1', 'execute', db, '--local', '--file', dump, '--yes'], {
81
+ stdio: 'inherit',
82
+ });
83
+ console.log(`pull-content: loaded production "${db}" into the local build database.`);
@@ -35,6 +35,17 @@
35
35
  {
36
36
  "name": "primary",
37
37
  "label": "Primary Navigation",
38
+ "items": [
39
+ {
40
+ "type": "custom",
41
+ "label": "Contact",
42
+ "url": "/contact"
43
+ }
44
+ ]
45
+ },
46
+ {
47
+ "name": "footer-sitemap",
48
+ "label": "Footer: Site Map",
38
49
  "items": [
39
50
  {
40
51
  "type": "custom",
@@ -43,15 +54,20 @@
43
54
  },
44
55
  {
45
56
  "type": "custom",
46
- "label": "About",
47
- "url": "/about"
57
+ "label": "Contact",
58
+ "url": "/contact"
48
59
  }
49
60
  ]
50
61
  },
51
62
  {
52
- "name": "footer",
53
- "label": "Footer",
63
+ "name": "footer-legal",
64
+ "label": "Footer: Legal",
54
65
  "items": [
66
+ {
67
+ "type": "custom",
68
+ "label": "Privacy Policy",
69
+ "url": "/privacy"
70
+ },
55
71
  {
56
72
  "type": "custom",
57
73
  "label": "Terms & Conditions",
@@ -59,8 +75,50 @@
59
75
  },
60
76
  {
61
77
  "type": "custom",
62
- "label": "Privacy Policy",
63
- "url": "/privacy"
78
+ "label": "Cookie Notice",
79
+ "url": "/cookies"
80
+ }
81
+ ]
82
+ }
83
+ ],
84
+ "widgetAreas": [
85
+ {
86
+ "name": "announcement",
87
+ "label": "Announcement bar",
88
+ "description": "Shown above the header on every page. Empty until you add a widget.",
89
+ "widgets": []
90
+ },
91
+ {
92
+ "name": "pre-footer",
93
+ "label": "Above the footer",
94
+ "description": "A call to action shown above the footer on every page (Widgets in the admin).",
95
+ "widgets": [
96
+ {
97
+ "type": "content",
98
+ "title": "Ready to talk?",
99
+ "content": [
100
+ {
101
+ "_type": "block",
102
+ "_key": "pf-text",
103
+ "style": "normal",
104
+ "markDefs": [],
105
+ "children": [
106
+ {
107
+ "_type": "span",
108
+ "_key": "pf-text-s",
109
+ "text": "Tell us what you need and we will reply within one business day.",
110
+ "marks": []
111
+ }
112
+ ]
113
+ },
114
+ {
115
+ "_type": "vitops.actionLink",
116
+ "_key": "pf-cta",
117
+ "label": "Contact us",
118
+ "href": "/contact",
119
+ "variant": "cta"
120
+ }
121
+ ]
64
122
  }
65
123
  ]
66
124
  }
@@ -436,6 +494,130 @@
436
494
  }
437
495
  ]
438
496
  }
497
+ },
498
+ {
499
+ "id": "contact",
500
+ "slug": "contact",
501
+ "status": "published",
502
+ "data": {
503
+ "title": "Contact",
504
+ "content": [
505
+ {
506
+ "_type": "vitops.banner",
507
+ "_key": "contact-placeholder",
508
+ "message": "Placeholder text — replace with your contact details before launch.",
509
+ "tone": "warning",
510
+ "dismissible": false
511
+ },
512
+ {
513
+ "_type": "block",
514
+ "_key": "contact-body",
515
+ "style": "normal",
516
+ "markDefs": [],
517
+ "children": [
518
+ {
519
+ "_type": "span",
520
+ "_key": "contact-body-1",
521
+ "text": "Tell visitors how to reach you: an email address, a phone number, a street address or a form. This page exists so the Contact link in the header and footer resolves; edit it in the EmDash admin.",
522
+ "marks": []
523
+ }
524
+ ]
525
+ },
526
+ {
527
+ "_type": "vitops.form",
528
+ "_key": "contact-form",
529
+ "heading": "Send us a message",
530
+ "level": "2",
531
+ "fields": [
532
+ {
533
+ "label": "Your name",
534
+ "name": "name",
535
+ "type": "text",
536
+ "required": true
537
+ },
538
+ {
539
+ "label": "Email",
540
+ "name": "email",
541
+ "type": "email",
542
+ "required": true
543
+ },
544
+ {
545
+ "label": "Phone",
546
+ "name": "phone",
547
+ "type": "tel",
548
+ "required": false
549
+ },
550
+ {
551
+ "label": "Message",
552
+ "name": "message",
553
+ "type": "textarea",
554
+ "required": true
555
+ }
556
+ ],
557
+ "submitLabel": "Send message",
558
+ "successMessage": "Thanks. Your message has been sent and we will reply soon."
559
+ }
560
+ ]
561
+ }
562
+ },
563
+ {
564
+ "id": "cookies",
565
+ "slug": "cookies",
566
+ "status": "published",
567
+ "data": {
568
+ "title": "Cookie Notice",
569
+ "content": [
570
+ {
571
+ "_type": "vitops.banner",
572
+ "_key": "cookies-placeholder",
573
+ "message": "Placeholder text — replace with your own cookie notice before launch. This is not legal advice. `vitops legal` can generate a draft from site.json.",
574
+ "tone": "warning",
575
+ "dismissible": false
576
+ },
577
+ {
578
+ "_type": "block",
579
+ "_key": "cookies-intro",
580
+ "style": "normal",
581
+ "markDefs": [],
582
+ "children": [
583
+ {
584
+ "_type": "span",
585
+ "_key": "cookies-intro-1",
586
+ "text": "This site uses a small number of cookies. Essential cookies keep the site working; others are only set if you choose to allow them.",
587
+ "marks": []
588
+ }
589
+ ]
590
+ },
591
+ {
592
+ "_type": "block",
593
+ "_key": "cookies-h",
594
+ "style": "h2",
595
+ "markDefs": [],
596
+ "children": [
597
+ {
598
+ "_type": "span",
599
+ "_key": "cookies-h-1",
600
+ "text": "What cookies we use",
601
+ "marks": []
602
+ }
603
+ ]
604
+ },
605
+ {
606
+ "_type": "block",
607
+ "_key": "cookies-body",
608
+ "style": "normal",
609
+ "markDefs": [],
610
+ "children": [
611
+ {
612
+ "_type": "span",
613
+ "_key": "cookies-body-1",
614
+ "text": "List each cookie, who sets it and why. Replace this paragraph with the real list before launch.",
615
+ "marks": []
616
+ }
617
+ ]
618
+ }
619
+ ]
620
+ }
439
621
  }
440
622
  ]
441
623
  }