create-website-build-kit 0.1.7 → 0.1.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-website-build-kit",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Scaffold a production marketing site \u2014 Astro on Cloudflare Workers, with the gates, the migration playbook and the accessibility work already wired.",
5
5
  "keywords": [
6
6
  "astro",
@@ -11,6 +11,7 @@ npm run media # after adding or replacing any ima
11
11
  npm run build:staging && npx wrangler dev # localhost:8788, real KV + secrets
12
12
  npm run a11y # accessibility check, one URL per family
13
13
  npm run tells # what is undecided, and the design tells
14
+ npm run check:copy # author notes that reached the rendered page
14
15
  npm run recon -- https://old-site.com # inventory the old site BEFORE designing routes
15
16
  npm run dns -- old-site.com # capture the zone. MX loss kills client email
16
17
  npm run seo -- https://old-site.com # optional: SEO baseline to diff after cutover
@@ -93,6 +94,14 @@ The rules whose failure looks like success — double-counted pageviews, a conve
93
94
  two pipes, a trigger on `sent=1` that catches almost nothing — are in `docs/analytics.md`.
94
95
  Read it before adding any tag.
95
96
 
97
+ **Notes to yourself never ship.** `check:copy` reads the text a browser would show — not the
98
+ source, not comments, not `<script>` — and looks for the markers people actually leave: `TODO`,
99
+ `FIXME`, `⚠ CONFIRM:`, `Lorem ipsum`, an unrendered `{{ placeholder }}`. It warns on staging and
100
+ **refuses on production**, because a note is normal while building and unacceptable at go-live.
101
+ One shipped as body copy on a service page — *"⚠ CONFIRM: the old site advertised classes every
102
+ Saturday at 9am…"* — past clean types, clean axe and clean tells. **The question is usually real:
103
+ move it to `BUILD-STATE.md`, do not just delete it.**
104
+
96
105
  **Evidence, not assertions.** `npm run a11y:evidence` writes a dated pack to
97
106
  `docs/a11y-evidence/` — commit it. It fills in the machine half and leaves the keyboard,
98
107
  screen-reader and forms passes **blank**, because no tool does those; a pack with them still
@@ -287,6 +287,7 @@ degrades into. Run the script; keep the commands below for when you need to see
287
287
  | --- | --- |
288
288
  | One pageview per visit | Only visible in analytics Realtime |
289
289
  | A valid submission stores **and** emails | Sending one creates a real lead and a real notification |
290
+ | **Data the client may not hold is dropped** | Only if `compliance.md` §10 named something — PHI, GDPR special categories, card numbers. Craft a submission containing exactly it, send it, then read the stored record back. The form not *asking* is not the control; the message box accepts anything. `verify` cannot do this: it would need the export token and the record schema |
290
291
  | The analytics container is the client's own | Fetch `gtm.js` and read it — see `analytics.md` |
291
292
  | A redirect lands on the **right** page | The script proves it resolves, not that it is correct |
292
293
  | It looks right on a phone | — |
@@ -490,16 +491,27 @@ In this order. Steps 1–3 happen days ahead, not on launch day.
490
491
  3. **Move Search Console verification to DNS TXT** if it currently relies on an HTML file, and
491
492
  confirm it still shows verified. File-based verification breaks the moment the file stops
492
493
  resolving, and losing verification loses the property's history.
493
- 4. **Full verification matrix against staging.** Every row.
494
- 5. **Deploy production** `npm run deploy:production`. Read the bindings table in the output;
494
+ 4. **Name what enforces every retention period the privacy notice states.** KV is done for you —
495
+ `leadRetentionDays` becomes an `expirationTtl` and the store enforces it. **Anything else is
496
+ not.** R2 keeps uploaded files forever unless the bucket has a **lifecycle rule**, and that
497
+ rule lives in the Cloudflare dashboard, so nothing in the repo and no gate here can see it.
498
+ D1 needs a scheduled delete you wrote.
499
+
500
+ ⚠ A notice claiming a period nothing enforces is a false statement, and it fails silently:
501
+ clean build, clean deploy, correct-looking policy, data still there a year later. Caught on a
502
+ real build where résumés went to R2 while the retention value drove only the KV record and the
503
+ page copy. If you cannot name the mechanism, build it or change the notice —
504
+ `stacks.md` §6.
505
+ 5. **Full verification matrix against staging.** Every row.
506
+ 6. **Deploy production** — `npm run deploy:production`. Read the bindings table in the output;
495
507
  it is the only visible signal that the right environment was built.
496
- 6. **Cut DNS over.** Watch, do not assume:
508
+ 7. **Cut DNS over.** Watch, do not assume:
497
509
  ```bash
498
510
  dig +short $PROD; dig +short www.$PROD
499
511
  curl -sI "https://$PROD/" | head -3
500
512
  ```
501
- 7. **Remove the staging route** from the worker, or staging becomes an indexable duplicate.
502
- 8. **Re-run the matrix against production**, including `robots.txt` (must now allow) and
513
+ 8. **Remove the staging route** from the worker, or staging becomes an indexable duplicate.
514
+ 9. **Re-run the matrix against production**, including `robots.txt` (must now allow) and
503
515
  `noindex` (must now be absent).
504
516
 
505
517
  ```bash
@@ -511,7 +523,7 @@ In this order. Steps 1–3 happen days ahead, not on launch day.
511
523
  `check:secrets` already ran as part of `deploy:production`. Run it again here because
512
524
  go-live is when it is most likely to fail: a secret set on the staging worker is not
513
525
  automatically on this one, and the failure is silent — leads store, nothing emails.
514
- 9. **Submit the sitemap** in [Search Console](https://search.google.com/search-console) and
526
+ 10. **Submit the sitemap** in [Search Console](https://search.google.com/search-console) and
515
527
  [Bing Webmaster Tools](https://www.bing.com/webmasters). If you kept the old filename, the
516
528
  existing entry keeps working and there is nothing to resubmit.
517
529
 
@@ -526,7 +538,7 @@ In this order. Steps 1–3 happen days ahead, not on launch day.
526
538
  submission and rejects the whole batch with a 403 if it is not live yet. And it is Bing,
527
539
  Yandex, Seznam and Naver: **Google does not participate**, so this is never the reason a
528
540
  page is or is not in Google. The script prints both of those on every run.
529
- 10. **Diff the zone against the capture.** The one step that catches a launch taking the
541
+ 11. **Diff the zone against the capture.** The one step that catches a launch taking the
530
542
  client's email with it:
531
543
 
532
544
  ```bash
@@ -536,10 +548,10 @@ In this order. Steps 1–3 happen days ahead, not on launch day.
536
548
  MX, SPF, DMARC, verification TXT and nameservers, compared against what was published
537
549
  before you touched anything. A dead site gets a phone call; dead email is silent.
538
550
 
539
- 11. **Send one real enquiry through the live form** and confirm the client received it in the
551
+ 12. **Send one real enquiry through the live form** and confirm the client received it in the
540
552
  inbox they actually read.
541
- 12. **Restore the DNS TTL** to something sane (3600s).
542
- 13. **Point the uptime monitor at a real page and the form endpoint** — not just the homepage.
553
+ 13. **Restore the DNS TTL** to something sane (3600s).
554
+ 14. **Point the uptime monitor at a real page and the form endpoint** — not just the homepage.
543
555
  The endpoint is what breaks.
544
556
 
545
557
  ---
@@ -34,7 +34,8 @@
34
34
  "preview": "wrangler dev",
35
35
  "deploy:staging": "npm run build:staging && wrangler deploy && node scripts/check-secrets.mjs",
36
36
  "deploy:production": "npm run build:production && wrangler deploy && node scripts/check-secrets.mjs",
37
- "check:secrets": "node scripts/check-secrets.mjs"
37
+ "check:secrets": "node scripts/check-secrets.mjs",
38
+ "check:copy": "node scripts/check-copy.mjs"
38
39
  },
39
40
  "dependencies": {
40
41
  "@astrojs/cloudflare": "^14.1.7",
@@ -102,6 +102,11 @@ if (env === 'staging') {
102
102
  step(process.execPath, ['scripts/staging-headers.mjs']);
103
103
  }
104
104
 
105
+ /* Warn on staging, refuse on production. A note in the copy is normal WHILE
106
+ building and unacceptable at go-live — the same split as
107
+ `tells --undecided-only`. */
108
+ step(process.execPath, ['scripts/check-copy.mjs', ...(env === 'production' ? ['--strict'] : [])]);
109
+
105
110
  step(process.execPath, ['scripts/check-env.mjs']);
106
111
 
107
112
  if (env === 'production') {
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Refuse to ship a build whose visible copy still talks to the author.
3
+ *
4
+ * node scripts/check-copy.mjs # warn
5
+ * node scripts/check-copy.mjs --strict # exit 1 on any marker
6
+ *
7
+ * `build:production` runs it strict; `build:staging` runs it as a warning,
8
+ * because a note in the copy is normal WHILE building and unacceptable at
9
+ * go-live. Same split as `tells --undecided-only`.
10
+ *
11
+ * ── THE FAILURE THIS EXISTS FOR ────────────────────────────────────────────
12
+ * A real build shipped this, on a service page, as body text a parent would
13
+ * read:
14
+ *
15
+ * "⚠ CONFIRM: the old site advertised classes every Saturday at 9am. Does
16
+ * this continue under the concierge model? Emitting a class time nobody is
17
+ * running is a locked door."
18
+ *
19
+ * It was written inline in the content file while drafting, and every gate
20
+ * passed over it: `astro check` clean, axe clean, `tells` clean, links fine.
21
+ * Nothing in a build can tell a sentence meant for the client from a sentence
22
+ * meant for the reader — except a list of the markers people actually use.
23
+ *
24
+ * ⚠ THE QUESTION IS USUALLY REAL. The fix is not to delete the note, it is to
25
+ * move it to `BUILD-STATE.md` where the other open questions live. A build
26
+ * record is read before go-live; a paragraph on a service page is read by a
27
+ * customer.
28
+ *
29
+ * ── WHY THIS LOOKS AT RENDERED TEXT, NOT THE FILE ──────────────────────────
30
+ * Searching source would flag every code comment that mentions TODO, and
31
+ * searching raw HTML would flag markers inside `<script>` and inside HTML
32
+ * comments — which are invisible to a reader and often deliberate. The check
33
+ * runs on the text a browser would show, plus JSON-LD, because a placeholder in
34
+ * structured data is quoted straight back by Google.
35
+ *
36
+ * `check-sitemap.mjs` carries the same lesson in the opposite direction: match
37
+ * the thing, not the word, or you train people to ignore the check.
38
+ */
39
+
40
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
41
+ import { join, relative, sep } from 'node:path';
42
+
43
+ const RESET = '\x1b[0m';
44
+ const RED = '\x1b[31m';
45
+ const GREEN = '\x1b[32m';
46
+ const YELLOW = '\x1b[33m';
47
+ const DIM = '\x1b[2m';
48
+
49
+ const strict = process.argv.includes('--strict');
50
+
51
+ const root = ['dist/client', 'dist'].find((d) => existsSync(d));
52
+ if (!root) {
53
+ console.error('check-copy: no dist/ — run a build first.');
54
+ process.exit(1);
55
+ }
56
+
57
+ /*
58
+ * ⚠ EVERY PATTERN HERE IS DELIBERATELY NARROW.
59
+ *
60
+ * `TODO` in caps is never ordinary prose. Lowercase "todo" is a Spanish word
61
+ * and an English noun. `CONFIRM` needs its colon or its warning sign, because
62
+ * "confirm your email address" is a real sentence a form says. Bare `TK` is
63
+ * excluded entirely — two capitals appear inside acronyms and product names,
64
+ * and a check that fires on those is a check people switch off.
65
+ */
66
+ const MARKERS = [
67
+ { name: 'TODO', re: /\bTODO\b/ },
68
+ { name: 'FIXME', re: /\bFIXME\b/ },
69
+ { name: 'XXX', re: /\bXXX\b/ },
70
+ { name: 'TKTK', re: /\bTKTK\b/ },
71
+ { name: 'CONFIRM:', re: /(?:⚠\s*)?\bCONFIRM\s*:/ },
72
+ { name: 'placeholder text', re: /\bLorem ipsum\b/i },
73
+ { name: 'unresolved template', re: /\{\{\s*[\w.]+\s*\}\}/ },
74
+ ];
75
+
76
+ const walk = (dir) =>
77
+ readdirSync(dir).flatMap((e) => {
78
+ const full = join(dir, e);
79
+ return statSync(full).isDirectory() ? walk(full) : [full];
80
+ });
81
+
82
+ /** What a reader actually sees, plus structured data. Never script or comments. */
83
+ function visibleText(html) {
84
+ const jsonLd = [...html.matchAll(/<script[^>]+application\/ld\+json[^>]*>([\s\S]*?)<\/script>/gi)]
85
+ .map((m) => m[1])
86
+ .join('\n');
87
+
88
+ const body = html
89
+ .replace(/<!--[\s\S]*?-->/g, ' ')
90
+ .replace(/<script[\s\S]*?<\/script>/gi, ' ')
91
+ .replace(/<style[\s\S]*?<\/style>/gi, ' ')
92
+ .replace(/<[^>]+>/g, ' ');
93
+
94
+ return `${body}\n${jsonLd}`;
95
+ }
96
+
97
+ const hits = [];
98
+
99
+ for (const file of walk(root).filter((f) => f.endsWith('.html'))) {
100
+ const text = visibleText(readFileSync(file, 'utf8'));
101
+ for (const { name, re } of MARKERS) {
102
+ if (!re.test(text)) continue;
103
+ /* Show the sentence, so the reader can judge it without opening the file. */
104
+ const at = text.search(re);
105
+ const context = text
106
+ .slice(Math.max(0, at - 60), at + 120)
107
+ .replace(/\s+/g, ' ')
108
+ .trim();
109
+ hits.push({
110
+ route: '/' + relative(root, file).split(sep).join('/').replace(/index\.html$/, ''),
111
+ name,
112
+ context,
113
+ });
114
+ }
115
+ }
116
+
117
+ if (!hits.length) {
118
+ console.log(`${GREEN}✓${RESET} no author notes in the rendered copy`);
119
+ process.exit(0);
120
+ }
121
+
122
+ const label = strict ? `${RED}✗${RESET}` : `${YELLOW}!${RESET}`;
123
+ console[strict ? 'error' : 'log'](
124
+ `\n${label} ${hits.length} author marker(s) in text a reader would see\n`,
125
+ );
126
+ for (const h of hits) {
127
+ console[strict ? 'error' : 'log'](` ${h.route} ${DIM}[${h.name}]${RESET}\n …${h.context}…`);
128
+ }
129
+
130
+ console[strict ? 'error' : 'log'](
131
+ `\n ${DIM}The question is usually real. Move it to BUILD-STATE.md, where the other\n` +
132
+ ` open questions live and someone reads it before go-live — do not just delete it.${RESET}\n`,
133
+ );
134
+
135
+ process.exit(strict ? 1 : 0);
@@ -313,6 +313,56 @@ tell(
313
313
  );
314
314
  }
315
315
 
316
+ /*
317
+ * Two rows adopted after running pbakaus/impeccable's detector over a real
318
+ * build. Both are signals `tells` did not have; both needed an exclusion its
319
+ * version does not have, and the exclusions are the reason they are usable.
320
+ */
321
+
322
+ /*
323
+ * "bounce and overshoot easing" — a cubic-bezier whose control points leave
324
+ * [0,1] on the y axis overshoots the target and springs back.
325
+ *
326
+ * ⚠ NOT ALWAYS WRONG, which is why it is a tell and not an error. Overshoot is
327
+ * right when the gesture itself carried momentum — a flick, a drag release.
328
+ * It is wrong on a menu that faded in, where nothing threw it.
329
+ */
330
+ {
331
+ const overshoot = [...sourceCss.matchAll(/cubic-bezier\(([^)]+)\)/g)]
332
+ .map((m) => m[1].split(',').map((n) => Number(n.trim())))
333
+ .filter((p) => p.length === 4 && p.every((n) => Number.isFinite(n)))
334
+ .filter(([, y1, , y2]) => y1 > 1 || y2 > 1 || y1 < 0 || y2 < 0);
335
+ tell(
336
+ 'bounce or overshoot easing',
337
+ overshoot.length > 0,
338
+ `${overshoot.length} easing curve(s) overshoot. Right after a flick or a drag release, where momentum came from the gesture; dated on anything that simply appeared.`,
339
+ );
340
+ }
341
+
342
+ /*
343
+ * "a thick accent bar down one side of a card" — border-inline-start (or
344
+ * border-left) at 3px or more.
345
+ *
346
+ * ⚠ BLOCKQUOTES ARE EXCLUDED, AND THAT EXCLUSION IS THE WHOLE ROW. A rule
347
+ * flagged `.prose blockquote { border-inline-start: 3px solid }` on a real
348
+ * build — a left rule on a quotation is a typographic convention older than
349
+ * the web, and reporting it would teach people to skim past the row. The
350
+ * pattern actually meant is a notice or card wearing a coloured tab.
351
+ */
352
+ {
353
+ const bars = [...sourceCss.matchAll(/([^{}]+)\{([^{}]*)\}/g)]
354
+ .filter(([, selector]) => !/blockquote|\bquote\b|\bcite\b/i.test(selector))
355
+ .filter(([, , body]) => {
356
+ const m = /border-(?:inline-start|left):\s*([\d.]+)px\s+solid/.exec(body);
357
+ return m && Number(m[1]) >= 3;
358
+ });
359
+ tell(
360
+ 'a thick accent bar down one side',
361
+ bars.length > 1,
362
+ `${bars.length} rule(s) put a 3px+ coloured border on one edge. On a card or a notice it is one of the most recognisable generated-UI tells. Blockquotes are excluded — a rule beside a quotation is a convention, not a tell.`,
363
+ );
364
+ }
365
+
316
366
  // "the 404, the empty state or the form's invalid state was never designed"
317
367
  tell(
318
368
  'no invalid / busy form state',
@@ -189,7 +189,12 @@
189
189
  --dur-slow: 420ms;
190
190
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
191
191
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
192
- --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
192
+ /* ⚠ NO SPRING TOKEN, DELIBERATELY. An overshoot curve is a look, and the
193
+ template does not ship looks — it shipped one anyway, unused, until a
194
+ detector found it in a client project that had inherited the dead token
195
+ without ever referencing it. Add one where a gesture carries real
196
+ momentum, and only there: a flick or a drag release. Nothing that merely
197
+ appeared should spring. */
193
198
 
194
199
  /* ── Chrome ────────────────────────────────────────────────────────────── */
195
200
  --header-h: 4.5rem;