@cparkerwebm/webmonterey 1.2.0 → 1.4.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 +102 -0
- package/README.md +16 -0
- package/dist/webm.mjs +187 -44
- package/package.json +1 -1
- package/skills/launch/SKILL.md +17 -4
- package/skills/new-component/SKILL.md +6 -0
- package/skills/start/SKILL.md +65 -14
- package/skills/traps/SKILL.md +8 -0
- package/skills/webmaster/SKILL.md +118 -0
- package/src/cli/checks.test.ts +61 -0
- package/src/cli/checks.ts +43 -5
- package/src/cli/doctor.ts +78 -2
- package/src/cli/scaffold.test.ts +9 -0
- package/src/cli/scaffold.ts +12 -30
- package/src/cli/settings.ts +106 -0
- package/src/cli/sync.test.ts +62 -0
- package/src/cli/sync.ts +64 -1
- package/src/includes/webmonterey/config.test.ts +51 -0
- package/src/includes/webmonterey/config.ts +49 -0
- package/src/includes/webmonterey/copy-defaults.ts +9 -5
- package/src/includes/webmonterey/webmaster/webmaster.test.ts +61 -1
- package/src/includes/webmonterey/webmaster/webmaster.ts +57 -1
- package/src/integration/index.ts +45 -12
- package/src/integration/virtual.d.ts +17 -3
- package/src/layouts/base.astro +9 -6
- package/src/pages/robots.txt.ts +5 -3
- package/src/pages/webmaster.astro +50 -28
- package/template/site/CLAUDE.md +30 -3
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,108 @@ build. See `/webm:upgrade`.
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## 1.4.0 — 2026-09-04
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **A site can lay out the `/webmaster` page itself.** Export `webmasterPage` from
|
|
19
|
+
`src/components/registry.ts` - an Astro component receiving
|
|
20
|
+
`{ title, description, intro, body }` - and the page renders it in place of the built-in
|
|
21
|
+
`<h1>` and stack of paragraphs. `intro` and each `body` entry are HTML, rendered with
|
|
22
|
+
`set:html` - the agency link is already in `intro`; `title` and `description` are text. The route, the copy, the document title, the meta
|
|
23
|
+
description, the share image and the agency JSON-LD stay the package's, so the page says the
|
|
24
|
+
same thing on every site and looks like the site it is on. Until now a site whose legal pages
|
|
25
|
+
used a richer document layout got a `/webmaster` that looked like a different site, and adding
|
|
26
|
+
its own `src/pages/webmaster.astro` collided with the injected route. The component carries no
|
|
27
|
+
copy of its own: the words are still `copy.webmaster` in `webmonterey.json`.
|
|
28
|
+
|
|
29
|
+
**`/webm:webmaster` is the skill**, materialized on the next `webm sync` - it carries a
|
|
30
|
+
complete reference component, the registry line and the checks to run on the built page, so a
|
|
31
|
+
site session has something to copy rather than a description. `/webm:new-component` sends a
|
|
32
|
+
session there the moment a site gains its document block, and `/webm:start` and
|
|
33
|
+
`/webm:launch` both point at it, so on a new site it happens in sequence; on an existing site
|
|
34
|
+
the upgrade lists it as a new skill.
|
|
35
|
+
The props are exported as `WebmasterPageProps` from
|
|
36
|
+
`@cparkerwebm/webmonterey/webmonterey/webmaster`.
|
|
37
|
+
|
|
38
|
+
**Nothing changes on a site that does not export it.** The built-in layout, `pageHeader`
|
|
39
|
+
seam included, renders as before, and examples/minimal asserts the whole `<main>` as a literal
|
|
40
|
+
so it cannot drift.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- **The `/webmaster` copy.** "This custom website was designed, built and managed by
|
|
45
|
+
WebMonterey, a webmaster service in Monterey, California. WebMonterey handles the hosting,
|
|
46
|
+
security, strategy and ongoing care of the site…" - _custom_ added, _maintenance_ dropped,
|
|
47
|
+
_updates_ is now _strategy_ - and the contact paragraph is bold. `copy.webmaster.intro` and
|
|
48
|
+
`copy.webmaster.body` now take the same inline subset as page prose (`**bold**`, `_italic_`,
|
|
49
|
+
`[text](/url)`), which is how the bold is expressed. A site overriding `copy.webmaster` in
|
|
50
|
+
`webmonterey.json` keeps its own words; one that does not gets these. The agency's
|
|
51
|
+
description in the page's JSON-LD drops _maintenance_ too.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- **"managed byWebMonterey".** The built-in `/webmaster` intro had no space before the agency
|
|
56
|
+
link - Astro drops the whitespace between an expression and an element on separate lines. It
|
|
57
|
+
reads "managed by WebMonterey" now.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 1.3.0 — 2026-09-02
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
- **A staging site is a preview build everywhere.** `environment: "staging"` in webmonterey.json
|
|
66
|
+
now makes every build a preview — every page noindex with no canonical, no sitemap, `robots.txt`
|
|
67
|
+
`Disallow: /`, no Google Tag Manager — on every hostname and in every build: a feature branch,
|
|
68
|
+
`main` on Workers Builds, a laptop. Until now only a non-production branch was a preview, so a
|
|
69
|
+
site that had not launched was crawlable on its `workers.dev` URL the moment `main` deployed.
|
|
70
|
+
The branch rule stays: a feature branch of a launched site is still a preview. An unset
|
|
71
|
+
`environment` is production, as everywhere else, so a site predating the field builds as before.
|
|
72
|
+
The decision is one pure function, `isPreviewBuild`; the build log says which signal made a
|
|
73
|
+
build a preview, and `virtual:webm/build` carries it as `reason`.
|
|
74
|
+
|
|
75
|
+
**A launched site whose webmonterey.json still says `"staging"` disappears from search after
|
|
76
|
+
this update:** every page goes noindex and the sitemap is gone. `/webm:launch` sets
|
|
77
|
+
`"environment": "production"` and `launched` together; `webm doctor` already fails a launched
|
|
78
|
+
site still declared staging, and its message now names this consequence. Flipping `environment`
|
|
79
|
+
is what makes a site indexable, so it must not happen before the custom domain is live —
|
|
80
|
+
`/webm:start` and `/webm:launch` both say so.
|
|
81
|
+
|
|
82
|
+
- **`/webm:start` creates the Worker.** Step 5 is now one deploy from the laptop — `npm run build
|
|
83
|
+
&& npx wrangler deploy`, guarded by `wrangler deployments list` so a re-run skips it — and step 6
|
|
84
|
+
connects the repo to the Worker that now exists (Settings → Builds), a smaller dashboard step
|
|
85
|
+
than importing a repository and typing the Worker name in by hand. The skill used to stop and
|
|
86
|
+
ask for the Worker to be made in the dashboard; on one site it was not, and the result was a
|
|
87
|
+
repo, a database and nothing serving. That laptop deploy is the only one a site ever gets: once
|
|
88
|
+
the repo is connected, a laptop deploy is a version no build produced, which the next push
|
|
89
|
+
reverts. The adapter's auto-provisioned `<slug>-session` KV namespace is expected and stays out
|
|
90
|
+
of wrangler.jsonc. The scaffolded README's deploy note says the same.
|
|
91
|
+
|
|
92
|
+
### Added
|
|
93
|
+
|
|
94
|
+
- **`webm doctor` checks that the Worker exists.** It asks wrangler for the deployments of the
|
|
95
|
+
Worker named in wrangler.jsonc and warns when there are none — the silent failure above. It skips
|
|
96
|
+
with a note when wrangler is not installed or not logged in, so CI and a fresh laptop are not
|
|
97
|
+
failed for being unable to ask.
|
|
98
|
+
|
|
99
|
+
- **Hard rule 12 in a site's CLAUDE.md: a session in a client repo never edits the package.** Not
|
|
100
|
+
in `node_modules`, and not in the package's checkout when it sits on the same machine. The
|
|
101
|
+
deliverable for an upstream problem is a description of the fix — what, where, expected
|
|
102
|
+
behaviour, how to verify — as a prompt for a session opened in the package repo; the site takes
|
|
103
|
+
the fix with `npm update`. Enforced as far as Claude Code's rules reach: `.claude/settings.json`
|
|
104
|
+
denies `Edit(**/node_modules/**)`, and **`webm sync` now merges the package's deny rules into a
|
|
105
|
+
site's settings on every install** — adding what is missing, leaving the site's own rules alone,
|
|
106
|
+
and dropping the two `Write(...)` rules 1.2.0 scaffolded, which Claude Code never consults and
|
|
107
|
+
warns about at startup. The docs have no rule syntax for "any path outside this project", so
|
|
108
|
+
that half stays prose.
|
|
109
|
+
|
|
110
|
+
**On an existing site:** the settings arrive with the next install. `CLAUDE.md` is the site's
|
|
111
|
+
own file, so copy rule 12 in from
|
|
112
|
+
`node_modules/@cparkerwebm/webmonterey/template/site/CLAUDE.md`.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
14
116
|
## 1.2.0 — 2026-09-02
|
|
15
117
|
|
|
16
118
|
### Added
|
package/README.md
CHANGED
|
@@ -20,6 +20,22 @@ the reset, the Cloudflare includes, the consent system, the form pipeline, the r
|
|
|
20
20
|
It ships **zero visible components**. Every block a visitor sees is built per client, in that
|
|
21
21
|
client's repo. Every default has a documented way to opt out of it.
|
|
22
22
|
|
|
23
|
+
A site hands the package its components through `src/components/registry.ts`: `blocks` for the
|
|
24
|
+
block router, `header`, `footer` and `panels` for the chrome, `pageHeader` in place of the
|
|
25
|
+
router's plain `<h1>`, `structuredData` for the site's JSON-LD, and `webmasterPage` for the body
|
|
26
|
+
of the `/webmaster` page. That last one is the seam for a site whose document pages have a richer
|
|
27
|
+
layout than a heading and a stack of paragraphs: the component receives the merged copy
|
|
28
|
+
(`{ title, description, intro, body }`; `intro` and `body` are HTML, the agency link already in
|
|
29
|
+
`intro`) and lays it out; the route, the words, the `<head>`, the share image and the agency graph
|
|
30
|
+
stay the package's. It carries no copy of its own - the words are overridden through
|
|
31
|
+
`copy.webmaster` in `webmonterey.json`, not in the component.
|
|
32
|
+
|
|
33
|
+
**The package is edited in this repo, and only here.** A session in a client site that finds a
|
|
34
|
+
package bug does not reach into `node_modules` or into this checkout; its deliverable is a
|
|
35
|
+
description of the fix, run later in a session opened here, and the site takes the result with
|
|
36
|
+
`npm update`. That is rule 12 of the site's `CLAUDE.md`, with an `Edit` deny on `node_modules`
|
|
37
|
+
behind it that `webm sync` keeps in place. The inverse holds: nothing here edits a client site.
|
|
38
|
+
|
|
23
39
|
## Creating a client site
|
|
24
40
|
|
|
25
41
|
```sh
|
package/dist/webm.mjs
CHANGED
|
@@ -196,6 +196,53 @@ var init_mcp = __esm({
|
|
|
196
196
|
}
|
|
197
197
|
});
|
|
198
198
|
|
|
199
|
+
// src/cli/settings.ts
|
|
200
|
+
function projectSettings(repo) {
|
|
201
|
+
return {
|
|
202
|
+
"//": `Project settings for ${repo}.`,
|
|
203
|
+
"//mcp": "A server declared in .mcp.json is INERT until approved on each machine. Without this line the rules that say consult the Astro and MDN docs before using an API would depend on whoever cloned the repo happening to hit Approve.",
|
|
204
|
+
includeCoAuthoredBy: false,
|
|
205
|
+
enabledMcpjsonServers: [...MCP_NAMES],
|
|
206
|
+
"//permissions": PERMISSIONS_NOTE,
|
|
207
|
+
permissions: { deny: [...DENY_RULES] }
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function withDenyRules(settings) {
|
|
211
|
+
const permissions = settings.permissions && typeof settings.permissions === "object" ? { ...settings.permissions } : {};
|
|
212
|
+
const current = Array.isArray(permissions.deny) ? permissions.deny.filter((r) => typeof r === "string") : [];
|
|
213
|
+
const removed = current.filter((r) => STALE_RULES.includes(r));
|
|
214
|
+
const kept = current.filter((r) => !STALE_RULES.includes(r));
|
|
215
|
+
const added = DENY_RULES.filter((r) => !kept.includes(r));
|
|
216
|
+
if (!added.length && !removed.length) return { settings, added, removed };
|
|
217
|
+
permissions.deny = [...kept, ...added];
|
|
218
|
+
return {
|
|
219
|
+
settings: { ...settings, "//permissions": PERMISSIONS_NOTE, permissions },
|
|
220
|
+
added,
|
|
221
|
+
removed
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
var DENY_RULES, STALE_RULES, PERMISSIONS_NOTE;
|
|
225
|
+
var init_settings = __esm({
|
|
226
|
+
"src/cli/settings.ts"() {
|
|
227
|
+
"use strict";
|
|
228
|
+
init_mcp();
|
|
229
|
+
DENY_RULES = [
|
|
230
|
+
"Read(**/.dev.vars)",
|
|
231
|
+
"Read(**/.dev.vars.*)",
|
|
232
|
+
"Read(**/.env)",
|
|
233
|
+
"Read(**/.env.*)",
|
|
234
|
+
"Read(**/*.pem)",
|
|
235
|
+
"Read(**/*.key)",
|
|
236
|
+
"Read(**/.npmrc)",
|
|
237
|
+
"Edit(**/.dev.vars)",
|
|
238
|
+
"Edit(**/.env)",
|
|
239
|
+
"Edit(**/node_modules/**)"
|
|
240
|
+
];
|
|
241
|
+
STALE_RULES = ["Write(**/.dev.vars)", "Write(**/.env)"];
|
|
242
|
+
PERMISSIONS_NOTE = "The deny list is package-managed: `webm sync` adds any rule that is missing on every install and leaves everything else in this file alone. Edit(**/node_modules/**) is rule 12 of CLAUDE.md made mechanical - a session in this repo never edits the package.";
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
199
246
|
// src/cli/scaffold.ts
|
|
200
247
|
function shiftDays(date, days) {
|
|
201
248
|
const shifted = /* @__PURE__ */ new Date(`${date}T00:00:00Z`);
|
|
@@ -279,7 +326,7 @@ export default defineConfig({
|
|
|
279
326
|
worker: n.worker,
|
|
280
327
|
slug: n.slug,
|
|
281
328
|
launched: null,
|
|
282
|
-
"//environment": "What this deployment is FOR. 'staging' redirects EVERY email the site sends to stagingEmail below, so testing a form on a preview cannot reach the client's real contacts. A new site starts here; /webm:launch flips it to 'production'.
|
|
329
|
+
"//environment": "What this deployment is FOR. 'staging' makes every build a PREVIEW - every page noindex with no canonical, no sitemap, robots.txt disallowing everything, no Google Tag Manager - on every hostname, main included, so a site that has not launched cannot be indexed before it exists; and it redirects EVERY email the site sends to stagingEmail below, so testing a form on a preview cannot reach the client's real contacts. A new site starts here; /webm:launch flips it to 'production' once the custom domain is live, and that flip is what makes the site indexable. A branch other than main is a preview regardless, and anything served from workers.dev redirects its mail regardless, so a branch preview of a live site is covered too.",
|
|
283
330
|
environment: "staging",
|
|
284
331
|
"//stagingEmail": "Where staging email goes instead of its real recipients. REQUIRED while environment is staging - a staging site with nowhere to send refuses to send rather than guessing. webm doctor checks.",
|
|
285
332
|
stagingEmail: options.stagingEmail ?? "",
|
|
@@ -372,31 +419,7 @@ export default defineConfig({
|
|
|
372
419
|
null,
|
|
373
420
|
2
|
|
374
421
|
) + "\n";
|
|
375
|
-
files[".claude/settings.json"] = JSON.stringify(
|
|
376
|
-
{
|
|
377
|
-
"//": `Project settings for ${n.repo}.`,
|
|
378
|
-
"//mcp": "A server declared in .mcp.json is INERT until approved on each machine. Without this line the rules that say consult the Astro and MDN docs before using an API would depend on whoever cloned the repo happening to hit Approve.",
|
|
379
|
-
includeCoAuthoredBy: false,
|
|
380
|
-
enabledMcpjsonServers: MCP_NAMES,
|
|
381
|
-
permissions: {
|
|
382
|
-
deny: [
|
|
383
|
-
"Read(**/.dev.vars)",
|
|
384
|
-
"Read(**/.dev.vars.*)",
|
|
385
|
-
"Read(**/.env)",
|
|
386
|
-
"Read(**/.env.*)",
|
|
387
|
-
"Read(**/*.pem)",
|
|
388
|
-
"Read(**/*.key)",
|
|
389
|
-
"Read(**/.npmrc)",
|
|
390
|
-
"Edit(**/.dev.vars)",
|
|
391
|
-
"Edit(**/.env)",
|
|
392
|
-
"Write(**/.dev.vars)",
|
|
393
|
-
"Write(**/.env)"
|
|
394
|
-
]
|
|
395
|
-
}
|
|
396
|
-
},
|
|
397
|
-
null,
|
|
398
|
-
2
|
|
399
|
-
) + "\n";
|
|
422
|
+
files[".claude/settings.json"] = JSON.stringify(projectSettings(n.repo), null, 2) + "\n";
|
|
400
423
|
files[".mcp.json"] = JSON.stringify(mcpConfig(), null, 2) + "\n";
|
|
401
424
|
files["src/actions/index.ts"] = `/*
|
|
402
425
|
* Form handlers.
|
|
@@ -550,8 +573,11 @@ suffix - \`${n.slug}-portal\`.
|
|
|
550
573
|
|
|
551
574
|
## Deploying
|
|
552
575
|
|
|
553
|
-
|
|
554
|
-
|
|
576
|
+
The Worker is created ONCE from a laptop - \`npm run build && npx wrangler deploy\` - and the
|
|
577
|
+
repo is then connected to it in the dashboard (Worker \u2192 Settings \u2192 Builds). /webm:start does
|
|
578
|
+
both. From then on, push to deploy: a \`wrangler deploy\` from a laptop after that creates a
|
|
579
|
+
version no build produced, so history stops describing what is live and the next push
|
|
580
|
+
reverts it.
|
|
555
581
|
`;
|
|
556
582
|
return files;
|
|
557
583
|
}
|
|
@@ -560,6 +586,7 @@ var init_scaffold = __esm({
|
|
|
560
586
|
"use strict";
|
|
561
587
|
init_slug();
|
|
562
588
|
init_mcp();
|
|
589
|
+
init_settings();
|
|
563
590
|
}
|
|
564
591
|
});
|
|
565
592
|
|
|
@@ -578,7 +605,7 @@ import {
|
|
|
578
605
|
writeFileSync,
|
|
579
606
|
readdirSync
|
|
580
607
|
} from "node:fs";
|
|
581
|
-
import { basename, join as join2 } from "node:path";
|
|
608
|
+
import { basename, dirname as dirname2, join as join2 } from "node:path";
|
|
582
609
|
function syncDir(source, target) {
|
|
583
610
|
if (!existsSync2(source)) return [];
|
|
584
611
|
mkdirSync(target, { recursive: true });
|
|
@@ -604,6 +631,30 @@ function addMissing(source, target) {
|
|
|
604
631
|
}
|
|
605
632
|
return added.sort();
|
|
606
633
|
}
|
|
634
|
+
function ensureSettings(siteRoot) {
|
|
635
|
+
const path = join2(siteRoot, ".claude", "settings.json");
|
|
636
|
+
if (!existsSync2(path)) {
|
|
637
|
+
mkdirSync(dirname2(path), { recursive: true });
|
|
638
|
+
writeFileSync(path, JSON.stringify(projectSettings(basename(siteRoot)), null, 2) + "\n");
|
|
639
|
+
return { added: [...DENY_RULES], removed: [], created: true, skipped: null };
|
|
640
|
+
}
|
|
641
|
+
let parsed;
|
|
642
|
+
try {
|
|
643
|
+
parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
644
|
+
} catch {
|
|
645
|
+
return {
|
|
646
|
+
added: [],
|
|
647
|
+
removed: [],
|
|
648
|
+
created: false,
|
|
649
|
+
skipped: ".claude/settings.json is not valid JSON, so it was left alone"
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
const { settings, added, removed } = withDenyRules(parsed);
|
|
653
|
+
if (added.length || removed.length) {
|
|
654
|
+
writeFileSync(path, JSON.stringify(settings, null, 2) + "\n");
|
|
655
|
+
}
|
|
656
|
+
return { added, removed, created: false, skipped: null };
|
|
657
|
+
}
|
|
607
658
|
function listSkills(dir) {
|
|
608
659
|
if (!existsSync2(dir)) return [];
|
|
609
660
|
return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync2(join2(dir, e.name, "SKILL.md"))).map((e) => e.name).sort();
|
|
@@ -660,7 +711,8 @@ function sync(siteRoot) {
|
|
|
660
711
|
* which is how code depending on an unpublished package reached main without a word.
|
|
661
712
|
*/
|
|
662
713
|
workflows: syncDir(join2(template, "workflows"), join2(siteRoot, ".github/workflows")),
|
|
663
|
-
migrations: addMissing(join2(template, "migrations"), join2(siteRoot, "migrations"))
|
|
714
|
+
migrations: addMissing(join2(template, "migrations"), join2(siteRoot, "migrations")),
|
|
715
|
+
settings: ensureSettings(siteRoot)
|
|
664
716
|
};
|
|
665
717
|
}
|
|
666
718
|
function ensureGitignored(siteRoot) {
|
|
@@ -691,6 +743,19 @@ function run(argv2) {
|
|
|
691
743
|
for (const m of result.migrations) {
|
|
692
744
|
console.log(` + migrations/${m} (apply it: npx wrangler d1 migrations apply <DB> --remote)`);
|
|
693
745
|
}
|
|
746
|
+
if (result.settings.created) {
|
|
747
|
+
console.log(` + .claude/settings.json`);
|
|
748
|
+
} else if (result.settings.skipped) {
|
|
749
|
+
console.log(` ${result.settings.skipped}`);
|
|
750
|
+
} else if (result.settings.added.length || result.settings.removed.length) {
|
|
751
|
+
const parts = [
|
|
752
|
+
result.settings.added.length && `+${result.settings.added.length} deny`,
|
|
753
|
+
result.settings.removed.length && `-${result.settings.removed.length} stale`
|
|
754
|
+
].filter(Boolean);
|
|
755
|
+
console.log(
|
|
756
|
+
` .claude/settings.json: ${parts.join(", ")} (package-managed rules; yours are kept)`
|
|
757
|
+
);
|
|
758
|
+
}
|
|
694
759
|
if (ensureGitignored(siteRoot)) {
|
|
695
760
|
console.log(` gitignored .claude/skills/${NAMESPACE}/`);
|
|
696
761
|
}
|
|
@@ -707,13 +772,14 @@ var init_sync = __esm({
|
|
|
707
772
|
"src/cli/sync.ts"() {
|
|
708
773
|
"use strict";
|
|
709
774
|
init_package_root();
|
|
775
|
+
init_settings();
|
|
710
776
|
NAMESPACE = "webm";
|
|
711
777
|
}
|
|
712
778
|
});
|
|
713
779
|
|
|
714
780
|
// src/cli/seed.ts
|
|
715
781
|
import { cpSync as cpSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
716
|
-
import { dirname as
|
|
782
|
+
import { dirname as dirname3, join as join3, relative } from "node:path";
|
|
717
783
|
function walk(dir) {
|
|
718
784
|
if (!existsSync3(dir)) return [];
|
|
719
785
|
return readdirSync2(dir, { withFileTypes: true }).flatMap((e) => {
|
|
@@ -732,7 +798,7 @@ function seed(packageRoot, siteRoot) {
|
|
|
732
798
|
const rel = relative(source, file);
|
|
733
799
|
const dest = join3(siteRoot, to === "." ? rel : join3(to, rel));
|
|
734
800
|
if (existsSync3(dest)) continue;
|
|
735
|
-
mkdirSync2(
|
|
801
|
+
mkdirSync2(dirname3(dest), { recursive: true });
|
|
736
802
|
cpSync2(file, dest);
|
|
737
803
|
written.push(relative(siteRoot, dest));
|
|
738
804
|
}
|
|
@@ -800,7 +866,7 @@ __export(new_exports, {
|
|
|
800
866
|
});
|
|
801
867
|
import { execFileSync } from "node:child_process";
|
|
802
868
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
803
|
-
import { dirname as
|
|
869
|
+
import { dirname as dirname4, join as join4, resolve } from "node:path";
|
|
804
870
|
function parseArgs(argv2) {
|
|
805
871
|
const positional = argv2.filter((a) => !a.startsWith("--"));
|
|
806
872
|
const flag = (name) => {
|
|
@@ -863,7 +929,7 @@ function run2(argv2) {
|
|
|
863
929
|
});
|
|
864
930
|
for (const [path, contents] of Object.entries(files)) {
|
|
865
931
|
const full = join4(root, path);
|
|
866
|
-
mkdirSync3(
|
|
932
|
+
mkdirSync3(dirname4(full), { recursive: true });
|
|
867
933
|
writeFileSync3(full, contents);
|
|
868
934
|
}
|
|
869
935
|
const seeded = seed(PACKAGE_ROOT, root);
|
|
@@ -1982,7 +2048,7 @@ var init_checks = __esm({
|
|
|
1982
2048
|
{
|
|
1983
2049
|
id: "environment",
|
|
1984
2050
|
title: "The declared environment matches where the site actually is",
|
|
1985
|
-
silentAs: "a launched site whose client email is still being diverted to the agency's inbox",
|
|
2051
|
+
silentAs: "a launched site whose client email is still being diverted to the agency's inbox, and whose every page is noindex",
|
|
1986
2052
|
run(ctx) {
|
|
1987
2053
|
const declared = ctx.site.environment;
|
|
1988
2054
|
if (declared !== void 0 && declared !== "production" && declared !== "staging") {
|
|
@@ -1992,12 +2058,37 @@ var init_checks = __esm({
|
|
|
1992
2058
|
}
|
|
1993
2059
|
if (declared === "staging" && isConfigured(ctx.site.launched)) {
|
|
1994
2060
|
return fail(
|
|
1995
|
-
`this site launched on ${ctx.site.launched} but is still declared staging, so every email it sends is being redirected away from its real recipients. Set "environment": "production" in webmonterey.json.`
|
|
2061
|
+
`this site launched on ${ctx.site.launched} but is still declared staging, so every email it sends is being redirected away from its real recipients - and since 1.3.0 every build of a staging site is a preview: noindex on every page, no canonical, no sitemap, robots.txt disallowing everything. The live site is dropping out of search. Set "environment": "production" in webmonterey.json.`
|
|
1996
2062
|
);
|
|
1997
2063
|
}
|
|
1998
2064
|
if (declared !== "staging" && !isConfigured(ctx.site.launched)) {
|
|
1999
2065
|
return warn(
|
|
2000
|
-
`this site has no launch date but is treated as production, so testing a form will email the client's real contacts. Set "environment": "staging" in webmonterey.json until /webm:launch.`
|
|
2066
|
+
`this site has no launch date but is treated as production, so testing a form will email the client's real contacts and every page is indexable on its workers.dev hostname. Set "environment": "staging" in webmonterey.json until /webm:launch.`
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
return pass;
|
|
2070
|
+
}
|
|
2071
|
+
},
|
|
2072
|
+
{
|
|
2073
|
+
/*
|
|
2074
|
+
* THE WORKER EXISTS. /webm:start used to end with a repo, a D1 database and an instruction
|
|
2075
|
+
* to create the Worker in the dashboard by hand - and on one site nobody did. Nothing local
|
|
2076
|
+
* notices: the build is green, every other check here is green, and the site is a
|
|
2077
|
+
* workers.dev hostname that answers nothing. The Worker is the one resource whose absence
|
|
2078
|
+
* has no symptom on disk, so this asks Cloudflare through wrangler - the one thing a laptop
|
|
2079
|
+
* can ask - and steps aside with a note when it cannot.
|
|
2080
|
+
*/
|
|
2081
|
+
id: "worker-exists",
|
|
2082
|
+
title: "The Worker exists",
|
|
2083
|
+
silentAs: "a site with a repo, a database and nothing serving",
|
|
2084
|
+
run(ctx) {
|
|
2085
|
+
if (ctx.worker.skipped) return { status: "pass", detail: `skipped: ${ctx.worker.skipped}` };
|
|
2086
|
+
if (!ctx.worker.name) {
|
|
2087
|
+
return warn("wrangler.jsonc names no Worker, so there is nothing to look for");
|
|
2088
|
+
}
|
|
2089
|
+
if (!ctx.worker.deployments) {
|
|
2090
|
+
return warn(
|
|
2091
|
+
`no deployment of a Worker named "${ctx.worker.name}" on this account. Create it once from the laptop - npm run build && npx wrangler deploy - then connect the repo to it in the dashboard (Worker \u2192 Settings \u2192 Builds). /webm:start, steps 5 and 6.`
|
|
2001
2092
|
);
|
|
2002
2093
|
}
|
|
2003
2094
|
return pass;
|
|
@@ -2079,8 +2170,10 @@ __export(doctor_exports, {
|
|
|
2079
2170
|
buildContext: () => buildContext,
|
|
2080
2171
|
run: () => run5
|
|
2081
2172
|
});
|
|
2173
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
2082
2174
|
import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync6 } from "node:fs";
|
|
2083
|
-
import {
|
|
2175
|
+
import { createRequire } from "node:module";
|
|
2176
|
+
import { dirname as dirname5, join as join7, relative as relative3, resolve as resolve2 } from "node:path";
|
|
2084
2177
|
function parseJsonc(source) {
|
|
2085
2178
|
const stripped = source.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, comment) => comment ? "" : m).replace(/,(\s*[}\]])/g, "$1");
|
|
2086
2179
|
return JSON.parse(stripped);
|
|
@@ -2140,13 +2233,63 @@ function readMcp(siteRoot) {
|
|
|
2140
2233
|
enabled: read(".claude/settings.json", (p) => p.enabledMcpjsonServers ?? null)
|
|
2141
2234
|
};
|
|
2142
2235
|
}
|
|
2236
|
+
function workerState(siteRoot, name) {
|
|
2237
|
+
const worker = { name: name ?? null, deployments: null, skipped: null };
|
|
2238
|
+
if (!worker.name) return worker;
|
|
2239
|
+
let bin;
|
|
2240
|
+
try {
|
|
2241
|
+
const require2 = createRequire(join7(resolve2(siteRoot), "package.json"));
|
|
2242
|
+
bin = join7(dirname5(require2.resolve("wrangler/package.json")), "bin/wrangler.js");
|
|
2243
|
+
} catch {
|
|
2244
|
+
return {
|
|
2245
|
+
...worker,
|
|
2246
|
+
skipped: "wrangler is not installed here, so the Worker was not looked for"
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
try {
|
|
2250
|
+
const out = execFileSync2(
|
|
2251
|
+
process.execPath,
|
|
2252
|
+
[bin, "deployments", "list", "--name", worker.name, "--json"],
|
|
2253
|
+
{
|
|
2254
|
+
cwd: siteRoot,
|
|
2255
|
+
encoding: "utf8",
|
|
2256
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2257
|
+
timeout: 3e4,
|
|
2258
|
+
env: { ...process.env, WRANGLER_SEND_METRICS: "false", NO_COLOR: "1" }
|
|
2259
|
+
}
|
|
2260
|
+
);
|
|
2261
|
+
const start = out.indexOf("[");
|
|
2262
|
+
const parsed = start >= 0 ? JSON.parse(out.slice(start)) : [];
|
|
2263
|
+
return { ...worker, deployments: Array.isArray(parsed) ? parsed.length : 0 };
|
|
2264
|
+
} catch (error) {
|
|
2265
|
+
const e = error;
|
|
2266
|
+
const text = `${e.stdout ?? ""}
|
|
2267
|
+
${e.stderr ?? ""}
|
|
2268
|
+
${e.message ?? ""}`;
|
|
2269
|
+
if (/code: 10007\]|does not exist on your account/i.test(text)) {
|
|
2270
|
+
return { ...worker, deployments: 0 };
|
|
2271
|
+
}
|
|
2272
|
+
if (/CLOUDFLARE_API_TOKEN|not (logged in|authenticated)|Authentication error|code: (10000|6111|9109)\]/i.test(
|
|
2273
|
+
text
|
|
2274
|
+
)) {
|
|
2275
|
+
return {
|
|
2276
|
+
...worker,
|
|
2277
|
+
skipped: "wrangler is not logged in (npx wrangler login), so whether the Worker exists was not checked"
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
const line = text.split("\n").map((l) => l.replace(/\x1b\[[0-9;]*m/g, "").trim()).find((l) => l && !l.startsWith("\u{1FAB5}"));
|
|
2281
|
+
return { ...worker, skipped: `wrangler could not answer: ${line ?? "no output"}` };
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2143
2284
|
function buildContext(siteRoot) {
|
|
2144
2285
|
const { site } = loadSiteFiles(siteRoot);
|
|
2145
2286
|
const wranglerPath = ["wrangler.jsonc", "wrangler.json"].map((f) => join7(siteRoot, f)).find(existsSync8);
|
|
2146
2287
|
const syncPath = join7(siteRoot, ".claude/skills/webm/.webm-sync.json");
|
|
2288
|
+
const wrangler = wranglerPath ? parseJsonc(readFileSync7(wranglerPath, "utf8")) : null;
|
|
2147
2289
|
return {
|
|
2148
2290
|
site,
|
|
2149
|
-
wrangler
|
|
2291
|
+
wrangler,
|
|
2292
|
+
worker: workerState(siteRoot, wrangler?.name),
|
|
2150
2293
|
pages: readTree(siteRoot, "src/pages", [".astro", ".ts"]),
|
|
2151
2294
|
components: readTree(siteRoot, "src/components", [".astro", ".ts"]),
|
|
2152
2295
|
today: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
@@ -2227,7 +2370,7 @@ __export(audit_exports, {
|
|
|
2227
2370
|
run: () => run6
|
|
2228
2371
|
});
|
|
2229
2372
|
import { existsSync as existsSync9, readdirSync as readdirSync7, readFileSync as readFileSync8, statSync as statSync3 } from "node:fs";
|
|
2230
|
-
import { join as join8, relative as relative4, resolve as
|
|
2373
|
+
import { join as join8, relative as relative4, resolve as resolve3 } from "node:path";
|
|
2231
2374
|
function attr(tag, name) {
|
|
2232
2375
|
const m = new RegExp(`\\s${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, "i").exec(tag);
|
|
2233
2376
|
if (!m) return null;
|
|
@@ -2376,7 +2519,7 @@ async function probe(urls) {
|
|
|
2376
2519
|
return bad.sort((a, b) => a.url.localeCompare(b.url));
|
|
2377
2520
|
}
|
|
2378
2521
|
async function run6(argv2) {
|
|
2379
|
-
const dist =
|
|
2522
|
+
const dist = resolve3(argv2.find((a) => !a.startsWith("-")) ?? "dist/client");
|
|
2380
2523
|
const noExternal = argv2.includes("--no-external");
|
|
2381
2524
|
if (!existsSync9(join8(dist, "index.html"))) {
|
|
2382
2525
|
console.error(`webm audit: no build at ${dist}. Run \`npm run build\` first.`);
|
|
@@ -2473,11 +2616,11 @@ var upgrade_exports = {};
|
|
|
2473
2616
|
__export(upgrade_exports, {
|
|
2474
2617
|
run: () => run7
|
|
2475
2618
|
});
|
|
2476
|
-
import { execFileSync as
|
|
2619
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
2477
2620
|
import { existsSync as existsSync10, readFileSync as readFileSync9 } from "node:fs";
|
|
2478
2621
|
import { join as join9 } from "node:path";
|
|
2479
2622
|
function git(siteRoot, args) {
|
|
2480
|
-
return
|
|
2623
|
+
return execFileSync3("git", args, { cwd: siteRoot, encoding: "utf8" }).trim();
|
|
2481
2624
|
}
|
|
2482
2625
|
function installedVersion(siteRoot) {
|
|
2483
2626
|
const path = join9(siteRoot, "node_modules", PACKAGE, "package.json");
|
|
@@ -2507,7 +2650,7 @@ function run7(argv2) {
|
|
|
2507
2650
|
git(siteRoot, ["checkout", "-b", branch]);
|
|
2508
2651
|
console.log(`Branched to ${branch}. Never upgrade on main.`);
|
|
2509
2652
|
}
|
|
2510
|
-
|
|
2653
|
+
execFileSync3("npm", ["install", `${PACKAGE}@${target}`], { cwd: siteRoot, stdio: "inherit" });
|
|
2511
2654
|
const to = installedVersion(siteRoot);
|
|
2512
2655
|
if (!to) {
|
|
2513
2656
|
console.error("webm upgrade: install did not produce a version. Check the npm output above.");
|
package/package.json
CHANGED
package/skills/launch/SKILL.md
CHANGED
|
@@ -99,6 +99,11 @@ to say, and this is where it gets decided - once, with the client's real details
|
|
|
99
99
|
4. **Validate** the built home page and one inner page with Google's Rich Results Test before
|
|
100
100
|
moving on.
|
|
101
101
|
|
|
102
|
+
5. **Look at `/webmaster`.** The package injects it on every site, with its own share image and
|
|
103
|
+
JSON-LD. If this site's document pages - privacy, terms - use a richer layout than an `<h1>`
|
|
104
|
+
and a stack of paragraphs, run `/webm:webmaster`: it exports a layout component from the
|
|
105
|
+
registry so the page looks like the site it is on, with the package's copy intact.
|
|
106
|
+
|
|
102
107
|
## 3. Turnstile
|
|
103
108
|
|
|
104
109
|
If the site has a form: create the widget in the Cloudflare dashboard (**Turnstile → Add
|
|
@@ -211,13 +216,21 @@ In one change:
|
|
|
211
216
|
"launched": "YYYY-MM-DD"
|
|
212
217
|
```
|
|
213
218
|
|
|
219
|
+
**This flip is also what makes the site indexable, so it happens here and not before.** While
|
|
220
|
+
`environment` says `staging`, every build is a preview - every page noindex with no canonical,
|
|
221
|
+
no sitemap, robots.txt disallowing everything, no Google Tag Manager - on every hostname, `main`
|
|
222
|
+
included. Flip it before the custom domain is live and the `workers.dev` copy is what gets
|
|
223
|
+
indexed.
|
|
224
|
+
|
|
214
225
|
Until `environment` flips, every message the site sends is redirected to `stagingEmail` - correct
|
|
215
226
|
right up to the moment the domain is attached and wrong immediately after: the form keeps saying
|
|
216
227
|
thank you, the client's inbox stays empty, and the first anyone hears of it is a customer asking
|
|
217
|
-
why nobody called back.
|
|
218
|
-
|
|
228
|
+
why nobody called back. And a launched site left on `staging` is invisible to search: every page
|
|
229
|
+
noindex, no sitemap, `Disallow: /`. `webm doctor` fails a launched site still declared staging,
|
|
230
|
+
which is why both fields change together.
|
|
219
231
|
|
|
220
|
-
Anything served from `workers.dev` is still treated as staging whatever this says,
|
|
221
|
-
previews of the live site keep
|
|
232
|
+
Anything served from `workers.dev` is still treated as staging for MAIL whatever this says, and
|
|
233
|
+
any branch other than `main` still builds as a preview, so branch previews of the live site keep
|
|
234
|
+
redirecting and stay out of the index. That is deliberate.
|
|
222
235
|
|
|
223
236
|
Commit, push, and confirm the deploy. Then run `npx webm doctor` one last time: zero failures.
|
|
@@ -83,3 +83,9 @@ npm run preview # NOT dev - preview is a real build on real workerd
|
|
|
83
83
|
|
|
84
84
|
`dev` cannot detect CSS bundling, cascade order, or the asset router. Every trap worth catching is
|
|
85
85
|
invisible in `dev` and visible in `preview`. See `/webm:traps`.
|
|
86
|
+
|
|
87
|
+
## If this is the site's document block
|
|
88
|
+
|
|
89
|
+
A long-form block - the one the privacy and terms pages use - is also the layout the package's
|
|
90
|
+
`/webmaster` page should have. The moment that block exists, run `/webm:webmaster`: it exports a
|
|
91
|
+
layout component built on this block, so `/webmaster` stops looking like a different site.
|