create-website-build-kit 0.1.7 → 0.1.9
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 +2 -1
- package/template/scripts/build.mjs +5 -0
- package/template/scripts/check-copy.mjs +135 -0
- package/template/scripts/tells.mjs +50 -0
- package/template/src/components/Header.astro +16 -1
- package/template/src/components/PageHero.astro +26 -1
- package/template/src/styles/global.css +10 -0
- package/template/src/styles/tokens.css +11 -1
- package/template/wrangler.jsonc +15 -0
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.9",
|
|
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
|
@@ -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',
|
|
@@ -29,7 +29,22 @@ const isCurrent = (href: string) => (href === '/' ? path === '/' : path.startsWi
|
|
|
29
29
|
{/* Text until there is a logo. A brand mark referenced before the file
|
|
30
30
|
exists renders as a broken image on every page, which is worse than
|
|
31
31
|
plain type — swap this for <img> once the artwork is in place, and
|
|
32
|
-
look at it on both a light and a dark background before shipping.
|
|
32
|
+
look at it on both a light and a dark background before shipping.
|
|
33
|
+
|
|
34
|
+
⚠ SIZE IT BY HEIGHT, NOT WIDTH, AND KEEP IT UNDER --header-h.
|
|
35
|
+
`inline-size: 12rem; block-size: auto` on a wide mark computes to
|
|
36
|
+
whatever its aspect ratio says — on a real build a 520×227 logo came
|
|
37
|
+
out 87px tall in an 88px bar, so the LOGO was setting the header's
|
|
38
|
+
height instead of the token.
|
|
39
|
+
|
|
40
|
+
That is not cosmetic. FOUR offsets are computed from --header-h:
|
|
41
|
+
this header's min-block-size, `.under-header`'s reserve, and
|
|
42
|
+
scroll-padding-top / scroll-margin-top, which are what stop an
|
|
43
|
+
anchor target landing underneath the fixed nav. A header taller than
|
|
44
|
+
its token leaves all four short by the same amount, and none of them
|
|
45
|
+
errors.
|
|
46
|
+
|
|
47
|
+
`block-size: clamp(2.25rem, 3.4vw, 2.75rem); inline-size: auto` */}
|
|
33
48
|
{business.name}
|
|
34
49
|
</a>
|
|
35
50
|
|
|
@@ -29,7 +29,7 @@ const { title, lede, breadcrumbs, class: className = '' } = Astro.props;
|
|
|
29
29
|
const crumbs = breadcrumbs ?? [];
|
|
30
30
|
---
|
|
31
31
|
|
|
32
|
-
<section class:list={['hero', 'under-header',
|
|
32
|
+
<section class:list={['hero', 'under-header', className]}>
|
|
33
33
|
<div class="container">
|
|
34
34
|
{
|
|
35
35
|
crumbs.length > 0 && (
|
|
@@ -59,6 +59,31 @@ const crumbs = breadcrumbs ?? [];
|
|
|
59
59
|
</section>
|
|
60
60
|
|
|
61
61
|
<style>
|
|
62
|
+
/*
|
|
63
|
+
* ⚠ THE RHYTHM BELONGS TO THE NEXT SECTION, NOT TO THIS ONE.
|
|
64
|
+
*
|
|
65
|
+
* This carried `.section--tight`, which is a SHORTHAND — it sets padding-block
|
|
66
|
+
* at both ends. Every page opens its following section with a rhythm class of
|
|
67
|
+
* its own, so two stacked: a measured 160px hole on the template's own
|
|
68
|
+
* /contact/, 176px on all four PageHero pages of a real build, and up to 232px
|
|
69
|
+
* where the next section is `.section` rather than `.section--tight`.
|
|
70
|
+
*
|
|
71
|
+
* Nothing could see it. Build green, types green, axe green, tells green — a
|
|
72
|
+
* hole is only visible by looking at the page.
|
|
73
|
+
*
|
|
74
|
+
* ⚠ AND THE SHORTHAND CLOBBERED THE HEADER OFFSET. `.under-header` reserves
|
|
75
|
+
* the fixed header's height on padding-block-START, and global.css warns in
|
|
76
|
+
* as many words that "a scoped component style would out-specify" it. A
|
|
77
|
+
* shorthand from this component is exactly such a style: on a real build it
|
|
78
|
+
* discarded the reserve and put the hero behind the nav.
|
|
79
|
+
*
|
|
80
|
+
* padding-block-END only, therefore. The offset stays with `.under-header`,
|
|
81
|
+
* the rhythm stays with whatever section comes next.
|
|
82
|
+
*/
|
|
83
|
+
.hero {
|
|
84
|
+
padding-block-end: 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
62
87
|
.hero__crumbs ol {
|
|
63
88
|
display: flex;
|
|
64
89
|
flex-wrap: wrap;
|
|
@@ -241,6 +241,16 @@ b {
|
|
|
241
241
|
* height or the heading sits behind the nav. Applied by each opening section
|
|
242
242
|
* rather than a blanket `main > :first-child` rule, which a scoped component
|
|
243
243
|
* style would out-specify.
|
|
244
|
+
*
|
|
245
|
+
* ⚠ AND ONE DID. A component wrote `padding-block: <a> <b>` on the same element
|
|
246
|
+
* — a SHORTHAND, so it set padding-block-start too, out-specified this, and
|
|
247
|
+
* the hero sat behind the nav on a real build. On the element that carries
|
|
248
|
+
* `.under-header`, set `padding-block-end` and never the shorthand.
|
|
249
|
+
*
|
|
250
|
+
* ⚠ NOT MOVED TO `main`, WHICH WOULD BE UN-OVERRIDABLE. The header's background
|
|
251
|
+
* is opaque `var(--bg)`, so padding on `main` would leave a strip of body
|
|
252
|
+
* colour behind it wherever a first section has a background of its own. The
|
|
253
|
+
* per-section reserve is deliberate; the shorthand is the bug.
|
|
244
254
|
*/
|
|
245
255
|
.under-header {
|
|
246
256
|
padding-block-start: calc(var(--header-h) + var(--space-2xl));
|
|
@@ -189,9 +189,19 @@
|
|
|
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 ────────────────────────────────────────────────────────────── */
|
|
200
|
+
/* ⚠ FOUR THINGS READ THIS: the header's min-block-size, `.under-header`'s
|
|
201
|
+
reserve, and scroll-padding-top / scroll-margin-top, which keep an anchor
|
|
202
|
+
target clear of the fixed nav. If the rendered header is taller than this
|
|
203
|
+
— a logo sized by width is the usual cause — all four are short by the same
|
|
204
|
+
amount and nothing reports it. Measure the bar, do not assume it. */
|
|
195
205
|
--header-h: 4.5rem;
|
|
196
206
|
--z-header: 100;
|
|
197
207
|
--z-menu: 110;
|
package/template/wrangler.jsonc
CHANGED
|
@@ -7,6 +7,21 @@
|
|
|
7
7
|
// the Astro adapter resolves its environment at build time and silently
|
|
8
8
|
// ignores `deploy --env`, which is how a staging deploy lands on production.
|
|
9
9
|
//
|
|
10
|
+
// ⚠ AND IT IGNORES `--config` THE SAME WAY, which is the next flag anyone
|
|
11
|
+
// reaches for. @astrojs/cloudflare generates dist/server/wrangler.json —
|
|
12
|
+
// the config actually deployed, the one carrying `main` and the real asset
|
|
13
|
+
// directory — and builds it from the DEFAULT config path only. So
|
|
14
|
+
// `wrangler deploy --config wrangler.production.jsonc` either fails with
|
|
15
|
+
// "Cannot use assets with a binding in an assets-only Worker" (the
|
|
16
|
+
// hand-written file has no `main`), or, if you work around that by
|
|
17
|
+
// deploying the generated file instead, ships with whatever name and routes
|
|
18
|
+
// THIS file held, whatever was built.
|
|
19
|
+
//
|
|
20
|
+
// One config is the reason that cannot happen here. A project that grows a
|
|
21
|
+
// second one has to rewrite dist/server/wrangler.json between build and
|
|
22
|
+
// deploy, because there is no adapter option for it. Found on a shipped
|
|
23
|
+
// build that needed two.
|
|
24
|
+
//
|
|
10
25
|
// npm run deploy:staging → builds staging, deploys this worker
|
|
11
26
|
// npm run deploy:production → builds production, deploys this worker
|
|
12
27
|
//
|