@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.
- package/CHANGELOG.md +56 -0
- package/LICENSE +21 -0
- package/README.md +104 -0
- package/agents/.gitkeep +0 -0
- package/dist/webm.mjs +2381 -0
- package/hooks/.gitkeep +0 -0
- package/package.json +101 -0
- package/schema/design.json +118 -0
- package/skills/launch/SKILL.md +183 -0
- package/skills/new-component/SKILL.md +85 -0
- package/skills/start/SKILL.md +117 -0
- package/skills/traps/SKILL.md +333 -0
- package/skills/upgrade/SKILL.md +42 -0
- package/src/actions/index.ts +348 -0
- package/src/cli/checks.test.ts +711 -0
- package/src/cli/checks.ts +822 -0
- package/src/cli/codemods.test.ts +42 -0
- package/src/cli/codemods.ts +51 -0
- package/src/cli/compare.test.ts +144 -0
- package/src/cli/compare.ts +222 -0
- package/src/cli/design-extract.test.ts +96 -0
- package/src/cli/design-extract.ts +229 -0
- package/src/cli/doctor.ts +187 -0
- package/src/cli/mcp.test.ts +57 -0
- package/src/cli/mcp.ts +110 -0
- package/src/cli/new.ts +148 -0
- package/src/cli/package-root.ts +58 -0
- package/src/cli/scaffold.test.ts +230 -0
- package/src/cli/scaffold.ts +424 -0
- package/src/cli/seed.ts +133 -0
- package/src/cli/slug.test.ts +70 -0
- package/src/cli/slug.ts +109 -0
- package/src/cli/sync.test.ts +137 -0
- package/src/cli/sync.ts +266 -0
- package/src/cli/upgrade.ts +93 -0
- package/src/design/__fixtures__/tokens-v1.4.1.css +227 -0
- package/src/design/brand.ts +49 -0
- package/src/design/compile.test.ts +98 -0
- package/src/design/compile.ts +155 -0
- package/src/design/defaults.ts +315 -0
- package/src/design/index.ts +18 -0
- package/src/design/resolve.test.ts +80 -0
- package/src/design/resolve.ts +108 -0
- package/src/design/types.ts +96 -0
- package/src/emails/autoresponse.test.ts +82 -0
- package/src/emails/autoresponse.ts +83 -0
- package/src/emails/footer.test.ts +102 -0
- package/src/emails/footer.ts +91 -0
- package/src/emails/index.ts +24 -0
- package/src/emails/subject.test.ts +66 -0
- package/src/emails/subject.ts +80 -0
- package/src/emails/submission-notification.ts +76 -0
- package/src/env.d.ts +12 -0
- package/src/includes/cloudflare/d1/client.ts +86 -0
- package/src/includes/cloudflare/r2/README.md +68 -0
- package/src/includes/cloudflare/r2/media.ts +41 -0
- package/src/includes/cloudflare/r2/url.test.ts +44 -0
- package/src/includes/cloudflare/r2/url.ts +34 -0
- package/src/includes/cloudflare/turnstile/Turnstile.astro +161 -0
- package/src/includes/cloudflare/turnstile/verify.ts +157 -0
- package/src/includes/cloudflare/workers/env.ts +56 -0
- package/src/includes/google/tag-manager/TagManager.astro +73 -0
- package/src/includes/sinch/mailgun/redirect.test.ts +96 -0
- package/src/includes/sinch/mailgun/redirect.ts +96 -0
- package/src/includes/sinch/mailgun/send.ts +186 -0
- package/src/includes/webmonterey/animations/animations.css +347 -0
- package/src/includes/webmonterey/animations/observe.test.ts +88 -0
- package/src/includes/webmonterey/animations/observe.ts +209 -0
- package/src/includes/webmonterey/compliance/ConsentInit.astro +74 -0
- package/src/includes/webmonterey/compliance/CookieConsent.astro +558 -0
- package/src/includes/webmonterey/compliance/consent-styles.test.ts +83 -0
- package/src/includes/webmonterey/compliance/consent.ts +142 -0
- package/src/includes/webmonterey/config.test.ts +94 -0
- package/src/includes/webmonterey/config.ts +346 -0
- package/src/includes/webmonterey/copy-defaults.ts +148 -0
- package/src/includes/webmonterey/copy.ts +13 -0
- package/src/includes/webmonterey/credits/Credit.astro +80 -0
- package/src/includes/webmonterey/credits/credit.test.ts +111 -0
- package/src/includes/webmonterey/credits/credit.ts +59 -0
- package/src/includes/webmonterey/forms/honeypot.test.ts +40 -0
- package/src/includes/webmonterey/forms/honeypot.ts +66 -0
- package/src/includes/webmonterey/prose/inline.test.ts +94 -0
- package/src/includes/webmonterey/prose/inline.ts +71 -0
- package/src/includes/webmonterey/scroll-top/ScrollTop.astro +209 -0
- package/src/includes/webmonterey/site.ts +136 -0
- package/src/includes/webmonterey/structured-data/nodes.ts +315 -0
- package/src/includes/webmonterey/zoned-hour.test.ts +49 -0
- package/src/integration/adapter.ts +53 -0
- package/src/integration/app-middleware.ts +43 -0
- package/src/integration/config.ts +96 -0
- package/src/integration/content.ts +66 -0
- package/src/integration/image-size.test.ts +83 -0
- package/src/integration/image-size.ts +100 -0
- package/src/integration/index.ts +386 -0
- package/src/integration/virtual.d.ts +101 -0
- package/src/layouts/base.astro +402 -0
- package/src/package.test.ts +121 -0
- package/src/pages/404.astro +33 -0
- package/src/pages/[...slug].astro +126 -0
- package/src/pages/robots.txt.ts +41 -0
- package/src/pages/webm.astro +23 -0
- package/src/scripts/.gitkeep +0 -0
- package/src/styles/base.css +185 -0
- package/src/styles/global.css +47 -0
- package/src/styles/layers.test.ts +32 -0
- package/src/styles/layers.ts +21 -0
- package/src/styles/layout.css +235 -0
- package/src/styles/reset.css +135 -0
- package/src/styles/utilities.css +90 -0
- package/src/worker.ts +76 -0
- package/template/assets/logo.svg +14 -0
- package/template/assets/open-graph.png +0 -0
- package/template/migrations/0001_create_submissions.sql +40 -0
- package/template/migrations/README.md +41 -0
- package/template/public/android-chrome-192x192.png +0 -0
- package/template/public/android-chrome-512x512.png +0 -0
- package/template/public/apple-touch-icon.png +0 -0
- package/template/public/favicon-16x16.png +0 -0
- package/template/public/favicon-32x32.png +0 -0
- package/template/public/favicon.ico +0 -0
- package/template/public/favicon.svg +4 -0
- package/template/public/open-graph.png +0 -0
- package/template/scripts/check-node.mjs +72 -0
- package/template/scripts/test-hooks.mjs +96 -0
- package/template/site/.editorconfig +22 -0
- package/template/site/.prettierignore +15 -0
- package/template/site/.prettierrc.json +17 -0
- package/template/site/CLAUDE.md +245 -0
- package/template/site/CONTENT.md +135 -0
- package/template/site/PRIVACY-POLICY-DRAFT.md +81 -0
- package/template/site/public/_headers +70 -0
- package/template/site/src/forms/contact.json +45 -0
- 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.
|