@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
package/hooks/.gitkeep ADDED
File without changes
package/package.json ADDED
@@ -0,0 +1,101 @@
1
+ {
2
+ "name": "@cparkerwebm/webmonterey",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "The WebMonterey Astro framework: plumbing, design system, and Claude Code skills for client sites on Cloudflare Workers",
6
+ "license": "MIT",
7
+ "author": "Carl Parker",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/cparkerwebm/webmonterey.git"
11
+ },
12
+ "//engines": "22.18 is the first release with unflagged TypeScript type stripping, which the webm CLI needs to import src/design/*.ts at runtime.",
13
+ "engines": {
14
+ "node": ">=22.18.0"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "//peerDependencies": "astro is NEVER a plain dependency. Two copies of Astro in one tree breaks the integration in ways that are hard to read. Widen the range only after examples/minimal is green on the new major - see ARCHITECTURE.md section 8.",
20
+ "peerDependencies": {
21
+ "astro": "^7.0.0"
22
+ },
23
+ "//dependencies": "The integration wires these itself, so a client site installs only astro and this package.",
24
+ "dependencies": {
25
+ "@astrojs/cloudflare": "^14.1.7",
26
+ "@astrojs/sitemap": "^3.7.3"
27
+ },
28
+ "devDependencies": {
29
+ "@astrojs/check": "^0.9.10",
30
+ "@cloudflare/workers-types": "^5.20260826.1",
31
+ "@types/node": "^26.1.2",
32
+ "astro": "^7.1.6",
33
+ "esbuild": "^0.28.2",
34
+ "prettier": "^3.9.6",
35
+ "prettier-plugin-astro": "^0.14.1",
36
+ "typescript": "^6.0.3"
37
+ },
38
+ "bin": {
39
+ "webm": "dist/webm.mjs"
40
+ },
41
+ "//exports": "The subpath map mirrors src/includes/ vendor-first tree, so an import in a client repo reads almost identically to the gen-2 relative path. `./design` deliberately contains no Astro imports so it stays testable under `node --test`.",
42
+ "exports": {
43
+ ".": "./src/integration/index.ts",
44
+ "./styles.css": "./src/styles/global.css",
45
+ "./layouts/base.astro": "./src/layouts/base.astro",
46
+ "./design": "./src/design/index.ts",
47
+ "./content": "./src/integration/content.ts",
48
+ "./actions": "./src/actions/index.ts",
49
+ "./pages/slug": "./src/pages/[...slug].astro",
50
+ "./pages/robots": "./src/pages/robots.txt.ts",
51
+ "./pages/webm": "./src/pages/webm.astro",
52
+ "./pages/404.astro": "./src/pages/404.astro",
53
+ "./emails": "./src/emails/index.ts",
54
+ "./cloudflare/d1": "./src/includes/cloudflare/d1/client.ts",
55
+ "./cloudflare/r2": "./src/includes/cloudflare/r2/media.ts",
56
+ "./cloudflare/turnstile": "./src/includes/cloudflare/turnstile/verify.ts",
57
+ "./cloudflare/turnstile/Turnstile.astro": "./src/includes/cloudflare/turnstile/Turnstile.astro",
58
+ "./cloudflare/workers": "./src/includes/cloudflare/workers/env.ts",
59
+ "./google/tag-manager": "./src/includes/google/tag-manager/TagManager.astro",
60
+ "./sinch/mailgun": "./src/includes/sinch/mailgun/send.ts",
61
+ "./webmonterey/site": "./src/includes/webmonterey/site.ts",
62
+ "./webmonterey/compliance": "./src/includes/webmonterey/compliance/consent.ts",
63
+ "./webmonterey/compliance/CookieConsent.astro": "./src/includes/webmonterey/compliance/CookieConsent.astro",
64
+ "./webmonterey/compliance/ConsentInit.astro": "./src/includes/webmonterey/compliance/ConsentInit.astro",
65
+ "./webmonterey/forms": "./src/includes/webmonterey/forms/honeypot.ts",
66
+ "./webmonterey/prose": "./src/includes/webmonterey/prose/inline.ts",
67
+ "./webmonterey/credits": "./src/includes/webmonterey/credits/credit.ts",
68
+ "./webmonterey/credits/Credit.astro": "./src/includes/webmonterey/credits/Credit.astro",
69
+ "./webmonterey/scroll-top/ScrollTop.astro": "./src/includes/webmonterey/scroll-top/ScrollTop.astro",
70
+ "./structured-data": "./src/includes/webmonterey/structured-data/nodes.ts",
71
+ "./schema/design.json": "./schema/design.json",
72
+ "./package.json": "./package.json",
73
+ "./adapter": "./src/integration/adapter.ts",
74
+ "./app-middleware": "./src/integration/app-middleware.ts",
75
+ "./worker": "./src/worker.ts"
76
+ },
77
+ "files": [
78
+ "src",
79
+ "dist",
80
+ "skills",
81
+ "agents",
82
+ "hooks",
83
+ "template",
84
+ "schema",
85
+ "README.md",
86
+ "CHANGELOG.md"
87
+ ],
88
+ "scripts": {
89
+ "build": "node scripts/build-cli.mjs",
90
+ "prepare": "npm run build",
91
+ "prepack": "npm run build",
92
+ "test": "node --test \"src/**/*.test.ts\" \"scripts/**/*.test.mjs\"",
93
+ "check": "tsc --noEmit",
94
+ "format": "prettier --write .",
95
+ "format:check": "prettier --check .",
96
+ "release": "node scripts/release.mjs",
97
+ "test:e2e": "node scripts/e2e.mjs",
98
+ "prepublishOnly": "node scripts/check-release.mjs"
99
+ },
100
+ "//bin": "No leading ./ - npm 11 strips it and warns that the bin was invalid. Points at the BUILT file. Node refuses to strip types for anything under node_modules, so the CLI cannot run from source once installed - see scripts/build-cli.mjs. The library still ships as .ts because Astro compiles it itself."
101
+ }
@@ -0,0 +1,118 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/cparkerwebm/webmonterey/schema/design.json",
4
+ "title": "WebMonterey design system",
5
+ "description": "Per-client design tokens and brand context. Sits beside webmonterey.json at the repo root. Compiled at build into CSS custom properties, email-safe literals, brand context and a resolved palette.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "$schema": { "type": "string" },
10
+ "version": { "const": 1, "description": "Bumped only for a breaking change to this shape." },
11
+
12
+ "brand": {
13
+ "type": "object",
14
+ "additionalProperties": false,
15
+ "description": "The words half of the design system. Reaches site copy, social posts and anything written in the client's name.",
16
+ "properties": {
17
+ "name": { "type": "string" },
18
+ "voice": {
19
+ "type": "string",
20
+ "description": "How this client writes. One or two sentences, concrete. e.g. 'Direct, credential-led, never breathless. Written for CPAs.'"
21
+ },
22
+ "rules": {
23
+ "type": "array",
24
+ "items": { "type": "string" },
25
+ "description": "Hard rules a writer or generator must not break. Phrase each as an instruction, e.g. 'Gold is a fill only - never gold text or links on white.'"
26
+ },
27
+ "logo": {
28
+ "type": "object",
29
+ "additionalProperties": false,
30
+ "properties": {
31
+ "primary": { "type": "string", "description": "Full lockup." },
32
+ "mark": {
33
+ "type": "string",
34
+ "description": "Icon-only mark, for favicons and tight spaces."
35
+ }
36
+ }
37
+ }
38
+ }
39
+ },
40
+
41
+ "color": {
42
+ "type": "object",
43
+ "additionalProperties": false,
44
+ "properties": {
45
+ "base": {
46
+ "type": "object",
47
+ "additionalProperties": false,
48
+ "description": "The neutral ramp. Everything semantic points at these, never the other way round. 100 lightest, 900 darkest - the direction does not invert for a dark design.",
49
+ "properties": {
50
+ "100": { "type": "string" },
51
+ "300": { "type": "string" },
52
+ "500": { "type": "string" },
53
+ "700": { "type": "string" },
54
+ "900": { "type": "string" }
55
+ }
56
+ },
57
+ "action": {
58
+ "type": "object",
59
+ "additionalProperties": false,
60
+ "description": "The brand color. Retheming a client usually starts and ends here.",
61
+ "properties": {
62
+ "base": { "type": "string" },
63
+ "dark": { "type": "string" },
64
+ "light": { "type": "string" }
65
+ }
66
+ },
67
+ "border": {
68
+ "type": "object",
69
+ "additionalProperties": false,
70
+ "properties": { "subtle": { "type": "string" } }
71
+ },
72
+ "state": {
73
+ "type": "object",
74
+ "additionalProperties": false,
75
+ "properties": {
76
+ "success": { "type": "string" },
77
+ "warning": { "type": "string" },
78
+ "danger": { "type": "string" },
79
+ "info": { "type": "string" }
80
+ }
81
+ }
82
+ }
83
+ },
84
+
85
+ "font": {
86
+ "type": "object",
87
+ "additionalProperties": false,
88
+ "description": "Full CSS font stacks, not family names - no fallbacks are appended. A self-hosted face still needs its @font-face declared in src/styles/custom/.",
89
+ "properties": {
90
+ "sans": { "type": "string" },
91
+ "mono": { "type": "string" }
92
+ }
93
+ },
94
+
95
+ "radius": {
96
+ "type": "object",
97
+ "additionalProperties": false,
98
+ "description": "Name only the steps that differ from the defaults.",
99
+ "properties": {
100
+ "none": { "type": "string" },
101
+ "xs": { "type": "string" },
102
+ "sm": { "type": "string" },
103
+ "md": { "type": "string" },
104
+ "lg": { "type": "string" },
105
+ "xl": { "type": "string" },
106
+ "pill": { "type": "string" },
107
+ "circle": { "type": "string" }
108
+ }
109
+ },
110
+
111
+ "overrides": {
112
+ "type": "object",
113
+ "description": "Raw token overrides, applied last. Keys must start with --webm-; anything else is a build error, because a property outside the prefix is set, never read, and the page looks untouched.",
114
+ "propertyNames": { "pattern": "^--webm-[a-z0-9-]+$" },
115
+ "additionalProperties": { "type": "string" }
116
+ }
117
+ }
118
+ }
@@ -0,0 +1,183 @@
1
+ ---
2
+ name: launch
3
+ description: Launch a WebMonterey client site onto its real domain - structured data, Turnstile, sending domain, production secrets, remote migrations, custom domain, verification, and the environment flip. Use for "launch the site", "point the domain at it", "we're going live", "take it out of preview".
4
+ ---
5
+
6
+ # Launch
7
+
8
+ Run only when the site is content-complete and approved on a preview URL. Every step is safe to
9
+ re-run; several are the kind that fail silently, which is why they are written down.
10
+
11
+ ## 1. Pre-flight
12
+
13
+ ```sh
14
+ npx webm doctor
15
+ npm run build
16
+ ```
17
+
18
+ Both clean. Doctor's warnings about placeholder artwork and the missing agency credit become
19
+ blocking here: replace the seeded favicons and share image with the client's own, and confirm
20
+ the footer imports `Credit.astro`.
21
+
22
+ ## 2. Structured data
23
+
24
+ **The package emits no JSON-LD on its own.** What a business claims about itself is the site's
25
+ to say, and this is where it gets decided - once, with the client's real details in front of you.
26
+
27
+ 1. **Identify the business.** Read `webmonterey.json`'s `organization`, the home page, the
28
+ contact page, and the live site if there is one. Decide the primary type: `Organization` is
29
+ the default; `LocalBusiness` (or a subtype - `Restaurant`, `Dentist`, `Store`) only if there is
30
+ a physical address the public visits; `Person` alongside it for a candidate, an artist, a
31
+ consultant whose name is the brand. Look up the subtype on schema.org rather than guessing.
32
+ 2. **Fill `organization`** - legalName, telephone (E.164), email, address, `sameAs` profiles,
33
+ `logo` (a path under `public/`, at least 112×112, NOT the share image), `hours` if there are
34
+ opening hours. **Every empty field is omitted; never invent one.** Wrong opening hours in
35
+ structured data are surfaced by search engines as fact.
36
+ 3. **Write the component.** `src/components/general/structured-data.astro`, composed from the
37
+ package's builders, and export it from the registry:
38
+
39
+ ```astro
40
+ ---
41
+ import {
42
+ graphContext,
43
+ renderJsonLd,
44
+ organizationNode,
45
+ websiteNode,
46
+ webPageNode,
47
+ breadcrumbNode,
48
+ personNode,
49
+ } from '@cparkerwebm/webmonterey/structured-data';
50
+ const { title, description, image } = Astro.props;
51
+ const ctx = graphContext(Astro.site!, Astro.url.pathname, { title, description });
52
+ const jsonLd = renderJsonLd([
53
+ organizationNode(ctx, image),
54
+ personNode(ctx), // null unless organization.founder is set
55
+ websiteNode(ctx),
56
+ webPageNode(ctx),
57
+ breadcrumbNode(ctx), // two levels from the path; pass a trail for deeper pages
58
+ ]);
59
+ ---
60
+
61
+ {jsonLd && <script type="application/ld+json" set:html={jsonLd} />}
62
+ ```
63
+
64
+ ```ts
65
+ // src/components/registry.ts
66
+ export { default as structuredData } from './general/structured-data.astro';
67
+ ```
68
+
69
+ The layout renders it in `<head>` on every indexable route. Add a node the builders do not
70
+ have - `Event`, `Product`, `FAQPage` - as a plain object in the array; `renderJsonLd` handles
71
+ escaping and `@id` wiring for anything you hand it. **Only describe what is visible on the
72
+ page.** Structured data about content a visitor cannot see is a manual-action risk.
73
+
74
+ 4. **Validate** the built home page and one inner page with Google's Rich Results Test before
75
+ moving on.
76
+
77
+ ## 3. Turnstile
78
+
79
+ If the site has a form: create the widget in the Cloudflare dashboard (**Turnstile → Add
80
+ widget**), hostname list = the apex domain AND `<account-subdomain>.workers.dev` so branch
81
+ previews keep working. Turnstile has no wildcards and the free tier allows 10 hostnames.
82
+
83
+ Then, **in the same change**: sitekey into `vars.PUBLIC_TURNSTILE_SITE_KEY` in `wrangler.jsonc`,
84
+ secret via `npx wrangler secret put TURNSTILE_SECRET_KEY`, and `features.turnstile: true`. A
85
+ mismatch fails exactly like a bot does and names nothing.
86
+
87
+ ## 4. Check the client's existing DMARC BEFORE adding a sending domain
88
+
89
+ ```sh
90
+ dig +short TXT _dmarc.<client-domain>
91
+ ```
92
+
93
+ A DMARC record on `example.com` applies to its subdomains by default. If the client already
94
+ publishes `p=reject` and DKIM on the new sending subdomain is not right, **every message
95
+ vanishes** - no bounce, no error, nothing in the logs. Fix the DKIM before the first send, not
96
+ after the first missing enquiry.
97
+
98
+ ## 5. Sending domain
99
+
100
+ Transactional mail sends from `webm.<client-domain>` as `website@webm.<client-domain>` - separate
101
+ from the client's own mail, so a bounce problem on one never poisons the other.
102
+
103
+ Add the domain in Mailgun, then add the SPF, DKIM and tracking records it gives you to the zone.
104
+ Every client zone is on the agency Cloudflare account, so this is a DNS edit.
105
+
106
+ ## 6. Production secrets
107
+
108
+ Anything in `.dev.vars` locally must exist as a real secret on the Worker. Local files are never
109
+ uploaded.
110
+
111
+ ```sh
112
+ npx wrangler secret list
113
+ npx wrangler secret put MAILGUN_API_KEY
114
+ npx wrangler secret put MAILGUN_DOMAIN # webm.<client-domain>
115
+ npx wrangler secret put TURNSTILE_SECRET_KEY # if not done in step 3
116
+ ```
117
+
118
+ **Record each one in the password manager as you create it.** Wrangler cannot read a secret
119
+ back out, so nothing else in the system backs them up.
120
+
121
+ Public values - the Turnstile site key, a GTM container id - are not secrets. They go in
122
+ `vars` in `wrangler.jsonc` or `gtmId` in `webmonterey.json`, and are committed.
123
+
124
+ ## 7. D1, if the site uses it
125
+
126
+ ```sh
127
+ npx wrangler d1 migrations list webm-<slug>-db --remote
128
+ npx wrangler d1 migrations apply webm-<slug>-db --remote
129
+ ```
130
+
131
+ `--remote` is the step people forget. Local migrations do nothing in production, and local and
132
+ remote are separate stores - **data never moves between them in either direction**.
133
+
134
+ ## 8. Custom domain
135
+
136
+ Cloudflare dashboard, the Worker, **Settings → Domains & Routes → Add → Custom domain**. The
137
+ apex only; Cloudflare provisions the certificate. Send `www` to the apex with a Redirect Rule,
138
+ not a second custom domain. MX records are untouched - a custom domain claims address records
139
+ only.
140
+
141
+ ## 9. Verify
142
+
143
+ Wait for a known marker before asserting anything - some edges serve the previous version for a
144
+ minute or two, and a check run in that window reports old titles and missing assets that are
145
+ fine.
146
+
147
+ Then, in a **real browser with the console open**:
148
+
149
+ - every page renders and every internal link resolves
150
+ - the contact form submits, and the notification arrives
151
+ - Turnstile renders - it refuses to render headless, so a headless pass proves nothing
152
+
153
+ And for every route with `prerender = false`, including its trailing-slash form:
154
+
155
+ ```sh
156
+ curl -H "Sec-Fetch-Dest: document" -H "Sec-Fetch-Mode: navigate" https://<domain>/<route>
157
+ ```
158
+
159
+ That header is what the asset router branches on. Without it curl gets the real page and Chrome
160
+ gets the 404.
161
+
162
+ **A form test before step 10 goes to `stagingEmail`** with `[staging → …]` in the subject naming
163
+ who it was really for. That is the system working - check that inbox, not the client's.
164
+
165
+ ## 10. Hand the site its email back, and record the launch
166
+
167
+ In one change:
168
+
169
+ ```json
170
+ "environment": "production",
171
+ "launched": "YYYY-MM-DD"
172
+ ```
173
+
174
+ Until `environment` flips, every message the site sends is redirected to `stagingEmail` - correct
175
+ right up to the moment the domain is attached and wrong immediately after: the form keeps saying
176
+ thank you, the client's inbox stays empty, and the first anyone hears of it is a customer asking
177
+ why nobody called back. `webm doctor` fails a launched site still declared staging, which is why
178
+ both fields change together.
179
+
180
+ Anything served from `workers.dev` is still treated as staging whatever this says, so branch
181
+ previews of the live site keep redirecting. That is deliberate.
182
+
183
+ Commit, push, and confirm the deploy. Then run `npx webm doctor` one last time: zero failures.
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: new-component
3
+ description: Add a new page block to a WebMonterey site - the folder, the schema, the registry entry and the content union. Use for "build a hero", "add a testimonial section", "we need a pricing table", "make a new block type".
4
+ ---
5
+
6
+ # New component
7
+
8
+ **The package ships zero visible components.** Every block a visitor sees is built here, in this
9
+ repo, for this client. That is deliberate: generation 1 shipped framework chrome and every site
10
+ overrode it.
11
+
12
+ ## Component IDs
13
+
14
+ A folder named `{singular-type}-{6 digits}`, containing an `.astro` file of the same name plus
15
+ its `schema.ts`:
16
+
17
+ ```
18
+ src/components/content/content-000004/
19
+ content-000004.astro
20
+ schema.ts
21
+ ```
22
+
23
+ IDs are allocated sequentially **per type** and never reused. Check what exists before picking one.
24
+
25
+ | Folder | Holds | Prefix |
26
+ | ------------- | ------------------------------------------------------- | ------------ |
27
+ | `asides/` | supporting content beside the main flow | `aside-` |
28
+ | `content/` | the main page content blocks | `content-` |
29
+ | `general/` | shared primitives — the fallback when nothing else fits | `general-` |
30
+ | `interfaces/` | interactive UI | `interface-` |
31
+ | `regions/` | page chrome — header, footer, nav | `region-` |
32
+
33
+ ## Three places, and missing either of the last two fails differently
34
+
35
+ **1. The folder.** Schema first, then markup.
36
+
37
+ ```ts
38
+ // schema.ts
39
+ import { z } from 'astro/zod';
40
+
41
+ export const schema = z.object({
42
+ type: z.literal('content-000004'),
43
+ heading: z.string(),
44
+ body: z.array(z.string()).default([]),
45
+ });
46
+ ```
47
+
48
+ **2. `src/components/registry.ts`** — two lines, an import and a map entry. The key **must** equal
49
+ the folder ID and the `type` in page JSON.
50
+
51
+ > **Forgetting this is the most common bug in the content model.** Nothing errors. `astro check`
52
+ > passes, the build succeeds, and the block renders as nothing. The router logs a warning at build
53
+ > time — read the build output.
54
+
55
+ **3. `src/content.config.ts`** — add the schema to the union passed to `webmontereyCollections`.
56
+
57
+ Miss the registry and the block validates but renders as nothing. Miss the union and valid JSON
58
+ fails to build.
59
+
60
+ ## Styling
61
+
62
+ **Tokens only. No literal values.** Every color, space, size, radius, shadow, z-index, duration
63
+ and easing is a `--webm-*` custom property. `webm doctor` warns on literal colors in component CSS.
64
+
65
+ To retheme, change the **token** in `design.json` — one declaration cascades everywhere.
66
+
67
+ **A class used by more than one page belongs in a shared stylesheet.** Astro scopes a component's
68
+ `<style>` to that component's markup, so a class defined in one page's `<style>` and used in
69
+ another renders completely unstyled — no error, no warning, a bare element. This bit one client
70
+ build five separate times.
71
+
72
+ ## Build it in isolation
73
+
74
+ `/webm` is a scratch route for exactly this — noindex, excluded from the sitemap. Compose the
75
+ block there, get it right, then wire it into a page. **Leave it empty when done.**
76
+
77
+ ## Verify
78
+
79
+ ```sh
80
+ npm run check
81
+ npm run preview # NOT dev - preview is a real build on real workerd
82
+ ```
83
+
84
+ `dev` cannot detect CSS bundling, cascade order, or the asset router. Every trap worth catching is
85
+ invisible in `dev` and visible in `preview`. See `/webm:traps`.
@@ -0,0 +1,117 @@
1
+ ---
2
+ name: start
3
+ description: Stand up a new WebMonterey client site - scaffold, GitHub repo, identity, Cloudflare resources, Workers Builds, first deploy. Use for "start a new site for <client>", "spin up <domain>", "set this up for a new client", "we have a new client".
4
+ ---
5
+
6
+ # Start a client site
7
+
8
+ **Resumable.** This is routinely invoked against a repo that is already half set up, so every
9
+ step checks before it acts and is safe to re-run.
10
+
11
+ **Derive, do not ask.** If the answer is in the repo, on disk, or on the live domain, take it
12
+ from there and say where you got it. Asking for a fact the repo already contains is the failure
13
+ this skill exists to prevent.
14
+
15
+ ## 0. Read the state before writing anything
16
+
17
+ ```sh
18
+ git remote get-url origin 2>/dev/null # repo created? which one?
19
+ npx webm doctor # everything below, in one command
20
+ npx wrangler whoami # auth + account
21
+ dig +short NS <domain> # is the zone on our Cloudflare account?
22
+ curl -sI https://<domain> | head -5 # is anything already live?
23
+ ```
24
+
25
+ **If a site is already live, say so prominently.** It changes the project: launch becomes a
26
+ cutover, and the existing site is the content source.
27
+
28
+ ## 1. Scaffold
29
+
30
+ Only if this directory is not already a site (no `webmonterey.json`):
31
+
32
+ ```sh
33
+ npx @cparkerwebm/webmonterey new <domain> --client="<Name>"
34
+ cd <domain_with_underscores>
35
+ ```
36
+
37
+ It writes the identity files, a working contact form, a home page, the fleet skills, and runs
38
+ `git init` and `npm install`. Nothing outside the directory is touched.
39
+
40
+ **Three names, and they differ on purpose:**
41
+
42
+ | | Example | |
43
+ | ----------- | ---------------------------------------------------- | ---------------------------- |
44
+ | GitHub repo | `webmonterey/autire_com` | Full domain, **underscores** |
45
+ | Slug | `autire` | Domain minus the TLD |
46
+ | Cloudflare | `webm-autire`, `webm-autire-db`, `webm-autire-media` | The slug, prefixed |
47
+
48
+ The slug drops the TLD because a Worker named `webm-autire-com` puts `autire-com` into every
49
+ preview hostname, and Chrome's lookalike check then warns the client the site looks fake.
50
+ `autire.com` and `autire.org` both want `autire` - check the slug is free before committing.
51
+
52
+ ## 2. Create the GitHub repo
53
+
54
+ Guarded, because this is re-run against sites that already have one. `git remote get-url`, not
55
+ `git remote -v` - the latter exits 0 with no output when there are no remotes and guards nothing.
56
+
57
+ ```sh
58
+ git remote get-url origin 2>/dev/null \
59
+ || gh repo create <org>/<repo> --private --source=. --remote=origin --push
60
+ ```
61
+
62
+ Private. Client CI needs no secret: the framework is a public package.
63
+
64
+ ## 3. Identity
65
+
66
+ `webmonterey.json` - confirm `client`, `domain`, `timeZone`, `locale`, `stagingEmail`. Leave
67
+ `launched` null and `environment` on `staging` until `/webm:launch`.
68
+
69
+ `organization` feeds nothing until the site has structured data; `/webm:launch` decides what
70
+ schema the business gets. Fill the contact fields now if they are known - never guess one.
71
+
72
+ `design.json` is optional. A site with none compiles the default tokens, which is the right
73
+ starting point before anyone has chosen a palette. Fonts are full CSS stacks; a self-hosted
74
+ face still needs its `@font-face` in `src/styles/custom/`.
75
+
76
+ ## 4. Cloudflare resources
77
+
78
+ Create only what the site needs. A marketing site with a contact form needs D1; it does not need
79
+ R2 until someone has a video.
80
+
81
+ ```sh
82
+ npx wrangler d1 create webm-<slug>-db --update-config # writes the binding into wrangler.jsonc
83
+ npx wrangler d1 migrations apply webm-<slug>-db --local
84
+ npx wrangler r2 bucket create webm-<slug>-media # only if media is going to R2
85
+ ```
86
+
87
+ Set `features.d1: true` once the binding exists. `features.turnstile` waits for `/webm:launch`,
88
+ which creates the widget and its keys together - a sitekey without its secret fails exactly
89
+ like a bot does.
90
+
91
+ **Every route with `export const prerender = false` goes in `run_worker_first`, in both slash
92
+ forms.** Miss one and it returns 200 to curl and a 404 page to Chrome. `webm doctor` checks.
93
+
94
+ ## 5. Workers Builds
95
+
96
+ Connect the repo in the Cloudflare dashboard: **Workers & Pages → Create → Import a repository**.
97
+ The Worker name must be `webm-<slug>` exactly - Workers Builds fails on a mismatch with
98
+ `wrangler.jsonc`.
99
+
100
+ No build variables are needed. **Push to deploy from then on** - a `wrangler deploy` from a
101
+ laptop creates a version no build produced, so history stops describing what is live, and the
102
+ next push reverts it.
103
+
104
+ ## 6. Verify the first deploy
105
+
106
+ Wait a minute after the build reports success - a brand-new Worker can return `error code:
107
+ 1042` on valid paths for about that long. Then, in a real browser with the console open, load
108
+ the `workers.dev` URL. The home page renders, the console is clean.
109
+
110
+ ## 7. Hand over
111
+
112
+ Workers Builds comments the preview URL on every PR - that is the client's review link. Preview
113
+ hostnames use the slug, so Chrome's lookalike warning should not appear; if it does, it is a
114
+ URL-shape false positive and **Ignore is safe**.
115
+
116
+ Next: `/webm:new-component` for each block, then `/webm:launch` when the site is
117
+ content-complete and approved on a preview.