create-website-build-kit 0.1.3 → 0.1.5
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/docs/runbook.md +5 -0
- package/template/docs/traps.md +35 -0
- package/template/package.json +4 -3
- package/template/scripts/check-env.mjs +62 -7
- package/template/scripts/check-secrets.mjs +152 -0
- package/template/scripts/tells.mjs +16 -3
- package/template/scripts/verify.mjs +1 -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.5",
|
|
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/docs/runbook.md
CHANGED
|
@@ -505,7 +505,12 @@ In this order. Steps 1–3 happen days ahead, not on launch day.
|
|
|
505
505
|
```bash
|
|
506
506
|
npm run verify -- "https://$PROD"
|
|
507
507
|
npm run check:sitemap # after a production build: nothing listed AND noindex
|
|
508
|
+
npm run check:secrets # the production worker holds every declared secret
|
|
508
509
|
```
|
|
510
|
+
|
|
511
|
+
`check:secrets` already ran as part of `deploy:production`. Run it again here because
|
|
512
|
+
go-live is when it is most likely to fail: a secret set on the staging worker is not
|
|
513
|
+
automatically on this one, and the failure is silent — leads store, nothing emails.
|
|
509
514
|
9. **Submit the sitemap** in [Search Console](https://search.google.com/search-console) and
|
|
510
515
|
[Bing Webmaster Tools](https://www.bing.com/webmasters). If you kept the old filename, the
|
|
511
516
|
existing entry keeps working and there is nothing to resubmit.
|
package/template/docs/traps.md
CHANGED
|
@@ -969,3 +969,38 @@ export removed the guesswork — prose never collides with a real export.
|
|
|
969
969
|
|
|
970
970
|
**A checker with false positives gets switched off, and then its silence means
|
|
971
971
|
"nobody looked" rather than "nothing wrong".**
|
|
972
|
+
|
|
973
|
+
### A checker that counts source AND build output counts everything twice
|
|
974
|
+
|
|
975
|
+
**Symptom:** `npm run tells` reports *"3 auto-fill/auto-fit minmax grids"* on a
|
|
976
|
+
project whose entire source contains **one**. The threshold is "more than
|
|
977
|
+
twice", so a single grid fails the check. Removing grids does not help — the
|
|
978
|
+
count only drops when you get to zero.
|
|
979
|
+
|
|
980
|
+
`tells.mjs` reads `allCss = [...styleFiles, ...distCss]`, and including the
|
|
981
|
+
built stylesheets is deliberate and right *for presence tests*: a rule that
|
|
982
|
+
never reaches the build is not a rule the site has. It is wrong for **counting**.
|
|
983
|
+
Astro inlines shared CSS into every entry bundle, so one rule in `project.css`
|
|
984
|
+
is read once from source and again from each built stylesheet:
|
|
985
|
+
|
|
986
|
+
```
|
|
987
|
+
1× src/styles/project.css
|
|
988
|
+
1× dist/client/_astro/Base.U1P5p-HP.css
|
|
989
|
+
1× dist/client/_astro/contact.D3DAZGAV.css
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
One rule, three matches, against a threshold of two.
|
|
993
|
+
|
|
994
|
+
**Fix:** count from source only. `tells.mjs` now builds a separate `sourceCss`
|
|
995
|
+
for the three tells that count rather than test presence — the grid count, the
|
|
996
|
+
long-animation count and the stripped-focus-ring count. The presence tests keep
|
|
997
|
+
reading the built CSS, because for those the build output is the point.
|
|
998
|
+
|
|
999
|
+
**Why it hid for so long:** the other two counting tells threshold at `> 0`, so
|
|
1000
|
+
duplication inflated their *reported number* without ever changing the verdict.
|
|
1001
|
+
Only the grid tell compares against a number greater than one, and only that one
|
|
1002
|
+
gave a wrong answer. The bug was in all three the whole time.
|
|
1003
|
+
|
|
1004
|
+
The general shape is worth keeping: **a checker that reads both a source and a
|
|
1005
|
+
generated copy of that source is counting the same thing more than once.** If
|
|
1006
|
+
its threshold is anything other than "any", it is wrong.
|
package/template/package.json
CHANGED
|
@@ -25,15 +25,16 @@
|
|
|
25
25
|
"recon": "node scripts/recon.mjs",
|
|
26
26
|
"extract": "node scripts/extract.mjs",
|
|
27
27
|
"dns": "node scripts/dns-snapshot.mjs",
|
|
28
|
-
"seo": "npx --yes
|
|
28
|
+
"seo": "npx --yes @nurkamol/seo-audit@1",
|
|
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
32
|
"build:staging": "PUBLIC_SITE_ENV=staging PUBLIC_SITE_URL=https://new.example.com astro build && PUBLIC_SITE_ENV=staging node scripts/staging-headers.mjs && PUBLIC_SITE_ENV=staging node scripts/check-env.mjs",
|
|
33
33
|
"build:production": "node scripts/tells.mjs --undecided-only && PUBLIC_SITE_ENV=production astro check && PUBLIC_SITE_ENV=production PUBLIC_SITE_URL=https://example.com astro build && PUBLIC_SITE_ENV=production node scripts/check-env.mjs && node scripts/check-sitemap.mjs",
|
|
34
34
|
"preview": "wrangler dev",
|
|
35
|
-
"deploy:staging": "npm run build:staging && wrangler deploy",
|
|
36
|
-
"deploy:production": "npm run build:production && wrangler deploy"
|
|
35
|
+
"deploy:staging": "npm run build:staging && wrangler deploy && node scripts/check-secrets.mjs",
|
|
36
|
+
"deploy:production": "npm run build:production && wrangler deploy && node scripts/check-secrets.mjs",
|
|
37
|
+
"check:secrets": "node scripts/check-secrets.mjs"
|
|
37
38
|
},
|
|
38
39
|
"dependencies": {
|
|
39
40
|
"@astrojs/cloudflare": "^14.1.7",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* ── THE FAILURE THIS EXISTS FOR ────────────────────────────────────────────
|
|
12
12
|
* `PUBLIC_SITE_ENV` decides indexability, canonical host, which KV namespace
|
|
13
13
|
* leads land in, and whether analytics is emitted at all. `wrangler.jsonc`
|
|
14
|
-
* decides which domains answer. Nothing connects the two
|
|
14
|
+
* decides which domains answer. Nothing connects the two — this script is the
|
|
15
|
+
* only thing that does, so it needs no per-project editing to work.
|
|
15
16
|
*
|
|
16
17
|
* At go-live, two edits have to happen together: the routes gain
|
|
17
18
|
* example.com, and the build command becomes `build:production`. Do
|
|
@@ -56,14 +57,68 @@ const patterns = (wrangler.routes ?? []).map((r) => (typeof r === 'string' ? r :
|
|
|
56
57
|
was pointed at staging-only routes. It would have blocked the cutover. */
|
|
57
58
|
const hostOf = (p) => String(p).split('/')[0];
|
|
58
59
|
/*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
60
|
+
* The production hostnames come from src/data/site.ts — the same list the site
|
|
61
|
+
* itself uses to decide indexability, canonicals and which KV namespace leads
|
|
62
|
+
* land in. NOT a copy of it.
|
|
63
|
+
*
|
|
64
|
+
* ── WHY THIS IS NOT A CONSTANT HERE ────────────────────────────────────────
|
|
65
|
+
* It used to be one, with a comment saying to keep it in step with site.ts.
|
|
66
|
+
* On the first real project it was not: site.ts had the client's domain, this
|
|
67
|
+
* file still had the template's example.com. So the guard matched nothing,
|
|
68
|
+
* called every deploy fine, and passed for the whole build — a guard that
|
|
69
|
+
* always passes is worse than none, because it reads as a check that ran.
|
|
70
|
+
*
|
|
71
|
+
* The drift WAS the failure this script exists to catch, reproduced inside the
|
|
72
|
+
* script. One source of truth is the only fix that holds; a sterner comment
|
|
73
|
+
* would not have survived the same afternoon.
|
|
74
|
+
*
|
|
75
|
+
* site.ts is TypeScript and imports `import.meta.env`, so node cannot import
|
|
76
|
+
* it. Read the literal out instead — same reasoning as stripping comments from
|
|
77
|
+
* wrangler.jsonc above rather than adding a parser.
|
|
63
78
|
*/
|
|
64
|
-
|
|
79
|
+
function readProductionHosts() {
|
|
80
|
+
let src;
|
|
81
|
+
try {
|
|
82
|
+
src = readFileSync('src/data/site.ts', 'utf8');
|
|
83
|
+
} catch {
|
|
84
|
+
/* An ENOENT stack trace is not an answer to someone mid-deploy. It also
|
|
85
|
+
usually means the script is being run from the wrong directory. */
|
|
86
|
+
console.error(
|
|
87
|
+
`\n${RED}✗ src/data/site.ts not found${RESET}\n\n` +
|
|
88
|
+
' This guard reads the production hostnames from it. Run it from the\n' +
|
|
89
|
+
' project root — `npm run build:staging` and `build:production` do.\n',
|
|
90
|
+
);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
const m = /export const PRODUCTION_HOSTS\s*=\s*\[([^\]]*)\]/.exec(src);
|
|
94
|
+
/* A guard that cannot find its own input must FAIL, never pass quietly —
|
|
95
|
+
that is the whole lesson above, and it applies to this branch too. */
|
|
96
|
+
if (!m) {
|
|
97
|
+
console.error(
|
|
98
|
+
`\n${RED}✗ cannot read PRODUCTION_HOSTS from src/data/site.ts${RESET}\n\n` +
|
|
99
|
+
' This guard derives the production hostnames from that export. Without it\n' +
|
|
100
|
+
' it cannot tell a production deploy from a staging one, so it refuses to\n' +
|
|
101
|
+
' pass rather than wave the build through.\n\n' +
|
|
102
|
+
" Expected a line like: export const PRODUCTION_HOSTS = ['example.com'] as const;\n",
|
|
103
|
+
);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
const hosts = [...m[1].matchAll(/['"]([^'"]+)['"]/g)].map((h) => h[1].toLowerCase());
|
|
107
|
+
if (!hosts.length) {
|
|
108
|
+
console.error(
|
|
109
|
+
`\n${RED}✗ PRODUCTION_HOSTS in src/data/site.ts is empty${RESET}\n\n` +
|
|
110
|
+
' Every deploy would read as staging, including the production one.\n',
|
|
111
|
+
);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
return hosts;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* Exact membership, never a suffix match — `new.example.com` ends in
|
|
118
|
+
`example.com` and is NOT production. Same rule as isProductionHost(). */
|
|
119
|
+
const PRODUCTION_HOSTS = readProductionHosts();
|
|
65
120
|
|
|
66
|
-
const isProdHost = (p) =>
|
|
121
|
+
const isProdHost = (p) => PRODUCTION_HOSTS.includes(hostOf(p).toLowerCase());
|
|
67
122
|
const routesProduction = patterns.some(isProdHost);
|
|
68
123
|
const routesStagingOnly = patterns.length > 0 && !routesProduction;
|
|
69
124
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refuse to call a deploy finished when the worker is missing a secret.
|
|
3
|
+
*
|
|
4
|
+
* npm run check:secrets
|
|
5
|
+
*
|
|
6
|
+
* Runs at the end of `deploy:staging` and `deploy:production`, after the deploy
|
|
7
|
+
* rather than before it — a worker that does not exist yet cannot be missing
|
|
8
|
+
* anything, and the first deploy is exactly when a secret has never been set.
|
|
9
|
+
*
|
|
10
|
+
* ── THE FAILURE THIS EXISTS FOR ────────────────────────────────────────────
|
|
11
|
+
* `secret()` in src/lib/runtime.ts returns `undefined` for a binding that was
|
|
12
|
+
* never set. Nothing throws. The contact form still validates, still stores the
|
|
13
|
+
* lead in KV, still returns 200, and still shows the visitor a thank-you.
|
|
14
|
+
*
|
|
15
|
+
* The API says `{"stored":true,"emailed":false}` and nobody reads API responses.
|
|
16
|
+
*
|
|
17
|
+
* So the site captures enquiries and notifies no one. There is no error, no
|
|
18
|
+
* failed request, no console warning, and nothing in the deploy log. It is found
|
|
19
|
+
* weeks later, by someone asking why the phone stopped ringing — and the leads
|
|
20
|
+
* are all still sitting in KV, which is the only reason this is recoverable.
|
|
21
|
+
*
|
|
22
|
+
* ⚠ THIS IS NOT HYPOTHETICAL. It shipped that way on the ochome build: deployed,
|
|
23
|
+
* verified green by `npm run verify`, storing leads, emailing nothing.
|
|
24
|
+
* `verify` lists it under "what this cannot see", which is honest and did not
|
|
25
|
+
* help — a note in a report nobody re-reads is not a gate.
|
|
26
|
+
*
|
|
27
|
+
* ── WHAT IT COMPARES ───────────────────────────────────────────────────────
|
|
28
|
+
* `.dev.vars.example` is the declared contract: every secret the code expects,
|
|
29
|
+
* committed, with placeholder values. `wrangler secret list` is what the
|
|
30
|
+
* deployed worker actually holds. The two disagreeing is the bug.
|
|
31
|
+
*
|
|
32
|
+
* Using the example file rather than a list hardcoded here means adding a
|
|
33
|
+
* secret to the code and to the example — which you must do anyway, or local
|
|
34
|
+
* `wrangler dev` breaks — extends this check for free. A hardcoded list would
|
|
35
|
+
* go stale silently, the way `check-env.mjs` did.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { readFileSync } from 'node:fs';
|
|
39
|
+
import { execFileSync } from 'node:child_process';
|
|
40
|
+
|
|
41
|
+
const RESET = '\x1b[0m';
|
|
42
|
+
const RED = '\x1b[31m';
|
|
43
|
+
const GREEN = '\x1b[32m';
|
|
44
|
+
const DIM = '\x1b[2m';
|
|
45
|
+
const YELLOW = '\x1b[33m';
|
|
46
|
+
|
|
47
|
+
/* `npx` is `npx.cmd` on Windows and execFileSync cannot resolve it without a
|
|
48
|
+
shell. Windows is a supported target and this is the second script to need
|
|
49
|
+
the line. */
|
|
50
|
+
const WIN = process.platform === 'win32';
|
|
51
|
+
|
|
52
|
+
/** Every NAME= in .dev.vars.example, which is the list the code expects. */
|
|
53
|
+
function declaredSecrets() {
|
|
54
|
+
let raw;
|
|
55
|
+
try {
|
|
56
|
+
raw = readFileSync('.dev.vars.example', 'utf8');
|
|
57
|
+
} catch {
|
|
58
|
+
console.error(
|
|
59
|
+
`\n${RED}✗ .dev.vars.example not found${RESET}\n\n` +
|
|
60
|
+
' It is the declared list of secrets this site needs, and this check\n' +
|
|
61
|
+
' has nothing to compare against without it. Run from the project root.\n',
|
|
62
|
+
);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
return [...raw.matchAll(/^\s*([A-Z][A-Z0-9_]*)\s*=/gm)].map((m) => m[1]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Secrets on the deployed worker.
|
|
70
|
+
*
|
|
71
|
+
* Returns null — not an empty list — when the worker does not exist yet, so a
|
|
72
|
+
* pre-deploy state is never reported as "every secret is missing".
|
|
73
|
+
*/
|
|
74
|
+
function deployedSecrets() {
|
|
75
|
+
let out;
|
|
76
|
+
try {
|
|
77
|
+
out = execFileSync(WIN ? 'npx.cmd' : 'npx', ['wrangler', 'secret', 'list'], {
|
|
78
|
+
encoding: 'utf8',
|
|
79
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
80
|
+
shell: WIN,
|
|
81
|
+
});
|
|
82
|
+
} catch (err) {
|
|
83
|
+
const text = `${err.stdout ?? ''}${err.stderr ?? ''}`;
|
|
84
|
+
/* A worker that has never been deployed, versus a real problem — being
|
|
85
|
+
logged out, or offline. Only the first is not a failure. */
|
|
86
|
+
if (/script_not_found|workers\.api\.error\.script_not_found|10007|not found/i.test(text)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
console.error(
|
|
90
|
+
`\n${RED}✗ could not read the worker's secrets${RESET}\n\n` +
|
|
91
|
+
`${text.trim().split('\n').slice(-6).map((l) => ` ${l}`).join('\n')}\n\n` +
|
|
92
|
+
' Usually `wrangler login`. This check cannot pass without an answer —\n' +
|
|
93
|
+
' it refuses rather than assume the secrets are fine.\n',
|
|
94
|
+
);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
/* wrangler prints a banner before the JSON. Take the array, not the noise. */
|
|
98
|
+
const match = out.match(/\[[\s\S]*\]/);
|
|
99
|
+
if (!match) return [];
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(match[0]).map((s) => s.name);
|
|
102
|
+
} catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const declared = declaredSecrets();
|
|
108
|
+
if (!declared.length) {
|
|
109
|
+
console.log(`${DIM}· .dev.vars.example declares no secrets — nothing to check${RESET}`);
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const deployed = deployedSecrets();
|
|
114
|
+
|
|
115
|
+
if (deployed === null) {
|
|
116
|
+
console.log(
|
|
117
|
+
`${DIM}· worker not deployed yet — nothing to check${RESET}\n` +
|
|
118
|
+
`${DIM} ${declared.length} secret(s) will be required once it is: ${declared.join(', ')}${RESET}`,
|
|
119
|
+
);
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const missing = declared.filter((name) => !deployed.includes(name));
|
|
124
|
+
const extra = deployed.filter((name) => !declared.includes(name));
|
|
125
|
+
|
|
126
|
+
if (missing.length) {
|
|
127
|
+
console.error(`\n${RED}✗ the deployed worker is missing ${missing.length} secret(s)${RESET}\n`);
|
|
128
|
+
for (const name of missing) {
|
|
129
|
+
console.error(` ${name}`);
|
|
130
|
+
}
|
|
131
|
+
console.error(
|
|
132
|
+
`\n The deploy succeeded. The site is live and INCOMPLETE — whatever reads\n` +
|
|
133
|
+
` these gets \`undefined\` and carries on silently. If BREVO_API_KEY is in\n` +
|
|
134
|
+
` the list, the form is storing leads and emailing nobody.\n\n` +
|
|
135
|
+
` Set them, then deploy again:\n\n` +
|
|
136
|
+
missing.map((n) => ` npx wrangler secret put ${n}`).join('\n') +
|
|
137
|
+
'\n',
|
|
138
|
+
);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (extra.length) {
|
|
143
|
+
/* Not a failure — a secret the code no longer reads is dead weight, not a
|
|
144
|
+
broken site. Worth saying once, because it is usually a rename that only
|
|
145
|
+
got done on one side. */
|
|
146
|
+
console.log(
|
|
147
|
+
`${YELLOW}!${RESET} on the worker but not in .dev.vars.example: ${extra.join(', ')}\n` +
|
|
148
|
+
`${DIM} either the code stopped reading it, or the example was never updated${RESET}`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log(`${GREEN}✓${RESET} every declared secret is set: ${declared.join(', ')}`);
|
|
@@ -64,6 +64,19 @@ const componentCss = componentFiles
|
|
|
64
64
|
.flatMap((source) => [...source.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g)].map((m) => m[1]))
|
|
65
65
|
.join('\n');
|
|
66
66
|
const everyCss = allCss + '\n' + componentCss;
|
|
67
|
+
|
|
68
|
+
/*
|
|
69
|
+
* ⚠ SOURCE ONLY, FOR ANYTHING THAT COUNTS. `allCss` deliberately includes the
|
|
70
|
+
* built stylesheets, which is right for presence tests — a rule that never
|
|
71
|
+
* reaches the build is not a rule the site has. It is wrong for counting: Astro
|
|
72
|
+
* inlines the same CSS into every entry bundle, so ONE rule in project.css is
|
|
73
|
+
* read once from source and again from each built stylesheet.
|
|
74
|
+
*
|
|
75
|
+
* That made the auto-fill grid tell fire on a single grid — 1 rule counted as
|
|
76
|
+
* 3, against a threshold of "more than twice". The tell was telling the truth
|
|
77
|
+
* about its own arithmetic and nothing about the site.
|
|
78
|
+
*/
|
|
79
|
+
const sourceCss = styleFiles.map(read).join('\n') + '\n' + componentCss;
|
|
67
80
|
/**
|
|
68
81
|
* Raw component source, not just its <style> blocks. Inline `style=`
|
|
69
82
|
* attributes are exactly where a card grid gets written when someone is
|
|
@@ -144,7 +157,7 @@ tell(
|
|
|
144
157
|
// "three equal cards, centred, more than twice on one page"
|
|
145
158
|
{
|
|
146
159
|
const grids = [
|
|
147
|
-
...(
|
|
160
|
+
...(sourceCss + componentSource).matchAll(/repeat\(\s*auto-(fill|fit)\s*,\s*minmax/g),
|
|
148
161
|
].length;
|
|
149
162
|
tell(
|
|
150
163
|
'the auto-fill card grid, more than twice',
|
|
@@ -185,7 +198,7 @@ tell(
|
|
|
185
198
|
|
|
186
199
|
// "any animation runs longer than ~400ms"
|
|
187
200
|
{
|
|
188
|
-
const slow = [...
|
|
201
|
+
const slow = [...sourceCss.matchAll(/(?:transition|animation)(?:-duration)?:[^;]*?(\d{3,4})ms/g)]
|
|
189
202
|
.map((m) => Number(m[1]))
|
|
190
203
|
.filter((ms) => ms > 400);
|
|
191
204
|
tell(
|
|
@@ -197,7 +210,7 @@ tell(
|
|
|
197
210
|
|
|
198
211
|
// "focus rings are the browser default, or removed"
|
|
199
212
|
{
|
|
200
|
-
const stripped = [...
|
|
213
|
+
const stripped = [...sourceCss.matchAll(/outline:\s*(none|0)\b/g)].length;
|
|
201
214
|
const restored = /:focus-visible[^{]*\{[^}]*outline:/.test(everyCss);
|
|
202
215
|
tell(
|
|
203
216
|
'focus ring removed and not replaced',
|
|
@@ -1051,7 +1051,7 @@ const warned = results.filter((r) => !r.ok && r.warn);
|
|
|
1051
1051
|
console.log(`\n${BOLD}── What this cannot see ${'─'.repeat(36)}${RESET}`);
|
|
1052
1052
|
for (const line of [
|
|
1053
1053
|
'One pageview per visit — a double-count is only visible in Realtime',
|
|
1054
|
-
'That a valid submission stores AND emails — send one by hand, once',
|
|
1054
|
+
'That a valid submission stores AND emails — send one by hand, once (`npm run check:secrets` covers whether the key is even set)',
|
|
1055
1055
|
'Whether the analytics container is the client\'s own — fetch it and read it',
|
|
1056
1056
|
'Whether a redirect target is the RIGHT page, only that it resolves',
|
|
1057
1057
|
'How fast it FEELS — weight and blocking counts are the inputs, never a timing. ' +
|