@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.
@@ -31,7 +31,12 @@ import { fileURLToPath } from 'node:url';
31
31
  import { compileToCss } from '../design/compile.ts';
32
32
  import { imageSize } from './image-size.ts';
33
33
  import { loadForms, loadSiteFiles, resolveSiteUrl } from './config.ts';
34
- import { APP_DIR, appEnabled, resolveAppPath } from '../includes/webmonterey/config.ts';
34
+ import {
35
+ APP_DIR,
36
+ appEnabled,
37
+ previewReason,
38
+ resolveAppPath,
39
+ } from '../includes/webmonterey/config.ts';
35
40
 
36
41
  export interface WebmontereyOptions {
37
42
  /**
@@ -79,10 +84,14 @@ export interface WebmontereyOptions {
79
84
  /**
80
85
  * The branch Workers Builds deploys to production. Default `main`.
81
86
  *
82
- * Every other branch is a PREVIEW, and a preview build is different on purpose: every page is
83
- * noindex, there is no sitemap, robots.txt disallows everything, and Google Tag Manager does
84
- * not load - so a client's review link can never be indexed, and clicking around it never
85
- * lands in their analytics. Detected from WORKERS_CI_BRANCH, which Workers Builds injects.
87
+ * Every other branch is a PREVIEW - and so is every build, on any branch and from any machine,
88
+ * of a site whose webmonterey.json says `environment: "staging"`. A preview build is different
89
+ * on purpose: every page is noindex with no canonical, there is no sitemap, robots.txt
90
+ * disallows everything, and Google Tag Manager does not load - so a client's review link can
91
+ * never be indexed, a site that has not launched cannot be indexed before it exists, and
92
+ * clicking around either never lands in their analytics. The branch comes from
93
+ * WORKERS_CI_BRANCH, which Workers Builds injects; the decision is `isPreviewBuild` in
94
+ * includes/webmonterey/config.ts.
86
95
  */
87
96
  productionBranch?: string;
88
97
  }
@@ -133,14 +142,29 @@ export default function webmonterey(options: WebmontereyOptions = {}): AstroInte
133
142
  const appPath = resolveAppPath(files.site);
134
143
 
135
144
  /*
136
- * BRANCH PREVIEW OR PRODUCTION. Workers Builds injects WORKERS_CI_BRANCH; anything that
137
- * is not the production branch is a preview. A local build has no branch and is treated
138
- * as production, which is what `npm run preview` and the e2e need. See the option.
145
+ * PREVIEW OR PRODUCTION, decided in one place - previewReason - from two signals. A site
146
+ * whose webmonterey.json says `environment: "staging"` is a preview in every build,
147
+ * whatever the branch and whatever the machine; and on a launched site any Workers
148
+ * Builds branch other than the production one is a preview too. A local build of a
149
+ * production site has no branch and is production output, which is what
150
+ * `npm run preview` and the e2e need. See the option, and the function.
139
151
  */
140
152
  const branch = process.env.WORKERS_CI_BRANCH ?? null;
141
- const preview = branch !== null && branch !== (options.productionBranch ?? 'main');
142
- if (preview) {
143
- logger.info(`branch "${branch}" is a preview: noindex, no sitemap, no analytics`);
153
+ const productionBranch = options.productionBranch ?? 'main';
154
+ const reason = previewReason({
155
+ environment: files.site.environment,
156
+ branch,
157
+ productionBranch,
158
+ });
159
+ const preview = reason !== null;
160
+ if (reason === 'staging') {
161
+ logger.info(
162
+ 'environment is "staging" in webmonterey.json: a preview build - noindex, no sitemap, no analytics',
163
+ );
164
+ } else if (reason === 'branch') {
165
+ logger.info(
166
+ `branch "${branch}" is not ${productionBranch}: a preview build - noindex, no sitemap, no analytics`,
167
+ );
144
168
  }
145
169
 
146
170
  /*
@@ -255,7 +279,7 @@ export default function webmonterey(options: WebmontereyOptions = {}): AstroInte
255
279
  load(id: string) {
256
280
  switch (id) {
257
281
  case resolved(VIRTUAL.build):
258
- return `export default ${JSON.stringify({ preview, branch })};`;
282
+ return `export default ${JSON.stringify({ preview, reason, branch })};`;
259
283
  case resolved(VIRTUAL.site):
260
284
  return `export default ${JSON.stringify(files.site)};`;
261
285
  case resolved(VIRTUAL.design):
@@ -363,6 +387,15 @@ export default function webmonterey(options: WebmontereyOptions = {}): AstroInte
363
387
  * router's plain <h1>; a site that does not keeps the <h1>.
364
388
  */
365
389
  `export const pageHeader = mod.pageHeader ?? null;`,
390
+ /*
391
+ * THE /webmaster PAGE'S BODY. The route, the copy, the share image and
392
+ * the agency graph stay the package's; a site whose document pages use
393
+ * a richer layout than an <h1> and a stack of paragraphs hands over the
394
+ * layout only. Without this a site's own src/pages/webmaster.astro
395
+ * collides with the injected route, and on one site the injected route
396
+ * won.
397
+ */
398
+ `export const webmasterPage = mod.webmasterPage ?? null;`,
366
399
  /*
367
400
  * THE SITE'S JSON-LD, rendered into <head> on every route. The package
368
401
  * emits none of its own: what a business claims about itself is the
@@ -5,11 +5,14 @@
5
5
  * a package cannot import relatively - see includes/webmonterey/config.ts.
6
6
  */
7
7
  /**
8
- * What this build is FOR. `preview` is true on any Workers Builds branch other than the
9
- * production one - every page noindex, no sitemap, no analytics. A local build is not a preview.
8
+ * What this build is FOR. `preview` is true when webmonterey.json says `environment: "staging"`,
9
+ * or on any Workers Builds branch other than the production one - every page noindex, no
10
+ * sitemap, no analytics. `reason` says which signal decided it. A local build of a production
11
+ * site is not a preview. See `isPreviewBuild` in includes/webmonterey/config.ts.
10
12
  */
11
13
  declare module 'virtual:webm/build' {
12
- const build: { preview: boolean; branch: string | null };
14
+ import type { PreviewReason } from '../includes/webmonterey/config.ts';
15
+ const build: { preview: boolean; reason: PreviewReason; branch: string | null };
13
16
  export default build;
14
17
  }
15
18
 
@@ -83,6 +86,17 @@ declare module 'virtual:webm/registry' {
83
86
  * every later fix to block lookup, unknown-type reporting and FAQ extraction.
84
87
  */
85
88
  export const pageHeader: AstroComponentFactory | null;
89
+ /**
90
+ * The body of the `/webmaster` page, when the site wants it laid out like its own document
91
+ * pages rather than as the package's `<h1>` and stack of paragraphs.
92
+ *
93
+ * Receives `WebmasterPageProps` from `includes/webmonterey/webmaster/webmaster.ts`:
94
+ * `{ title, description, intro, body }`, the merged `copy.webmaster` - `intro` and `body` as
95
+ * HTML through the inline prose renderer, the agency link already in `intro`. The component
96
+ * lays the words out and carries no copy of its own. The route, the `<head>`, the share image and the JSON-LD stay the package's.
97
+ * Null means the built-in layout, `pageHeader` seam included.
98
+ */
99
+ export const webmasterPage: AstroComponentFactory | null;
86
100
  /**
87
101
  * The site's JSON-LD, rendered into <head> on every indexable route. The package emits none of
88
102
  * its own; this component composes a graph from the builders in
@@ -183,12 +183,15 @@ const {
183
183
  } = Astro.props;
184
184
 
185
185
  /*
186
- * A BRANCH PREVIEW IS NOINDEX, EVERY PAGE. The review link a client gets is a public workers.dev
187
- * URL, and a search engine that finds one indexes a duplicate of the site under the wrong
188
- * hostname. noindex also suppresses the canonical and og:url below, so the preview sends one
189
- * signal rather than "do not index me" beside "my real address is over there". GTM is skipped on
190
- * a preview for the same reason in the other direction: a client clicking through their review
191
- * link must not show up in their own analytics.
186
+ * A PREVIEW BUILD IS NOINDEX, EVERY PAGE. A preview is a staging site (`environment` in
187
+ * webmonterey.json) on any hostname, or a non-production branch of a launched one - see
188
+ * isPreviewBuild in includes/webmonterey/config.ts. The review link a client gets is a public
189
+ * workers.dev URL, and a search engine that finds one indexes a duplicate of the site under the
190
+ * wrong hostname - or, for a site that has not launched, indexes the site before it exists.
191
+ * noindex also suppresses the canonical and og:url below, so the preview sends one signal rather
192
+ * than "do not index me" beside "my real address is over there". GTM is skipped on a preview for
193
+ * the same reason in the other direction: a client clicking through their review link must not
194
+ * show up in their own analytics.
192
195
  */
193
196
  const noindex = noindexProp || build.preview;
194
197
  const analyticsOn = analytics && !build.preview;
@@ -25,9 +25,11 @@ import build from 'virtual:webm/build';
25
25
 
26
26
  export const GET: APIRoute = ({ site }) => {
27
27
  /*
28
- * A BRANCH PREVIEW DISALLOWS EVERYTHING. Every page on a preview is already noindex; this is
29
- * the belt to that brace, and it is the one place Disallow is right - there is nothing on a
30
- * preview a crawler should ever fetch, and no noindex tag it needs to see.
28
+ * A PREVIEW DISALLOWS EVERYTHING - a staging site on any hostname, or a non-production branch
29
+ * of a launched one (isPreviewBuild in includes/webmonterey/config.ts). Every page on a preview
30
+ * is already noindex; this is the belt to that brace, and it is the one place Disallow is
31
+ * right - there is nothing on a preview a crawler should ever fetch, and no noindex tag it
32
+ * needs to see.
31
33
  */
32
34
  if (build.preview) {
33
35
  return new Response('User-agent: *\nDisallow: /\n', {
@@ -27,17 +27,25 @@ import { copy } from '../includes/webmonterey/copy.ts';
27
27
  import {
28
28
  AGENCY,
29
29
  creditUrl,
30
+ webmasterPageProps,
30
31
  WEBMASTER_OG_PATH,
31
32
  } from '../includes/webmonterey/webmaster/webmaster.ts';
32
33
  import { domain, hasDomain } from '../includes/webmonterey/site.ts';
33
34
  import { compact, renderJsonLd } from '../includes/webmonterey/structured-data/nodes.ts';
34
- import { pageHeader } from 'virtual:webm/registry';
35
+ import { pageHeader, webmasterPage } from 'virtual:webm/registry';
35
36
  import ogImage from 'virtual:webm/webmaster-og';
36
37
  import build from 'virtual:webm/build';
37
38
 
38
39
  const PageHeader = pageHeader;
40
+ const WebmasterPage = webmasterPage;
39
41
  const text = copy.webmaster;
40
42
  const agencyHref = hasDomain ? creditUrl(domain, 'website') : AGENCY.url;
43
+ /*
44
+ * The words, resolved once for both layouts: the intro with the agency link in it, every
45
+ * paragraph through the inline prose renderer, so `**bold**` in copy.webmaster works here
46
+ * exactly as it does in page JSON.
47
+ */
48
+ const props = webmasterPageProps(text, agencyHref);
41
49
 
42
50
  /*
43
51
  * The graph needs a real origin for stable @id values, which is the same gate the canonical tag
@@ -85,37 +93,51 @@ if (Astro.site && !build.preview) {
85
93
 
86
94
  {
87
95
  /*
88
- * The site's page header if it declares one, so this page looks like every other page on the
89
- * site; the router's plain <h1> otherwise. Same fallback the block router uses.
96
+ * THE BODY IS THE SITE'S IF IT WANTS IT. A site whose document pages - privacy, terms - use a
97
+ * richer layout than an <h1> and a stack of paragraphs exports `webmasterPage` from its
98
+ * registry and gets the merged copy handed to it: the same title, description and paragraphs
99
+ * as below, with the agency link already resolved into `intro`. Everything above this
100
+ * expression - the route, the <head>, the share image, the agency graph - stays the
101
+ * package's, so the page reads the same on every site and looks like the one it is on.
90
102
  */
91
- PageHeader ? (
92
- <PageHeader title={text.title} description={text.description} />
103
+ WebmasterPage ? (
104
+ <WebmasterPage {...props} />
93
105
  ) : (
94
- <section class="webm-section" data-space="sm">
95
- <div class="webm-container" data-width="text">
96
- <h1>{text.title}</h1>
97
- </div>
98
- </section>
99
- )
100
- }
101
-
102
- <section class="webm-section" data-space="lg">
103
- <div class="webm-container" data-width="text">
104
- <div class="webm-stack">
106
+ <Fragment>
105
107
  {
106
108
  /*
107
- * The agency is named as a link in the first paragraph - the one outbound link on the
108
- * site, followed, with the UTM parameters. Opens in a new tab because it leaves the
109
- * site; noopener without noreferrer, because the referrer is the attribution.
109
+ * The site's page header if it declares one, so this page looks like every other page
110
+ * on the site; the router's plain <h1> otherwise. Same fallback the block router uses.
110
111
  */
112
+ PageHeader ? (
113
+ <PageHeader title={text.title} description={text.description} />
114
+ ) : (
115
+ <section class="webm-section" data-space="sm">
116
+ <div class="webm-container" data-width="text">
117
+ <h1>{text.title}</h1>
118
+ </div>
119
+ </section>
120
+ )
111
121
  }
112
- <p>
113
- {text.intro.before}
114
- <a href={agencyHref} target="_blank" rel="noopener">{AGENCY.name}</a>
115
- {text.intro.after}
116
- </p>
117
- {text.body.map((paragraph) => <p>{paragraph}</p>)}
118
- </div>
119
- </div>
120
- </section>
122
+
123
+ <section class="webm-section" data-space="lg">
124
+ <div class="webm-container" data-width="text">
125
+ <div class="webm-stack">
126
+ {/*
127
+ * The agency is named as a link in the first paragraph - the one outbound link
128
+ * on the site, followed, with the UTM parameters. Opens in a new tab because it
129
+ * leaves the site; noopener without noreferrer, because the referrer is the
130
+ * attribution. See introHtml in webmaster.ts - the same string a site's own
131
+ * layout receives, so the two cannot disagree about the link.
132
+ */}
133
+ <p set:html={props.intro} />
134
+ {props.body.map((paragraph) => (
135
+ <p set:html={paragraph} />
136
+ ))}
137
+ </div>
138
+ </div>
139
+ </section>
140
+ </Fragment>
141
+ )
142
+ }
121
143
  </Base>
@@ -14,7 +14,8 @@ The relationship is a WordPress parent theme and child theme.
14
14
 
15
15
  **A fix to the package reaches this site on `npm update`.** That is the entire point of the
16
16
  package existing, and it is why the default answer to "the shared behavior is wrong" is to fix
17
- it upstream rather than to work around it here.
17
+ it upstream rather than to work around it here. Upstream means the package's own repo, in a
18
+ session opened there — never from here. Rule 12 says what a session in this repo does instead.
18
19
 
19
20
  ## The one rule that protects that
20
21
 
@@ -198,7 +199,29 @@ alone renders.
198
199
  ### 11. Never edit inside `node_modules`
199
200
 
200
201
  A change there survives until the next install and not one second longer. If the package is
201
- wrong, fix the package — see the top of this file.
202
+ wrong, the package gets fixed by a session in the package repo, not this one. See rule 12.
203
+
204
+ ### 12. A session in a client repo never edits the package
205
+
206
+ Not in `node_modules`, and not in the package's own checkout if it happens to be on this machine.
207
+ When something in the package is wrong, the deliverable from a session in this repo is a
208
+ **description of the fix** — what is wrong, where (file and line in the installed package), what
209
+ the behavior should be, and how to verify it — written as a prompt the user can run in a session
210
+ opened in the package repo. The package is versioned, tested and published on its own; this site
211
+ takes the fix with `npm update`.
212
+
213
+ This session stays inside this site's scope. An override in `design.json`, `src/styles/custom/`
214
+ or `src/actions/index.ts` is fine; reaching upstream is not.
215
+
216
+ Why this is a rule: "fix it upstream", read from inside a client repo, invited exactly the wrong
217
+ thing. The package source sat next door on the same machine, a session opened it mid-task and
218
+ edited it — a change nobody reviewed, in a repo nobody had open, on no branch, which then had to
219
+ be published before this site could even use it. The client-site session ended with the site
220
+ depending on a package version that did not exist.
221
+
222
+ `.claude/settings.json` denies `Edit` under any `node_modules/`, so the first half is mechanical
223
+ and `webm sync` keeps it that way. Claude Code has no rule syntax for "any path outside this
224
+ project", so the second half is this paragraph.
202
225
 
203
226
  ## Structure
204
227
 
@@ -208,7 +231,10 @@ webmonterey.json domain, client name, environment, features.
208
231
  src/
209
232
  components/ EVERY visible component. The package ships none.
210
233
  registry.ts maps a block `type` to its component. A block whose type is
211
- not here renders as nothing, silently.
234
+ not here renders as nothing, silently. Also exports the chrome:
235
+ `header`, `footer`, `panels`, `pageHeader`, `structuredData`, and
236
+ `webmasterPage` - the body of /webmaster, laid out like this
237
+ site's document pages, with the package's copy handed in.
212
238
  content/pages/*.json the words. One file per route; home.json is `/`.
213
239
  forms/*.json form definitions. Filename is the form id.
214
240
  actions/index.ts re-exports the package pipeline. Wrap to customize.
@@ -241,6 +267,7 @@ replaced wholesale. This client's own skills go beside it at `.claude/skills/<na
241
267
  | `/webm:start` | standing a new site up: repo, Cloudflare resources, first deploy |
242
268
  | `/webm:launch` | launch checklist: structured data, sending domain, secrets, DNS, cutover |
243
269
  | `/webm:upgrade` | taking a new package version |
270
+ | `/webm:webmaster` | laying out the package's `/webmaster` page like this site's own pages |
244
271
 
245
272
  Content edits: branch, edit `src/content/pages/*.json`, `npm run check`, push — the branch
246
273
  preview is the client's review link. Merge when approved. Never change a block's `type`.