@cparkerwebm/webmonterey 1.2.0 → 1.3.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.
@@ -3,9 +3,11 @@ import assert from 'node:assert/strict';
3
3
  import {
4
4
  APP_DIR,
5
5
  isConfigured,
6
+ isPreviewBuild,
6
7
  isStagingDeployment,
7
8
  isValidTimeZone,
8
9
  PLACEHOLDER,
10
+ previewReason,
9
11
  resolveAppPath,
10
12
  resolveDisplayName,
11
13
  workerFirstPaths,
@@ -92,3 +94,52 @@ test('shortName falls back to the display name when unset', () => {
92
94
  assert.equal(isConfigured('FoML'), true);
93
95
  assert.equal(isConfigured(''), false);
94
96
  });
97
+
98
+ /* --- preview builds ------------------------------------------------------ */
99
+
100
+ test('a staging site is a preview with no branch at all - the laptop build', () => {
101
+ assert.equal(isPreviewBuild({ environment: 'staging', branch: null }), true);
102
+ assert.equal(previewReason({ environment: 'staging', branch: null }), 'staging');
103
+ });
104
+
105
+ test('a staging site is a preview on main - the case that was crawlable', () => {
106
+ /* autire.webmonterey.workers.dev: main on Workers Builds, environment staging, and indexable
107
+ * because only a non-production BRANCH used to be a preview. */
108
+ assert.equal(isPreviewBuild({ environment: 'staging', branch: 'main' }), true);
109
+ assert.equal(previewReason({ environment: 'staging', branch: 'main' }), 'staging');
110
+ });
111
+
112
+ test('a production site with no branch is production output', () => {
113
+ assert.equal(isPreviewBuild({ environment: 'production', branch: null }), false);
114
+ assert.equal(previewReason({ environment: 'production', branch: null }), null);
115
+ });
116
+
117
+ test('a production site on main is production output', () => {
118
+ assert.equal(isPreviewBuild({ environment: 'production', branch: 'main' }), false);
119
+ });
120
+
121
+ test('a feature branch of a production site is still a preview', () => {
122
+ /* webmonterey.json is committed, so the branch inherits production from main. The branch rule
123
+ * is what keeps a launched site's review links out of the index. */
124
+ assert.equal(isPreviewBuild({ environment: 'production', branch: 'feature/x' }), true);
125
+ assert.equal(previewReason({ environment: 'production', branch: 'feature/x' }), 'branch');
126
+ });
127
+
128
+ test('productionBranch renames which branch is production', () => {
129
+ const on = { environment: 'production', productionBranch: 'release' } as const;
130
+ assert.equal(isPreviewBuild({ ...on, branch: 'release' }), false);
131
+ assert.equal(isPreviewBuild({ ...on, branch: 'main' }), true);
132
+ });
133
+
134
+ test('staging wins over the production branch, whatever it is called', () => {
135
+ assert.equal(
136
+ previewReason({ environment: 'staging', branch: 'release', productionBranch: 'release' }),
137
+ 'staging',
138
+ );
139
+ });
140
+
141
+ test('an unset environment is production, so a site predating the field builds as before', () => {
142
+ assert.equal(isPreviewBuild({ environment: undefined, branch: null }), false);
143
+ assert.equal(isPreviewBuild({ environment: undefined, branch: 'main' }), false);
144
+ assert.equal(isPreviewBuild({ environment: undefined, branch: 'feature/x' }), true);
145
+ });
@@ -125,6 +125,14 @@ export interface SiteConfig {
125
125
  * Read it anywhere via `environment`, `isStaging` and `isProduction` from webmonterey/site.
126
126
  * The first consumer is transactional email, which redirects every recipient to `stagingEmail`
127
127
  * on a staging deployment rather than mailing the client's real contacts from a preview.
128
+ *
129
+ * THE SECOND CONSUMER IS INDEXABILITY. A staging site is a PREVIEW BUILD on every hostname and
130
+ * in every build - laptop, `main` on Workers Builds, anywhere: every page is noindex with no
131
+ * canonical, there is no sitemap, robots.txt disallows everything, and Google Tag Manager does
132
+ * not load. Until this switch existed a site that had not launched was crawlable on its
133
+ * workers.dev URL the moment `main` deployed, because only a non-production BRANCH was a
134
+ * preview. See `isPreviewBuild`. Flipping this to production is therefore what makes a site
135
+ * indexable, which is why /webm:launch does it only once the custom domain is live.
128
136
  */
129
137
  environment?: 'production' | 'staging';
130
138
 
@@ -344,3 +352,44 @@ export function isStagingDeployment(
344
352
  /* Match the label, not a substring: a client domain ending "notworkers.dev" is not a preview. */
345
353
  return Boolean(hostname && /(^|\.)workers\.dev$/i.test(hostname));
346
354
  }
355
+
356
+ /** Which signal made a build a preview, or null for production output. */
357
+ export type PreviewReason = 'staging' | 'branch' | null;
358
+
359
+ /**
360
+ * Why this build is a preview - noindex on every page, no canonical, no sitemap, robots.txt
361
+ * disallowing everything, no analytics - or null when it is production output.
362
+ *
363
+ * TWO INDEPENDENT SIGNALS, for the same reason `isStagingDeployment` has two.
364
+ *
365
+ * `environment` is what the deployment is FOR. A site that has not launched says `staging`, and
366
+ * a site that has not launched must not be indexable ANYWHERE: not on a feature branch, not on
367
+ * `main`, not from a laptop. Before this signal existed only a non-production branch was a
368
+ * preview, so `main` on Workers Builds was production output for every site that had not gone
369
+ * live yet - indexable pages with a canonical, a sitemap and `Allow: /` on a public workers.dev
370
+ * hostname. autire.webmonterey.workers.dev was crawlable that way.
371
+ *
372
+ * The branch covers the opposite case: webmonterey.json is committed, so a feature branch of a
373
+ * LAUNCHED site inherits `production` from main and would build indexable pages under a review
374
+ * URL. Workers Builds injects WORKERS_CI_BRANCH; anything other than the production branch is a
375
+ * preview whatever the config says.
376
+ *
377
+ * An unset environment is production, as it is everywhere else - see SiteConfig.environment for
378
+ * why that default is the safe one - so a site predating the field builds exactly as before.
379
+ * `branch` is null when nothing injected one, which is every laptop build.
380
+ */
381
+ export function previewReason(input: {
382
+ environment: SiteConfig['environment'] | undefined;
383
+ branch: string | null | undefined;
384
+ productionBranch?: string;
385
+ }): PreviewReason {
386
+ if (input.environment === 'staging') return 'staging';
387
+ const branch = input.branch ?? null;
388
+ if (branch !== null && branch !== (input.productionBranch ?? 'main')) return 'branch';
389
+ return null;
390
+ }
391
+
392
+ /** Whether this build is a preview. `previewReason` says which signal decided it. */
393
+ export function isPreviewBuild(input: Parameters<typeof previewReason>[0]): boolean {
394
+ return previewReason(input) !== null;
395
+ }
@@ -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):
@@ -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
 
@@ -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', {
@@ -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