@cparkerwebm/webmonterey 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/LICENSE +21 -0
  3. package/README.md +104 -0
  4. package/agents/.gitkeep +0 -0
  5. package/dist/webm.mjs +2381 -0
  6. package/hooks/.gitkeep +0 -0
  7. package/package.json +101 -0
  8. package/schema/design.json +118 -0
  9. package/skills/launch/SKILL.md +183 -0
  10. package/skills/new-component/SKILL.md +85 -0
  11. package/skills/start/SKILL.md +117 -0
  12. package/skills/traps/SKILL.md +333 -0
  13. package/skills/upgrade/SKILL.md +42 -0
  14. package/src/actions/index.ts +348 -0
  15. package/src/cli/checks.test.ts +711 -0
  16. package/src/cli/checks.ts +822 -0
  17. package/src/cli/codemods.test.ts +42 -0
  18. package/src/cli/codemods.ts +51 -0
  19. package/src/cli/compare.test.ts +144 -0
  20. package/src/cli/compare.ts +222 -0
  21. package/src/cli/design-extract.test.ts +96 -0
  22. package/src/cli/design-extract.ts +229 -0
  23. package/src/cli/doctor.ts +187 -0
  24. package/src/cli/mcp.test.ts +57 -0
  25. package/src/cli/mcp.ts +110 -0
  26. package/src/cli/new.ts +148 -0
  27. package/src/cli/package-root.ts +58 -0
  28. package/src/cli/scaffold.test.ts +230 -0
  29. package/src/cli/scaffold.ts +424 -0
  30. package/src/cli/seed.ts +133 -0
  31. package/src/cli/slug.test.ts +70 -0
  32. package/src/cli/slug.ts +109 -0
  33. package/src/cli/sync.test.ts +137 -0
  34. package/src/cli/sync.ts +266 -0
  35. package/src/cli/upgrade.ts +93 -0
  36. package/src/design/__fixtures__/tokens-v1.4.1.css +227 -0
  37. package/src/design/brand.ts +49 -0
  38. package/src/design/compile.test.ts +98 -0
  39. package/src/design/compile.ts +155 -0
  40. package/src/design/defaults.ts +315 -0
  41. package/src/design/index.ts +18 -0
  42. package/src/design/resolve.test.ts +80 -0
  43. package/src/design/resolve.ts +108 -0
  44. package/src/design/types.ts +96 -0
  45. package/src/emails/autoresponse.test.ts +82 -0
  46. package/src/emails/autoresponse.ts +83 -0
  47. package/src/emails/footer.test.ts +102 -0
  48. package/src/emails/footer.ts +91 -0
  49. package/src/emails/index.ts +24 -0
  50. package/src/emails/subject.test.ts +66 -0
  51. package/src/emails/subject.ts +80 -0
  52. package/src/emails/submission-notification.ts +76 -0
  53. package/src/env.d.ts +12 -0
  54. package/src/includes/cloudflare/d1/client.ts +86 -0
  55. package/src/includes/cloudflare/r2/README.md +68 -0
  56. package/src/includes/cloudflare/r2/media.ts +41 -0
  57. package/src/includes/cloudflare/r2/url.test.ts +44 -0
  58. package/src/includes/cloudflare/r2/url.ts +34 -0
  59. package/src/includes/cloudflare/turnstile/Turnstile.astro +161 -0
  60. package/src/includes/cloudflare/turnstile/verify.ts +157 -0
  61. package/src/includes/cloudflare/workers/env.ts +56 -0
  62. package/src/includes/google/tag-manager/TagManager.astro +73 -0
  63. package/src/includes/sinch/mailgun/redirect.test.ts +96 -0
  64. package/src/includes/sinch/mailgun/redirect.ts +96 -0
  65. package/src/includes/sinch/mailgun/send.ts +186 -0
  66. package/src/includes/webmonterey/animations/animations.css +347 -0
  67. package/src/includes/webmonterey/animations/observe.test.ts +88 -0
  68. package/src/includes/webmonterey/animations/observe.ts +209 -0
  69. package/src/includes/webmonterey/compliance/ConsentInit.astro +74 -0
  70. package/src/includes/webmonterey/compliance/CookieConsent.astro +558 -0
  71. package/src/includes/webmonterey/compliance/consent-styles.test.ts +83 -0
  72. package/src/includes/webmonterey/compliance/consent.ts +142 -0
  73. package/src/includes/webmonterey/config.test.ts +94 -0
  74. package/src/includes/webmonterey/config.ts +346 -0
  75. package/src/includes/webmonterey/copy-defaults.ts +148 -0
  76. package/src/includes/webmonterey/copy.ts +13 -0
  77. package/src/includes/webmonterey/credits/Credit.astro +80 -0
  78. package/src/includes/webmonterey/credits/credit.test.ts +111 -0
  79. package/src/includes/webmonterey/credits/credit.ts +59 -0
  80. package/src/includes/webmonterey/forms/honeypot.test.ts +40 -0
  81. package/src/includes/webmonterey/forms/honeypot.ts +66 -0
  82. package/src/includes/webmonterey/prose/inline.test.ts +94 -0
  83. package/src/includes/webmonterey/prose/inline.ts +71 -0
  84. package/src/includes/webmonterey/scroll-top/ScrollTop.astro +209 -0
  85. package/src/includes/webmonterey/site.ts +136 -0
  86. package/src/includes/webmonterey/structured-data/nodes.ts +315 -0
  87. package/src/includes/webmonterey/zoned-hour.test.ts +49 -0
  88. package/src/integration/adapter.ts +53 -0
  89. package/src/integration/app-middleware.ts +43 -0
  90. package/src/integration/config.ts +96 -0
  91. package/src/integration/content.ts +66 -0
  92. package/src/integration/image-size.test.ts +83 -0
  93. package/src/integration/image-size.ts +100 -0
  94. package/src/integration/index.ts +386 -0
  95. package/src/integration/virtual.d.ts +101 -0
  96. package/src/layouts/base.astro +402 -0
  97. package/src/package.test.ts +121 -0
  98. package/src/pages/404.astro +33 -0
  99. package/src/pages/[...slug].astro +126 -0
  100. package/src/pages/robots.txt.ts +41 -0
  101. package/src/pages/webm.astro +23 -0
  102. package/src/scripts/.gitkeep +0 -0
  103. package/src/styles/base.css +185 -0
  104. package/src/styles/global.css +47 -0
  105. package/src/styles/layers.test.ts +32 -0
  106. package/src/styles/layers.ts +21 -0
  107. package/src/styles/layout.css +235 -0
  108. package/src/styles/reset.css +135 -0
  109. package/src/styles/utilities.css +90 -0
  110. package/src/worker.ts +76 -0
  111. package/template/assets/logo.svg +14 -0
  112. package/template/assets/open-graph.png +0 -0
  113. package/template/migrations/0001_create_submissions.sql +40 -0
  114. package/template/migrations/README.md +41 -0
  115. package/template/public/android-chrome-192x192.png +0 -0
  116. package/template/public/android-chrome-512x512.png +0 -0
  117. package/template/public/apple-touch-icon.png +0 -0
  118. package/template/public/favicon-16x16.png +0 -0
  119. package/template/public/favicon-32x32.png +0 -0
  120. package/template/public/favicon.ico +0 -0
  121. package/template/public/favicon.svg +4 -0
  122. package/template/public/open-graph.png +0 -0
  123. package/template/scripts/check-node.mjs +72 -0
  124. package/template/scripts/test-hooks.mjs +96 -0
  125. package/template/site/.editorconfig +22 -0
  126. package/template/site/.prettierignore +15 -0
  127. package/template/site/.prettierrc.json +17 -0
  128. package/template/site/CLAUDE.md +245 -0
  129. package/template/site/CONTENT.md +135 -0
  130. package/template/site/PRIVACY-POLICY-DRAFT.md +81 -0
  131. package/template/site/public/_headers +70 -0
  132. package/template/site/src/forms/contact.json +45 -0
  133. package/template/workflows/ci.yml +52 -0
@@ -0,0 +1,17 @@
1
+ {
2
+ "printWidth": 100,
3
+ "singleQuote": true,
4
+ "semi": true,
5
+ "trailingComma": "all",
6
+ "plugins": ["prettier-plugin-astro"],
7
+ "overrides": [
8
+ {
9
+ "files": "*.astro",
10
+ "options": { "parser": "astro" }
11
+ },
12
+ {
13
+ "files": "*.jsonc",
14
+ "options": { "parser": "jsonc", "trailingComma": "none" }
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,245 @@
1
+ # CLAUDE.md
2
+
3
+ This is a **WebMonterey client site**. It is a thin repo: the framework lives in the
4
+ `@cparkerwebm/webmonterey` package, and what is here is what makes this client's site
5
+ different from every other one.
6
+
7
+ The relationship is a WordPress parent theme and child theme.
8
+
9
+ | | Parent (the package) | Child (this repo) |
10
+ | ------------- | -------------------------------------------------------- | ---------------------------------- |
11
+ | Owns | layouts, styles, form pipeline, emails, routes, includes | components, content, palette, copy |
12
+ | Changes by | `npm update @cparkerwebm/webmonterey` | editing files here |
13
+ | Overridden by | this repo, always | — |
14
+
15
+ **A fix to the package reaches this site on `npm update`.** That is the entire point of the
16
+ package existing, and it is why the default answer to "the shared behavior is wrong" is to fix
17
+ it upstream rather than to work around it here.
18
+
19
+ ## The one rule that protects that
20
+
21
+ **Prefer overriding to editing around.** When something in the package is not right for this
22
+ client, express it as an override in this repo — a token in `design.json`, a rule in
23
+ `src/styles/custom/`, a wrapper in `src/actions/index.ts`. Do not reach into `node_modules` and
24
+ do not copy a package file here so you can change two lines: a copy stops receiving fixes the
25
+ moment it is made, silently, and nothing will ever tell you.
26
+
27
+ Copying a package file here is legitimate exactly once: when this client genuinely needs
28
+ different behavior, not a different value. Say so in a comment at the top of the copy, naming
29
+ what it forked from and why, so the next person knows it is deliberate.
30
+
31
+ ## Commands
32
+
33
+ | Command | Runs |
34
+ | ----------------- | ---------------------------------------------------------- |
35
+ | `npm run dev` | dev server — fast, and blind to half the things that break |
36
+ | `npm run preview` | **a real build on real workerd** |
37
+ | `npm run check` | types and content schema |
38
+ | `npx webm doctor` | the things that fail silently |
39
+ | `npm run format` | prettier |
40
+
41
+ **Run `npm run preview`, not `dev`, before merging anything** that touches styles,
42
+ `wrangler.jsonc`, a route's `prerender` flag, or the form pipeline. `dev` cannot see CSS
43
+ bundling, cascade order, or the asset router. Every trap in `/webm:traps` was invisible in
44
+ `dev` and visible in `preview`.
45
+
46
+ ## Hard rules
47
+
48
+ ### 1. Vanilla CSS and vanilla JS only
49
+
50
+ **Banned outright:** Tailwind, Bootstrap, Sass/Less/Stylus, CSS-in-JS, PostCSS plugin chains,
51
+ React, Vue, Svelte, Solid, Preact, Alpine, htmx, jQuery, Lodash, and any UI or utility
52
+ framework.
53
+
54
+ **Allowed:** plain `.css` using `@layer` and custom properties, plain `.js`/`.ts` modules,
55
+ `.astro` components, and `zod` (build-time validation only, never shipped).
56
+
57
+ TypeScript is allowed and expected — the rule targets frameworks and preprocessors, not types.
58
+
59
+ Do not add a dependency to solve something CSS or the web platform already does.
60
+
61
+ ### 2. Style through tokens, and set tokens in `design.json`
62
+
63
+ Every color, space, size, radius, shadow, z-index, duration and easing is a `--webm-*` custom
64
+ property. Component CSS contains **no literal values** for these.
65
+
66
+ The palette lives in [design.json](design.json), which compiles to the token layer at build
67
+ time. Retheming this client is editing that file — one value cascades everywhere.
68
+
69
+ For a rule rather than a value, [src/styles/custom/](src/styles/custom/) is the seam:
70
+ `webm.components.custom` beats `webm.components.core` at identical specificity, so an override
71
+ lands without `!important` and stays legible as an override.
72
+
73
+ **Do not edit the package's stylesheets.** That is the change from generation 2, where a client
74
+ repo was a copy of the starter and editing it in place was correct. Here it is a copy-forward
75
+ that stops receiving fixes.
76
+
77
+ ### 3. Never restyle for accessibility unless asked
78
+
79
+ A client's colors, contrast, focus rings, type sizes and spacing are **configured**, not
80
+ accidental. Do not "fix" any of them because an audit or a WCAG level says they fall short —
81
+ including darkening a color, enlarging text, or swapping a token as a side effect of unrelated
82
+ work.
83
+
84
+ **Report it, do not change it.** State the measured number and the element, then wait:
85
+
86
+ > The secondary consent buttons are 1.19:1 against the card. Want me to change that?
87
+
88
+ Two things this is not. It does not license shipping markup that is broken rather than merely
89
+ low-contrast — semantics, labels, roles, keyboard operability and focus ORDER are correctness,
90
+ and they get fixed like any other bug. And it does not apply once you have been asked: a
91
+ request to fix a contrast problem is the permission, for that problem.
92
+
93
+ A client signs off on a palette. A change made on our own initiative between one deploy and the
94
+ next is a change they never approved and will not think to look for.
95
+
96
+ ### 4. Page content is JSON, never hardcoded
97
+
98
+ Copy, headings, links and block ordering live in `src/content/pages/*.json` — one file per
99
+ page. Never hardcode client copy into an `.astro` file. If content cannot be expressed in the
100
+ block schema, the component's `schema.ts` is what changes, not the markup.
101
+
102
+ Private data — user logins, form submissions — goes to **Cloudflare D1**, never to JSON.
103
+
104
+ ### 5. The prefix is `webm-`, never `wm-`
105
+
106
+ Custom properties (`--webm-action`), class names (`.webm-section`), Cloudflare resource names
107
+ (`webm-<slug>`), the CLI, the skills namespace. No exceptions.
108
+
109
+ ### 6. Cloudflare bindings come from `cloudflare:workers`
110
+
111
+ ```ts
112
+ import { env } from 'cloudflare:workers';
113
+ const { results } = await env.DB.prepare('SELECT 1').run();
114
+ ```
115
+
116
+ Request metadata is `Astro.request.cf`. The execution context is `Astro.locals.cfContext`.
117
+
118
+ ### 7. Content edits happen on a branch
119
+
120
+ Never edit page JSON directly on `main`. Branch, edit, push — Workers Builds gives the branch
121
+ its own preview URL for client review, and a schema-breaking edit fails there instead of on the
122
+ live site. Merge when approved.
123
+
124
+ ### 8. Every third party is consent-gated
125
+
126
+ Anything loaded from outside the site — pixel, analytics, embed, widget — must be gated on the
127
+ visitor's cookie choice. Categories: `essential` (never gated), `functional`, `analytics`,
128
+ `marketing`. Unsure between the last two? Choose `marketing`; it is what US privacy law treats
129
+ as "sale or sharing".
130
+
131
+ ```ts
132
+ import { whenConsented } from '@cparkerwebm/webmonterey/webmonterey/compliance';
133
+
134
+ whenConsented('marketing', () => {
135
+ /* create the script element here, not before */
136
+ });
137
+ ```
138
+
139
+ Loading a script and "not using it" is not gating — the request itself sets the cookie.
140
+
141
+ **Write the gated version even when `features.compliance` is `false`.** With compliance off,
142
+ `whenConsented` fires immediately for every category, so the same code works on both kinds of
143
+ site and turning consent on later needs no changes.
144
+
145
+ **Never read `document.cookie` to check consent.** That misses the Global Privacy Control path,
146
+ and a visitor sending GPC has legally opted out — from January 2027 every browser must offer
147
+ that setting, so it stops being an edge case.
148
+
149
+ **Prefer Google Tag Manager for pixels and analytics.** `TagManager` is wired into the layout and
150
+ `ConsentInit` sets Consent Mode v2 before it loads, so a tag added in the GTM UI inherits the
151
+ visitor's choice with no code change. Write code only for an embed or widget that must render
152
+ into a specific element. Accessibility overlays (UserWay, accessiBe) are rejected on liability,
153
+ not price.
154
+
155
+ ### 9. Look it up before you use it — four servers, one per question
156
+
157
+ All four are declared in [.mcp.json](.mcp.json) and pre-approved in
158
+ [.claude/settings.json](.claude/settings.json), so they work on any machine without anyone
159
+ clicking Approve. `webm doctor` fails if a site has lost one.
160
+
161
+ | Question | Server |
162
+ | ---------------------------------------------------------------------- | -------------- |
163
+ | What does this Astro API do, and is it still called that? | `astro-docs` |
164
+ | What does this web platform API do, and which browsers have it? | `mdn` |
165
+ | Is this the right way to do SEO, a11y, performance, privacy, security? | `website-spec` |
166
+ | Will this email render, arrive, and be readable? | `email-spec` |
167
+
168
+ **Astro moves faster than any training corpus and the web platform never stops.** Much of what a
169
+ model recalls about both is two majors out of date, and the recalled version is always confidently
170
+ wrong rather than obviously wrong. Look it up.
171
+
172
+ **`astro-docs` and `mdn` outrank the two specification servers on facts.** The Website and Email
173
+ Specifications are opinionated best-practice guides — genuinely good ones, and the source for
174
+ _what a good site or email does_ — but they are not normative standards despite the name, and
175
+ they are at 0.x. On what an API is or does, MDN and the Astro docs win.
176
+
177
+ **`email-spec` before touching `src/emails/`.** An email template that looks right in a browser is
178
+ evidence of nothing: the rendering quirks and the deliverability rules are the whole problem, and
179
+ they are exactly what recall gets wrong.
180
+
181
+ If a server is unavailable in a session — `mdn` is explicitly an experiment Mozilla may withdraw —
182
+ **say so and cite the documentation site directly.** Never fall back to recall silently. A session
183
+ that quietly stops checking is the failure all four are here to prevent.
184
+
185
+ ### 10. A class used by more than one page belongs in a shared stylesheet
186
+
187
+ Astro scopes a component's `<style>` to that component's markup. A class defined in one page's
188
+ `<style>` block and used in another renders **completely unstyled** — no error, no warning, just
189
+ a bare element. It is invisible until someone looks at the second page.
190
+
191
+ Shared class → `src/styles/custom/`. A page's own `<style>` block is only for markup that page
192
+ alone renders.
193
+
194
+ ### 11. Never edit inside `node_modules`
195
+
196
+ A change there survives until the next install and not one second longer. If the package is
197
+ wrong, fix the package — see the top of this file.
198
+
199
+ ## Structure
200
+
201
+ ```
202
+ design.json this client's palette. Compiles to --webm-* tokens.
203
+ webmonterey.json domain, client name, environment, features.
204
+ src/
205
+ components/ EVERY visible component. The package ships none.
206
+ registry.ts maps a block `type` to its component. A block whose type is
207
+ not here renders as nothing, silently.
208
+ content/pages/*.json the words. One file per route; home.json is `/`.
209
+ forms/*.json form definitions. Filename is the form id.
210
+ actions/index.ts re-exports the package pipeline. Wrap to customize.
211
+ pages/webapp/ the web app, if this site grows one. Folder is fixed; the public
212
+ URL is `app.path` in webmonterey.json. Every page `prerender = false`.
213
+ styles/custom/ per-client CSS overrides.
214
+ assets/ images processed at build time.
215
+ public/ served verbatim. Favicons, _headers.
216
+ migrations/ D1 schema. Additive only — never edit an applied migration.
217
+ ```
218
+
219
+ ## Component IDs
220
+
221
+ Components are numbered, not named for what they look like: `content-000001`, not `hero-split`.
222
+ A name describing the current design stops being true the first time the design changes, and
223
+ renaming it means touching every content file that references it.
224
+
225
+ The `type` in a page JSON file must match a key in `registry.ts`. Inventing one — or copying it
226
+ from another project — makes the block render as nothing, with no error.
227
+
228
+ ## Skills
229
+
230
+ `.claude/skills/webm/` is materialized from the package on every install. Do not edit it; it is
231
+ replaced wholesale. This client's own skills go beside it at `.claude/skills/<name>/`.
232
+
233
+ | | |
234
+ | --------------------- | ------------------------------------------------------------------------- |
235
+ | `/webm:traps` | the things that fail silently — read this before debugging anything weird |
236
+ | `/webm:new-component` | adding a block type |
237
+ | `/webm:start` | standing a new site up: repo, Cloudflare resources, first deploy |
238
+ | `/webm:launch` | launch checklist: structured data, sending domain, secrets, DNS, cutover |
239
+ | `/webm:upgrade` | taking a new package version |
240
+
241
+ Content edits: branch, edit `src/content/pages/*.json`, `npm run check`, push — the branch
242
+ preview is the client's review link. Merge when approved. Never change a block's `type`.
243
+
244
+ Palette: `design.json`. Fonts are full CSS stacks; a self-hosted face still needs its
245
+ `@font-face` in `src/styles/custom/`. The `overrides` map takes any `--webm-*` token raw.
@@ -0,0 +1,135 @@
1
+ # Editing this site's content
2
+
3
+ For anyone changing words on the site — no coding required.
4
+
5
+ > **Using Claude Cowork?** Paste this whole file into the folder's **Folder instructions**
6
+ > when you connect the site. Cowork does not read it automatically.
7
+
8
+ ---
9
+
10
+ ## Where the words live
11
+
12
+ All page content is in **`src/content/pages/`**. One file per page. The filename is the web
13
+ address:
14
+
15
+ | File | Page on the site |
16
+ | ------------------------------------- | ------------------ |
17
+ | `src/content/pages/home.json` | the home page, `/` |
18
+ | `src/content/pages/about.json` | `/about` |
19
+ | `src/content/pages/contact.json` | `/contact` |
20
+ | `src/content/pages/services/seo.json` | `/services/seo` |
21
+
22
+ `home.json` is the one exception — it becomes `/`, not `/home`.
23
+
24
+ Adding a new file adds a new page. Deleting one removes that page.
25
+
26
+ ---
27
+
28
+ ## What a page file looks like
29
+
30
+ ```json
31
+ {
32
+ "title": "About",
33
+ "description": "Family-run since 1994.",
34
+ "blocks": [
35
+ {
36
+ "type": "content-000001",
37
+ "heading": "Who we are",
38
+ "body": "We have served the Monterey Bay area for thirty years."
39
+ }
40
+ ]
41
+ }
42
+ ```
43
+
44
+ - **`title`** — the browser tab, and what search results show as the headline. Required.
45
+ - **`description`** — the grey summary line in Google results. One or two sentences.
46
+ - **`blocks`** — the sections of the page, top to bottom.
47
+
48
+ ---
49
+
50
+ ## What you can change
51
+
52
+ **Any text.** Headings, body copy, button labels, link addresses, `title`, `description`.
53
+
54
+ **The order of sections.** Move a whole `{ ... }` block up or down inside `blocks` and the
55
+ section moves up or down on the page.
56
+
57
+ **Remove a section.** Delete its whole `{ ... }` block, including the comma that separates it
58
+ from the next one.
59
+
60
+ ---
61
+
62
+ ## What you must not change
63
+
64
+ **Never invent or edit a `"type"` value.** That is not a label — it is the name of a building
65
+ block that has to already exist. If you make one up, or copy one from another site, that
66
+ section renders as **nothing at all** and the page silently loses content.
67
+
68
+ To add a kind of section that does not exist yet, ask for a new component to be built.
69
+
70
+ **Do not touch these at all:**
71
+
72
+ - `src/components/` — the building blocks themselves
73
+ - `src/styles/` — fonts, colors, spacing
74
+ - `src/layouts/`, `src/pages/`, `src/includes/`
75
+ - anything ending in `.astro`, `.ts`, `.css`
76
+ - `package.json`, `astro.config.mjs`, `wrangler.jsonc`, `tsconfig.json`
77
+ - `webmonterey.json` — the site's identity. Changing `domain` breaks search listings.
78
+ - `.dev.vars` — passwords and keys, never to be opened, copied, or pasted anywhere
79
+
80
+ **The cookie banner and privacy dialog are not page content.** They are legally significant:
81
+ the wording describes what the site actually does with data, and the categories map to real
82
+ behavior. Changing that text on your own can make the site's disclosure inaccurate. If a
83
+ client wants different wording, raise it — do not edit it here.
84
+
85
+ ---
86
+
87
+ ## Getting JSON right
88
+
89
+ The format is strict, and one wrong character stops the whole site from building.
90
+
91
+ - Every name and every piece of text is in `"double quotes"`. Never `'single'`, never curly
92
+ `"smart"` quotes — if you draft in Word or Google Docs, it will substitute those silently.
93
+ - A comma between items, but **no comma after the last one** in a list or block.
94
+ - Every `{` needs a `}`, every `[` needs a `]`.
95
+ - An apostrophe inside text is fine: `"We're open"`. A double quote inside text needs a
96
+ backslash: `"They said \"yes\""`.
97
+
98
+ ---
99
+
100
+ ## How to publish a change
101
+
102
+ **Always work on a branch. Never edit the live site directly.**
103
+
104
+ ```sh
105
+ git checkout -b content/homepage-copy # 1. start a branch
106
+ # 2. make your edits
107
+ npm run check # 3. confirm nothing is broken
108
+ git add -A
109
+ git commit -m "Update homepage copy"
110
+ git push -u origin HEAD # 4. push
111
+ ```
112
+
113
+ `npm run check` is the safety net — it catches a missing comma or a wrong field before it can
114
+ reach anyone. **If it reports an error, fix it before pushing.**
115
+
116
+ Pushing gives the branch its own preview web address, something like
117
+ `content-homepage-copy-webm-example-com.workers.dev`. Send that link for review.
118
+
119
+ When it is approved, merge the branch into `main`. That publishes it to the real site.
120
+
121
+ ---
122
+
123
+ ## If something goes wrong
124
+
125
+ **The build failed.** Almost always a JSON typo — a missing comma, a smart quote, an extra
126
+ comma after the last item. The error message names the file.
127
+
128
+ **A section vanished.** Its `"type"` does not match a real component. Check the spelling
129
+ against the folder names in `src/components/`.
130
+
131
+ **A change is not showing up.** Confirm you edited the right file for that page, that the
132
+ branch was pushed, and that you are looking at the preview URL for _that_ branch.
133
+
134
+ Nothing here can be broken permanently — every change is on a branch until it is merged, and
135
+ anything merged can be reverted.
@@ -0,0 +1,81 @@
1
+ # Privacy policy — DRAFT
2
+
3
+ **NOT READY TO PUBLISH.** Every `{{PLACEHOLDER}}` below must be filled in, and every section
4
+ describing something this site does not do must be deleted, before launch. This is a drafting
5
+ aid, not legal advice — the client, and their counsel if they have any, must review it.
6
+
7
+ ## Why this is markdown and not a page
8
+
9
+ The package ships zero components, so it cannot ship page JSON: a block whose `type` is not in
10
+ this site's registry fails the content schema and the build stops. Once the site has a prose
11
+ block, `/webm:edit-content` turns this into `src/content/pages/privacy.json` — the text below is
12
+ already in reading order, one heading per section.
13
+
14
+ ---
15
+
16
+ **Effective date:** {{DATE}}
17
+
18
+ {{LEGAL_ENTITY_NAME}} ("we", "us") operates {{DOMAIN}}. This policy explains what the site collects, why, and the choices you have.
19
+
20
+ ## What we collect
21
+
22
+ **Information you give us.** If you submit a form, we receive what you typed into it — typically your name, email address, phone number and message.
23
+
24
+ **Information collected automatically.** Our hosting provider, Cloudflare, processes technical information required to serve the site: your IP address, browser user-agent, the pages you request, and the country your request came from. This is standard server logging and is used to deliver the site, keep it available, and defend it against abuse.
25
+
26
+ ## Cookies and similar technologies
27
+
28
+ We group these into four categories:
29
+
30
+ - **Essential** — required for the site to work and to resist abuse. These are never optional.
31
+ - **Functional** — remember preferences, such as your cookie choices.
32
+ - **Analytics** — help us understand which pages are used.
33
+ - **Marketing** — used for advertising measurement or audience building.
34
+
35
+ When you first visit, a banner lets you accept or reject each optional category. Nothing in the functional, analytics or marketing categories loads until you choose. You can change your choices at any time from the cookie settings link in the site footer.
36
+
37
+ **Global Privacy Control.** If your browser sends a GPC signal, we treat it as an opt-out of analytics and marketing automatically, and you will not be asked again. You do not need to interact with the banner for this to take effect.
38
+
39
+ ## Who we share information with
40
+
41
+ We do not sell your personal information. We do not share it for cross-context behavioral advertising except as described under Analytics below, and only where you have consented.
42
+
43
+ The site relies on these processors:
44
+
45
+ - **Cloudflare** — hosting, content delivery, and security. Processes technical request data as described above. _Essential._
46
+ - **Cloudflare Turnstile** — spam and abuse protection on forms. Runs only on pages with a form and is treated as essential, because without it the form cannot resist automated abuse.
47
+ - **Mailgun (Sinch)** — delivers form submissions to us by email.
48
+ - **Google Tag Manager / Google Analytics** — measures site usage. _Loads only with your consent to the analytics category._ Google Consent Mode v2 is enabled, so Google is told your choice directly.
49
+ - {{ADD_OR_REMOVE_PROCESSORS_TO_MATCH_THE_SITE}}
50
+
51
+ ## How long we keep things
52
+
53
+ Form submissions are retained for {{RETENTION_PERIOD}} and then deleted. Server logs are retained by Cloudflare under their own retention schedule.
54
+
55
+ ## Your rights
56
+
57
+ Depending on where you live — including under the California Consumer Privacy Act as amended by the CPRA — you may have the right to:
58
+
59
+ - know what personal information we hold about you and how it is used;
60
+ - request a copy of it;
61
+ - request that we correct or delete it;
62
+ - opt out of its sale or sharing (we do not sell it, and the GPC signal and cookie banner both control sharing);
63
+ - not be discriminated against for exercising any of these rights.
64
+
65
+ To make a request, contact us at {{CONTACT_EMAIL}}. We may need to verify your identity before acting on it.
66
+
67
+ ## Children
68
+
69
+ The site is not directed at children under 13 and we do not knowingly collect their information.
70
+
71
+ ## Changes
72
+
73
+ We will update this page if our practices change, and revise the effective date above.
74
+
75
+ ## Contact
76
+
77
+ {{LEGAL_ENTITY_NAME}}
78
+
79
+ {{POSTAL_ADDRESS}}
80
+
81
+ {{CONTACT_EMAIL}}
@@ -0,0 +1,70 @@
1
+ # Cloudflare Workers static-asset headers.
2
+ #
3
+ # The adapter MERGES this with its own generated rules (it adds immutable Cache-Control for
4
+ # /_astro/*), so do not add caching rules for hashed assets here.
5
+ #
6
+ # Deliberately conservative: these are safe for every client site as shipped. The full CSP
7
+ # ships REPORT-ONLY — it enforces nothing and cannot break a site. See the notes below before
8
+ # promoting it to enforcing.
9
+
10
+ /*
11
+ # Stop browsers guessing a MIME type and executing an upload as script.
12
+ X-Content-Type-Options: nosniff
13
+
14
+ # Nobody may frame this site. Replaces the legacy X-Frame-Options.
15
+ Content-Security-Policy: frame-ancestors 'none'
16
+
17
+ # Send the origin to other sites, the full URL to our own. Keeps paths out of referrers.
18
+ Referrer-Policy: strict-origin-when-cross-origin
19
+
20
+ # Turn off APIs the site does not use, for itself and anything it embeds.
21
+ Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=(), usb=()
22
+
23
+ # Two years, and eligible for preload. See the caveat below before uncommenting.
24
+ # Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
25
+
26
+ # Report-only. Violations appear in the browser console and NOTHING is blocked. See below.
27
+ Content-Security-Policy-Report-Only: default-src 'self'; base-uri 'self'; object-src 'none'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data: https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://challenges.cloudflare.com; frame-src https://challenges.cloudflare.com; connect-src 'self' https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com https://challenges.cloudflare.com
28
+
29
+ # THE REPORT-ONLY CSP
30
+ #
31
+ # Report-only enforces nothing, so this is safe to ship unconfigured: a client site with an
32
+ # untuned policy gets console warnings, never a broken page. Read the console on a real
33
+ # browser — a status-code check cannot see a CSP violation.
34
+ #
35
+ # WHAT IT COVERS. The hosts above are the starter's complete third-party list: Google Tag
36
+ # Manager and Google Analytics (only ever loaded with analytics consent) and Cloudflare
37
+ # Turnstile. Everything else is 'self'.
38
+ #
39
+ # WHAT YOU MUST ADD PER CLIENT:
40
+ # - R2 media — `img-src`/`media-src https://media.<client-domain>` if the site uses it.
41
+ # Not included here because the host is derived from the client's own domain.
42
+ # - Any third party added through the `add-third-party` skill. Adding the script without
43
+ # adding its host here means the policy stops describing the site.
44
+ #
45
+ # WHY 'unsafe-inline' IS IN BOTH script-src AND style-src. Not laziness — it is structural,
46
+ # and measured on a real build. Every page carries three inline scripts and three inline
47
+ # styles:
48
+ # - ConsentInit's blocking script, which must run before any third party and therefore
49
+ # cannot be an external file.
50
+ # - The `@layer` order statement, which base.astro emits inline BECAUSE a bare @layer
51
+ # statement does not survive bundling (see CLAUDE.md Traps — moving it out of the
52
+ # document silently inverts the cascade).
53
+ # - Astro's own inlining of small component scripts and scoped styles, which changes hash
54
+ # every build, so static hashes in this file would go stale on the next deploy.
55
+ # Nonces would be the real fix and need a server-rendered response; most routes here are
56
+ # static. So 'unsafe-inline' stays until that changes — but note it weakens ONLY the inline
57
+ # case. default-src, form-action, base-uri, object-src and the host allowlists still catch an
58
+ # injected external script, a form retargeted at another origin, and a rewritten <base>.
59
+ #
60
+ # PROMOTING IT TO ENFORCING. Per client, once the console is clean on every page including
61
+ # forms: rename the header to `Content-Security-Policy`. Do it on a preview URL first, click
62
+ # through every route, and submit a form — Turnstile renders in an iframe, so `frame-src` is
63
+ # the directive most likely to bite, and it fails in a way that looks like the widget simply
64
+ # not appearing.
65
+ #
66
+ # HSTS
67
+ #
68
+ # Only enable once the custom domain is live and HTTPS is confirmed working, INCLUDING every
69
+ # subdomain. `includeSubDomains` will break any subdomain still served over plain HTTP, and
70
+ # `preload` is effectively irreversible for two years.
@@ -0,0 +1,45 @@
1
+ {
2
+ "//": "A form definition. Copy this file to add another form; the filename is the form's id, and that id is what lands in the `form` column in D1.",
3
+ "name": "Contact",
4
+ "//notify": "WHO GETS THE EMAIL. Ships EMPTY on purpose — an unset recipient is obvious, whereas a placeholder address silently swallows every enquiry. Set this before launch.",
5
+ "//subject": "TOPIC ONLY. The `[Client Name] ` prefix is added by src/emails/subject.ts from webmonterey.json — do not write it here or it renders twice. {{field}} placeholders interpolate submitted values; an unmatched one becomes empty rather than showing up literally.",
6
+ "notify": {
7
+ "to": [],
8
+ "subject": "New contact enquiry from {{name}}"
9
+ },
10
+ "//autoresponse": "THE CONFIRMATION SENT BACK TO THE VISITOR. Omit this whole block to send nothing — that is the default, because an autoresponder that goes out before the client has agreed to its wording is a promise the agency made on their behalf. `subject` is TOPIC ONLY, same as notify.subject: the `[Client Name] ` prefix is added by src/emails/subject.ts. It is sent to the address in the form's `email` field, with Reply-To set to the FIRST notify.to recipient, so a visitor replying reaches a human. It never sends if notify.to is empty.",
11
+ "autoresponse": {
12
+ "subject": "We received your enquiry",
13
+ "body": "Thanks for getting in touch. We have received your message and will get back to you shortly."
14
+ },
15
+ "//fields": "Rendered in order. `type` maps to an input type; `required` is enforced server-side, not just in the browser.",
16
+ "fields": [
17
+ {
18
+ "name": "name",
19
+ "label": "Your name",
20
+ "type": "text",
21
+ "required": true,
22
+ "autocomplete": "name"
23
+ },
24
+ {
25
+ "name": "email",
26
+ "label": "Email address",
27
+ "type": "email",
28
+ "required": true,
29
+ "autocomplete": "email"
30
+ },
31
+ {
32
+ "name": "phone",
33
+ "label": "Phone",
34
+ "type": "tel",
35
+ "required": false,
36
+ "autocomplete": "tel"
37
+ },
38
+ {
39
+ "name": "message",
40
+ "label": "How can we help?",
41
+ "type": "textarea",
42
+ "required": true
43
+ }
44
+ ]
45
+ }
@@ -0,0 +1,52 @@
1
+ # PACKAGE-MANAGED. Rewritten by `webm sync` on every install - edit it in the package
2
+ # (template/workflows/ci.yml), never here, or your change disappears on the next `npm update`.
3
+ #
4
+ # WHY THIS EXISTS. Sites once shipped with no CI at all, and a dropped action, an absent
5
+ # migration and a route with no middleware all sat in main for weeks because the only thing that
6
+ # would have run `astro check` was a person remembering to.
7
+ #
8
+ # `webm doctor` is in here deliberately. The type checker catches what TypeScript can see; doctor
9
+ # catches what it cannot - an action nothing exports, a table with no migration, a D1 flag with no
10
+ # binding, an on-demand route the asset router will 404.
11
+ name: CI
12
+
13
+ on:
14
+ push:
15
+ pull_request:
16
+
17
+ # One run per branch. Keyed on head_ref||ref_name rather than github.ref, which differs between
18
+ # push and pull_request events and so runs a branch with an open PR twice.
19
+ concurrency:
20
+ group: ci-${{ github.head_ref || github.ref_name }}
21
+ cancel-in-progress: true
22
+
23
+ permissions:
24
+ contents: read
25
+
26
+ jobs:
27
+ verify:
28
+ name: Check, test, build
29
+ runs-on: ubuntu-latest
30
+ timeout-minutes: 15
31
+ steps:
32
+ - uses: actions/checkout@v5
33
+ - uses: actions/setup-node@v6
34
+ with:
35
+ node-version-file: .nvmrc
36
+ cache: npm
37
+
38
+ # No registry credential. The framework is a public package on npmjs.
39
+ - run: npm ci
40
+
41
+ # astro check. This is the one that fails on a missing prop, a dropped action or an
42
+ # ambient type that never came across.
43
+ - run: npm run check
44
+
45
+ - run: npm test --if-present
46
+
47
+ # The checks a compiler cannot make. Non-blocking for now so it reports rather than
48
+ # gates - a doctor that fails a repo on its first run gets switched off.
49
+ - run: npx webm doctor
50
+ continue-on-error: true
51
+
52
+ - run: npm run build