create-website-build-kit 0.1.6 → 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 +1 -1
- package/template/CLAUDE.md +9 -0
- package/template/docs/runbook.md +22 -10
- package/template/package.json +4 -3
- package/template/scripts/build.mjs +119 -0
- package/template/scripts/check-copy.mjs +135 -0
- package/template/scripts/check-sitemap.mjs +8 -2
- package/template/scripts/lib/routes.mjs +8 -2
- package/template/scripts/tells.mjs +50 -0
- package/template/src/styles/tokens.css +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-website-build-kit",
|
|
3
|
-
"version": "0.1.
|
|
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",
|
package/template/CLAUDE.md
CHANGED
|
@@ -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
|
package/template/docs/runbook.md
CHANGED
|
@@ -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. **
|
|
494
|
-
|
|
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
|
-
|
|
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
|
-
|
|
502
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
542
|
-
|
|
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
|
---
|
package/template/package.json
CHANGED
|
@@ -29,12 +29,13 @@
|
|
|
29
29
|
"redirects": "node scripts/redirects.mjs",
|
|
30
30
|
"handover": "node scripts/md-to-pdf.mjs docs/handover.md docs/handover.pdf",
|
|
31
31
|
"build": "astro build",
|
|
32
|
-
"build:staging": "
|
|
33
|
-
"build:production": "node scripts/
|
|
32
|
+
"build:staging": "node scripts/build.mjs staging",
|
|
33
|
+
"build:production": "node scripts/build.mjs production",
|
|
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",
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run a build for one environment.
|
|
3
|
+
*
|
|
4
|
+
* node scripts/build.mjs staging
|
|
5
|
+
* node scripts/build.mjs production
|
|
6
|
+
*
|
|
7
|
+
* `npm run build:staging` and `npm run build:production` are one-line aliases
|
|
8
|
+
* for these.
|
|
9
|
+
*
|
|
10
|
+
* ── WHY A SCRIPT AND NOT INLINE ENV IN package.json ────────────────────────
|
|
11
|
+
* ⚠ `PUBLIC_SITE_ENV=staging astro build` IS POSIX SHELL SYNTAX, AND npm ON
|
|
12
|
+
* WINDOWS RUNS SCRIPTS THROUGH cmd.exe. There it is not an assignment, it is
|
|
13
|
+
* a command name, and the build dies immediately with
|
|
14
|
+
*
|
|
15
|
+
* 'PUBLIC_SITE_ENV' is not recognized as an internal or external command
|
|
16
|
+
*
|
|
17
|
+
* So the two most important commands in the kit did not work at all on a
|
|
18
|
+
* platform the kit says it supports. Nothing caught it because every CI job
|
|
19
|
+
* ran on ubuntu.
|
|
20
|
+
*
|
|
21
|
+
* ── AND WHY IT IS SAFER EVEN WHERE THE SHELL SYNTAX WORKS ──────────────────
|
|
22
|
+
* The production line repeated `PUBLIC_SITE_ENV=production` four times, once
|
|
23
|
+
* per command. Miss one and that step runs as `development` while the others
|
|
24
|
+
* do not: `astro check` types a different environment than the one that gets
|
|
25
|
+
* built, or `check-env` validates an environment nobody deployed.
|
|
26
|
+
*
|
|
27
|
+
* A mixed-environment build is precisely what `check-env.mjs` exists to catch,
|
|
28
|
+
* and it is the sort of thing that survives review because every line looks
|
|
29
|
+
* right on its own. Setting it once removes the class.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { spawnSync } from 'node:child_process';
|
|
33
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
34
|
+
|
|
35
|
+
const RESET = '\x1b[0m';
|
|
36
|
+
const RED = '\x1b[31m';
|
|
37
|
+
const DIM = '\x1b[2m';
|
|
38
|
+
|
|
39
|
+
const env = process.argv[2];
|
|
40
|
+
|
|
41
|
+
if (env !== 'staging' && env !== 'production') {
|
|
42
|
+
console.error(
|
|
43
|
+
`\n${RED}✗ usage: node scripts/build.mjs <staging|production>${RESET}\n\n` +
|
|
44
|
+
' The environment is not optional. A build that does not declare one\n' +
|
|
45
|
+
' emits localhost canonical URLs — cleanly, and wrong.\n',
|
|
46
|
+
);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/*
|
|
51
|
+
* The site URL per environment. Read from package.json rather than restated
|
|
52
|
+
* here, so there is one place a project sets its hostnames.
|
|
53
|
+
*
|
|
54
|
+
* ⚠ Kept OUT of src/data/site.ts on purpose: astro.config.mjs needs the value
|
|
55
|
+
* before any TypeScript is loaded, and site.ts imports `import.meta.env`.
|
|
56
|
+
*/
|
|
57
|
+
const SITE_URLS = {
|
|
58
|
+
staging: 'https://new.example.com',
|
|
59
|
+
production: 'https://example.com',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Run one step with the environment already set, and stop the build if it
|
|
64
|
+
* fails. `shell: true` on Windows because `npx`-style shims are .cmd files
|
|
65
|
+
* that execFile cannot resolve — the same reason the a11y scripts need it.
|
|
66
|
+
*/
|
|
67
|
+
function step(command, args) {
|
|
68
|
+
const run = spawnSync(command, args, {
|
|
69
|
+
stdio: 'inherit',
|
|
70
|
+
env: { ...process.env, PUBLIC_SITE_ENV: env, PUBLIC_SITE_URL: SITE_URLS[env] },
|
|
71
|
+
shell: process.platform === 'win32',
|
|
72
|
+
});
|
|
73
|
+
if (run.status !== 0) {
|
|
74
|
+
console.error(`\n${RED}✗ build failed at:${RESET} ${command} ${args.join(' ')}\n`);
|
|
75
|
+
process.exit(run.status ?? 1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* astro is a local binary; go through the package's own bin rather than a
|
|
80
|
+
global `astro`, which may not exist and would be the wrong version if it did. */
|
|
81
|
+
const astro = ['node_modules/astro/astro.js'];
|
|
82
|
+
const hasLocalAstro = existsSync('node_modules/astro/astro.js');
|
|
83
|
+
|
|
84
|
+
const run = (args) =>
|
|
85
|
+
hasLocalAstro
|
|
86
|
+
? step(process.execPath, [...astro, ...args])
|
|
87
|
+
: step('npx', ['--no-install', 'astro', ...args]);
|
|
88
|
+
|
|
89
|
+
console.log(`${DIM}building ${env} → ${SITE_URLS[env]}${RESET}\n`);
|
|
90
|
+
|
|
91
|
+
if (env === 'production') {
|
|
92
|
+
/* Refuse a production build of a template that has not been designed yet.
|
|
93
|
+
Before anything else, because it is the cheapest check and the most
|
|
94
|
+
embarrassing thing to deploy. */
|
|
95
|
+
step(process.execPath, ['scripts/tells.mjs', '--undecided-only']);
|
|
96
|
+
run(['check']);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
run(['build']);
|
|
100
|
+
|
|
101
|
+
if (env === 'staging') {
|
|
102
|
+
step(process.execPath, ['scripts/staging-headers.mjs']);
|
|
103
|
+
}
|
|
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
|
+
|
|
110
|
+
step(process.execPath, ['scripts/check-env.mjs']);
|
|
111
|
+
|
|
112
|
+
if (env === 'production') {
|
|
113
|
+
step(process.execPath, ['scripts/check-sitemap.mjs']);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* A sanity line, so the log says which environment actually ran rather than
|
|
117
|
+
which one was asked for. They are the same now; they were not always. */
|
|
118
|
+
const pkg = existsSync('package.json') ? JSON.parse(readFileSync('package.json', 'utf8')) : {};
|
|
119
|
+
console.log(`\n${DIM}${pkg.name ?? 'site'} built as ${env}${RESET}`);
|
|
@@ -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);
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
31
|
-
import { join, relative } from 'node:path';
|
|
31
|
+
import { join, relative, sep } from 'node:path';
|
|
32
32
|
|
|
33
33
|
const RESET = '[0m';
|
|
34
34
|
const RED = '[31m';
|
|
@@ -78,7 +78,13 @@ const ROBOTS_META = /<meta[^>]+name=["']robots["'][^>]+content=["'][^"']*\bnoind
|
|
|
78
78
|
const noindexed = walk(root)
|
|
79
79
|
.filter((f) => f.endsWith('.html'))
|
|
80
80
|
.filter((f) => ROBOTS_META.test(readFileSync(f, 'utf8')))
|
|
81
|
-
|
|
81
|
+
/* ⚠ SEPARATORS NORMALISED — `relative()` RETURNS BACKSLASHES ON WINDOWS.
|
|
82
|
+
A URL path is always `/`. Without this the map produced `/about\\` for
|
|
83
|
+
`about\\index.html`, which matched nothing: check-sitemap's contradiction
|
|
84
|
+
check silently passed a site that listed a noindexed URL in its sitemap, and
|
|
85
|
+
Search Console reports that as an error against the whole submission. */
|
|
86
|
+
.map((f) => '/' + relative(root, f).split(sep).join('/')
|
|
87
|
+
.replace(/index\.html$/, '').replace(/\.html$/, '/'))
|
|
82
88
|
.map((p) => (p.endsWith('/') ? p : `${p}/`));
|
|
83
89
|
|
|
84
90
|
const contradictions = noindexed.filter((p) => listed.has(p));
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
14
|
-
import { join, relative } from 'node:path';
|
|
14
|
+
import { join, relative, sep } from 'node:path';
|
|
15
15
|
|
|
16
16
|
/** Routes this build emitted, as absolute paths. 404 is excluded — it is not a route. */
|
|
17
17
|
export function routesFromDist() {
|
|
@@ -26,7 +26,13 @@ export function routesFromDist() {
|
|
|
26
26
|
|
|
27
27
|
return walk(root)
|
|
28
28
|
.filter((f) => f.endsWith('.html') && !f.endsWith('404.html'))
|
|
29
|
-
|
|
29
|
+
/* ⚠ SEPARATORS NORMALISED — `relative()` RETURNS BACKSLASHES ON WINDOWS.
|
|
30
|
+
A URL path is always `/`. Without this the map produced `/about\\` for
|
|
31
|
+
`about\\index.html`, which matched nothing: check-sitemap's contradiction
|
|
32
|
+
check silently passed a site that listed a noindexed URL in its sitemap, and
|
|
33
|
+
Search Console reports that as an error against the whole submission. */
|
|
34
|
+
.map((f) => '/' + relative(root, f).split(sep).join('/')
|
|
35
|
+
.replace(/index\.html$/, '').replace(/\.html$/, '/'))
|
|
30
36
|
.map((p) => (p.endsWith('/') ? p : `${p}/`))
|
|
31
37
|
.sort();
|
|
32
38
|
}
|
|
@@ -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
|
-
|
|
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;
|