@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,333 @@
1
+ ---
2
+ name: traps
3
+ description: Known traps in Astro on Cloudflare Workers that contradict what a model recalls. Load before touching styles or the cascade, a route's prerender flag, wrangler.jsonc, images, D1, Turnstile, or a deploy. Each entry was found on a real client site, not read in docs.
4
+ ---
5
+
6
+ # Traps
7
+
8
+ Every entry here was found on a live client site. Each contradicts what a model is likely to
9
+ recall, and most produce **no error** — a wrong render, a dead URL, a form that silently rejects.
10
+
11
+ **The through-line for the cascade and router entries: neither `curl` nor `astro dev` can detect
12
+ any of them.** "It works locally" proves nothing about a server-rendered route, an image on that
13
+ route, or how a browser treats a hostname. What does work:
14
+
15
+ - `npm run preview` — a real build on real `workerd`, with the asset router in play
16
+ - `curl -H "Sec-Fetch-Dest: document" -H "Sec-Fetch-Mode: navigate" <url>` — makes curl look like
17
+ a navigation, which is what the asset router branches on
18
+ - **a real browser with the console open**, for anything hostname-scoped. A status-code check
19
+ cannot see a widget that failed to render.
20
+
21
+ Four of these are now `webm doctor` checks. Run it first.
22
+
23
+ ---
24
+
25
+ ## Cascade and CSS
26
+
27
+ **A bare `@layer` order statement does not survive bundling.** It is not attached to any rule, so
28
+ nothing carries it into the output chunk, and layer order falls back to first appearance in the
29
+ bundle. On a real client site `webm.components.core` landed before `webm.reset`, so
30
+ `img { max-inline-size: 100% }` beat a component's `max-inline-size: 11.25rem` and rendered a
31
+ 180px logo at **883px**.
32
+
33
+ The trigger is **import order in page frontmatter**. Astro emits CSS chunks in module-graph
34
+ order, so a component imported above the layout puts its `components.core` chunk ahead of the
35
+ `reset` chunk. Reordering two import lines — which no reviewer would flag — is enough.
36
+
37
+ `base.astro` emits the statement as an inline `<style is:inline>` ahead of every stylesheet.
38
+ In this package it comes from `styles/layers.ts`, so the two lists cannot drift.
39
+
40
+ **An empty `@layer` block is dropped by the minifier**, which is why `components.custom` and
41
+ `overrides` are absent from built CSS until a client writes a rule.
42
+
43
+ **`[hidden]` does not hide anything on its own.** The UA stylesheet's `[hidden] { display: none }`
44
+ is in the UA origin, so any author `display` beats it — a grid or flex child simply ignores the
45
+ attribute and `el.hidden = true` appears to do nothing, with no error. `reset.css` sets
46
+ `[hidden] { display: none !important }`. The `!important` is load-bearing: `webm.reset` is the
47
+ first layer, so a plain declaration there loses on layer order alone, and for `!important`
48
+ declarations that order inverts.
49
+
50
+ **Scoped styles are scoped. A class is not reusable across pages.** A class written in one page's
51
+ `<style>` and used in another renders **completely unstyled** — no error, no warning, a bare
52
+ element. This bit one client build five separate times. Shared classes go in `src/styles/` or
53
+ `styles/custom/`.
54
+
55
+ **`reset.css` restores `dialog { margin: auto }`.** The blanket `* { margin: 0 }` otherwise
56
+ overrides the UA stylesheet and every modal pins itself to the top-left corner. This has already
57
+ been deleted once by accident while editing the adjacent rule — the comment survived and the rule
58
+ did not, so the file still read as correct.
59
+
60
+ **Do not add `inlineStylesheets: 'always'`.** CSS attribution to on-demand routes works: a
61
+ `prerender = false` route gets its own chunk containing the full layer stack in the right order.
62
+ The workaround costs ~20KB inlined into every page in exchange for nothing.
63
+
64
+ **Restyling a base class in `webm.overrides` orphans every modifier of it.** Layer order beats
65
+ specificity outright, so a rule on `.webm-consent-btn` in the last layer also beats the package's
66
+ own `.webm-consent-btn--primary` — the modifier's declarations are in an earlier layer and lose
67
+ even though the selector is more specific. The primary button keeps the text color meant for the
68
+ old background and ships as pale-on-pale. **This is the failure mode of the last layer**: it wins
69
+ against the package, and the package's variants are part of what it wins against. Restyle the
70
+ modifier alongside the base, or move the rule into `webm.components.custom`, where it competes on
71
+ specificity like normal CSS.
72
+
73
+ **A custom property whose value contains `var()` resolves where it is DECLARED, not where it is
74
+ used.** `--webm-focus-ring: var(--webm-focus-width) solid var(--webm-focus-color)` on `:root`
75
+ inherits down **already resolved**, so a component setting `--webm-focus-color` on itself changes
76
+ nothing — the ring keeps the site-wide color, and any component on a colored surface is stuck
77
+ with an invisible focus indicator. There is no error and nothing looks wrong in review. Compose
78
+ from the parts at the point of use (`outline: var(--webm-focus-width) solid var(--webm-focus-color)`)
79
+ and the overrides apply. The same trap applies to any composed token — shadows, transitions,
80
+ gradients.
81
+
82
+ ---
83
+
84
+ ## The asset router and on-demand routes
85
+
86
+ **`not_found_handling: "404-page"` intercepts navigations before the Worker sees them.** A
87
+ navigation request matching no static asset is served `404.html` and never falls through — and
88
+ every `prerender = false` route is such a path.
89
+
90
+ The trigger is `Sec-Fetch-Dest: document`, which browsers send and curl does not. **The same URL
91
+ in the same second returns the real page to curl and the 404 page to Chrome.** A no-JS form POST
92
+ is a navigation too, so it takes out progressive enhancement specifically.
93
+
94
+ Fix: `assets.run_worker_first` in wrangler.jsonc. **Every route given `prerender = false` must be
95
+ listed, including its trailing-slash form** — they are separate paths to the asset router.
96
+ `webm doctor` checks this.
97
+
98
+ **`<Image>` and `getImage` return a dead URL on an on-demand route.** `imageService: 'compile'`
99
+ optimizes at build and ships no runtime image endpoint, but `<Image>` on a `prerender = false`
100
+ route still emits `/_image?href=…`, which is never deployed. It 404s in production and renders
101
+ broken. Invisible locally — `astro dev` serves `/_image` happily.
102
+
103
+ Branch on `Astro.isPrerendered` and fall back to a plain `<img>`. Do **not** switch to
104
+ `passthrough` (kills optimization site-wide) or `cloudflare-binding` (a paid product). `webm
105
+ doctor` checks this.
106
+
107
+ ---
108
+
109
+ ## Hostnames, deploys and platform
110
+
111
+ **A Workers URL is `<worker>.<account-subdomain>.workers.dev`.** Dropping the account subdomain
112
+ produces something that looks plausible and resolves to nothing. Configured into a Turnstile
113
+ widget it yields `TurnstileError: 110200`, and the failure is nearly invisible: the widget never
114
+ renders, never mints a token, every submission is rejected 403, and the form looks normal. The
115
+ only DOM symptom is a missing iframe.
116
+
117
+ **Branch previews are a SIBLING label, not a subdomain** —
118
+ `<branch>-<worker>.<account-subdomain>.workers.dev`. Listing the exact worker hostname in
119
+ Turnstile does not cover them, so the form breaks on precisely the links clients are sent. List
120
+ `<account-subdomain>.workers.dev`; a hostname covers its subdomains. Turnstile does not support
121
+ wildcards, and the free tier allows 10 hostnames per widget.
122
+
123
+ **Chrome's "This site looks fake" warning on preview links comes from the Worker's NAME.** Its
124
+ lookalike-domain check flags "domains that embed other domain names within their own hostname",
125
+ and a Worker called `webm-acme-com` puts `acme-com` into every preview hostname. That is why
126
+ Workers are named `webm-<slug>` with the TLD dropped — `webm-acme` embeds nothing that reads as a
127
+ domain. `webm new` does this; do not name a Worker after the full domain.
128
+
129
+ If a warning still appears on a correctly named Worker it is a URL-shape false positive, not a
130
+ Safe Browsing verdict — **Ignore is safe**. There is no other lever: a custom domain points at a
131
+ _Worker_, not a _version_, so it can only serve production, and per-version preview URLs are
132
+ `workers.dev`-only.
133
+
134
+ **`workers_dev: false` does NOT disable preview URLs.** Separate switches, which is why
135
+ wrangler.jsonc sets both explicitly.
136
+
137
+ **`wrangler deployments list` misleads twice.** `Source: Unknown (deployment)` appears even for
138
+ Workers Builds deployments — it describes the author, not the origin. And the list pages at 10
139
+ entries, so "has the count gone up?" can never become a way of waiting for a deploy.
140
+
141
+ **Verification immediately after a deploy gives false negatives.** Some edges serve the previous
142
+ version for a minute or two. Wait for a known marker before asserting anything.
143
+
144
+ **A brand-new Worker can return `error code: 1042` on valid paths for about a minute.** Retry
145
+ once after ~60s before diagnosing. The documented meaning of 1042 does not apply to a freshly
146
+ deployed static site.
147
+
148
+ **After a repo is connected to Workers Builds, `npx wrangler deploy` from a laptop is wrong.** It
149
+ succeeds, which is the trap: it creates a version no dashboard build produced, so build history no
150
+ longer describes what is live, and the next `git push` reverts it.
151
+
152
+ **Local and remote D1/R2 are separate stores.** Rows and objects never move between them, in
153
+ either direction, and nothing warns you. Code moves on push; schema moves via
154
+ `wrangler d1 migrations apply --remote`; **data does not move at all.**
155
+
156
+ **Astro's dev server daemonizes.** It detaches and the launching command exits, which reads as a
157
+ crash. Manage it with `astro dev status` / `logs` / `stop`.
158
+
159
+ ---
160
+
161
+ ## TypeScript and APIs
162
+
163
+ **`querySelector<HTMLSelectElement>` does not compile — and it is the only element type that
164
+ does not.** `worker-configuration.d.ts` declares HTMLRewriter's `interface Element`, whose
165
+ `remove()` returns `Element` so calls can chain. Being an interface it _merges_ with `lib.dom`'s
166
+ `Element`, so the global now carries a `remove(): Element` overload, and
167
+ `querySelector<T extends Element>` demands it of `T`. Every element type inherits that — except
168
+ `HTMLSelectElement`, which declares its own `remove(index: number): void`, and an own declaration
169
+ shadows the inherited one.
170
+
171
+ Cast instead: `document.querySelector('#t') as HTMLSelectElement | null`. Do not "fix" it by
172
+ removing `worker-configuration.d.ts` from tsconfig — that is what types the bindings.
173
+ `webm doctor` checks this.
174
+
175
+ **`Astro.locals.runtime` does not exist.** Removed in `@astrojs/cloudflare` v13. Any snippet using
176
+ `Astro.locals.runtime.env.DB` is Pages-era. Use `import { env } from 'cloudflare:workers'`.
177
+
178
+ **Bindings are unavailable on prerendered routes.** Any route touching `env.DB` needs
179
+ `export const prerender = false`.
180
+
181
+ **`import.meta.env` is NOT the workaround for that.** Both `.env` and `.dev.vars` populate it
182
+ server-side at build time, so on a prerendered route the secret is written **into
183
+ `dist/client/*.html`**, which Cloudflare serves publicly. A local `npm run build` before
184
+ `wrangler deploy` is exactly the machine that has `.dev.vars` present. Never
185
+ `JSON.stringify(import.meta.env)` into a page.
186
+
187
+ **`astro/zod` is zod 4.** The discriminated-union option constraint is
188
+ `core.$ZodTypeDiscriminable` — internal, not re-exported. Zod 3's `ZodDiscriminatedUnionOption`
189
+ no longer exists, so any snippet using it is pre-v4. Derive from the function signature instead.
190
+ `astro:schema`, and `z` from `astro:content`, are deprecated.
191
+
192
+ **The collections config is `src/content.config.ts`.** `src/content/config.ts` was removed in v6.
193
+
194
+ **`src/fetch.ts` is reserved in Astro 7.** Do not create it.
195
+
196
+ **Never put a non-ASCII character in a `throw new Error` message.** Astro puts the message into an
197
+ `x-astro-prerender-error` header during prerendering; an em-dash triggers a warning and renders
198
+ mangled. Fine in comments and UI copy — just not there.
199
+
200
+ ---
201
+
202
+ ## Cron Triggers
203
+
204
+ **A `triggers.crons` entry on its own deploys cleanly and never runs anything.**
205
+
206
+ The adapter generates the Worker entrypoint, and that generated file exports `fetch` and nothing
207
+ else. There is no `scheduled` handler in it. But the config half works perfectly: a `triggers`
208
+ block in `wrangler.jsonc` merges straight into the generated `dist/server/wrangler.json` and
209
+ deploys without a warning. Cloudflare then invokes a handler the Worker does not have, on
210
+ schedule, forever, and nothing in the repo says so.
211
+
212
+ Verified rather than assumed — with a cron in `wrangler.jsonc` and no `main`:
213
+
214
+ ```sh
215
+ npx astro build
216
+ grep scheduled dist/server/entry.mjs # nothing
217
+ npx wrangler dev --local --test-scheduled
218
+ curl 'http://127.0.0.1:8787/__scheduled?cron=0+*+*+*+*'
219
+ # -> 404, served by the asset router
220
+ ```
221
+
222
+ ### The fix, which is supported and already in production
223
+
224
+ Point `main` at a **source** entrypoint that re-exports the adapter's own handler. This is the one
225
+ legitimate reason to set that key.
226
+
227
+ ```jsonc
228
+ // wrangler.jsonc
229
+ "main": "./src/worker.ts",
230
+ "triggers": { "crons": ["0 * * * *"] }
231
+ ```
232
+
233
+ ```ts
234
+ // src/worker.ts
235
+ import { defineWorker } from '@cparkerwebm/webmonterey/worker';
236
+ import { runSweep } from './includes/sweep.ts';
237
+
238
+ export default defineWorker({
239
+ scheduled: (controller, env, ctx) => ctx.waitUntil(runSweep(env)),
240
+ });
241
+ ```
242
+
243
+ `defineWorker` supplies the adapter's `fetch` and does not let you replace it — every page, action
244
+ and API route arrives through that function, so a site that meant to add a cron and accidentally
245
+ replaced the request path takes itself down while the cron works perfectly. Site-wide request
246
+ logic goes in Astro middleware instead.
247
+
248
+ `webm doctor` fails on a cron with no `main`, on a `main` that names a build artefact, on an
249
+ entrypoint with no `scheduled`, and on one that forgets the adapter handler.
250
+
251
+ **`./dist/_worker.js/index.js` is the value NOT to use.** It is the Pages-era form, it names a
252
+ build artefact rather than a source file, and it fails the build with "main field doesn't point to
253
+ an existing file".
254
+
255
+ **Cron runs in UTC, always** — there is no timezone setting anywhere in Cloudflare. A job that has
256
+ to land at a local hour should fire hourly and let the handler check the clock in the site's own
257
+ zone with `hourNow()`; pinning it to a fixed UTC hour drifts by one twice a year.
258
+
259
+ **Cloudflare Queues: a producer is two lines; a consumer is not supported by the package.** A
260
+ producer is a binding plus `env.QUEUE.send()`. A consumer needs a `queue()` handler on the Worker
261
+ export, and `defineWorker` accepts one — but nothing in the package has exercised it. Spike it on a
262
+ throwaway Worker before a client depends on it.
263
+
264
+ ## Build and platform defaults
265
+
266
+ **Static assets build to `dist/client/`, not `dist/`.** Astro's own docs and most tutorials say
267
+ `./dist`.
268
+
269
+ **`npx wrangler deploy` from the repo root is correct — do not add `--config`.** The build writes
270
+ `.wrangler/deploy/config.json`, which redirects Wrangler to the generated
271
+ `dist/client/wrangler.json`. The root file is still the one you edit.
272
+
273
+ **Do not hand-write `public/.assetsignore`.** Adapter v14 generates
274
+ `dist/client/.assetsignore` itself, and the worker is emitted to `dist/_worker.js` — a sibling of
275
+ the assets directory, never inside it.
276
+
277
+ **The adapter binds Cloudflare Images by default.** `imageService` defaults to
278
+ `'cloudflare-binding'`, a separate paid product transforming at request time. The integration sets
279
+ `'compile'`.
280
+
281
+ **An `env.SESSION` KV binding is added automatically** and auto-provisioned on deploy. It backs
282
+ `Astro.session`. Leave it alone.
283
+
284
+ **Turnstile has no official Astro or Workers integration.** `@cloudflare/pages-plugin-turnstile`
285
+ is Pages-only. Verification is a manual POST to
286
+ `https://challenges.cloudflare.com/turnstile/v0/siteverify`.
287
+
288
+ **Astro deleted every `llms.txt` / `llms-full.txt`.** Those URLs 404. The MCP server is the only
289
+ official AI-docs endpoint.
290
+
291
+ **Cloudflare's own Astro framework guide is stale** — it still says "Astro 6 beta" and points at
292
+ `locals` for bindings. Where it disagrees with docs.astro.build on adapter APIs, **docs.astro.build
293
+ wins.** Cloudflare docs remain authoritative for wrangler and the platform.
294
+
295
+ **`git` does not track empty directories.** Every empty folder needs a `.gitkeep`.
296
+
297
+ ---
298
+
299
+ ## Email and DNS
300
+
301
+ **A staging site's email is redirected, and both directions of getting that wrong are silent.**
302
+ `environment` in webmonterey.json decides it: on `staging`, every recipient is rewritten to
303
+ `stagingEmail` and the subject is prefixed `[staging → who-it-was-for]`. Left on `staging` after
304
+ launch, the client's enquiries go to the agency and their own inbox stays empty — the form still
305
+ says thank you. Left on `production` before launch, testing a preview mails the client's real
306
+ contacts for real. `webm doctor` checks both directions against `launched`; `/webm:launch` is
307
+ where it gets flipped.
308
+
309
+ **A staging site with no `stagingEmail` refuses to send at all.** The package carries no default
310
+ inbox, so there is nowhere safe to redirect to. `sendEmail` throws rather than mailing the real
311
+ recipients; `webm doctor` fails the site before anyone submits a form.
312
+
313
+ **Anything on `workers.dev` redirects regardless of what `environment` says.** webmonterey.json is
314
+ committed, so a branch preview of a live site inherits `production` from main — the hostname test
315
+ is what stops that preview mailing real people. It matches the `workers.dev` label specifically,
316
+ not "any hostname that is not the canonical domain": `domain` is stored bare, so the latter would
317
+ divert every enquiry the day a site answers on `www.`.
318
+
319
+ **A scheduled handler has no request, so it has no hostname.** Only `environment` protects the
320
+ nightly sweep — the check that reads a hostname cannot see a cron at all. This is why the switch
321
+ is config rather than something derived from the URL.
322
+
323
+ **Check the client's existing DMARC before adding a sending subdomain.** A DMARC record on
324
+ `example.com` applies to its subdomains by default. If the client publishes `p=reject` and DKIM on
325
+ the new subdomain is not right, **every message vanishes** — no bounce, no error, nothing in the
326
+ logs.
327
+
328
+ **R2 has no object versioning.** Its equivalent is **bucket locks**, which prevent deletion and
329
+ overwriting for a retention period. Any snippet enabling "R2 versioning" is wrong — the command
330
+ does not exist.
331
+
332
+ **`rclone sync` is not a backup.** It makes the destination match the source, so a file deleted at
333
+ the source disappears from the backup within 24 hours. Use `rclone copy`.
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: upgrade
3
+ description: Move a WebMonterey site to a newer version of the framework package. Use for "upgrade the package", "update webmonterey", "there's a new version", "bump the framework".
4
+ ---
5
+
6
+ # Upgrade
7
+
8
+ ## Run the CLI, do not follow steps from memory
9
+
10
+ ```sh
11
+ npx webm upgrade
12
+ ```
13
+
14
+ **This skill is deliberately thin, and that is the point.** An invoked skill's text enters the
15
+ conversation once and is not re-read on later turns. So this skill runs `npm install`, which
16
+ overwrites its own `SKILL.md` with the new version's — and then keeps executing the _old_
17
+ instructions to completion.
18
+
19
+ The binary on disk is the new version immediately. The markdown is frozen for the session. So the
20
+ steps live in the CLI, where they update the moment the install finishes.
21
+
22
+ ## What it does
23
+
24
+ 1. Branch — never upgrade on `main`
25
+ 2. Bump `@cparkerwebm/webmonterey`
26
+ 3. Run any codemods the new version ships
27
+ 4. `webm sync` — re-materialize the fleet skills
28
+ 5. `webm doctor` — the traps that fail silently
29
+ 6. `npm run check` and `npm run build`
30
+
31
+ ## After it finishes
32
+
33
+ **Run `/reload-plugins`** if the version changed anything outside `skills/`. New and edited
34
+ `SKILL.md` files are picked up live; the plugin's other components are not.
35
+
36
+ Push the branch and check the preview URL before merging. A framework upgrade is exactly the kind
37
+ of change where `preview` catches what `dev` cannot.
38
+
39
+ ## If a site should not upgrade
40
+
41
+ **Pinning is a legitimate answer.** A site that is fine can sit on an old major indefinitely; the
42
+ fleet dashboard shows who is where. Do not upgrade a site with no reason to change.