create-website-build-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +54 -0
  2. package/index.mjs +149 -0
  3. package/package.json +42 -0
  4. package/template/.dev.vars.example +3 -0
  5. package/template/.github/workflows/gates.yml +58 -0
  6. package/template/.node-version +1 -0
  7. package/template/.pa11yci.json +24 -0
  8. package/template/BUILD-STATE.md +47 -0
  9. package/template/CLAUDE.md +153 -0
  10. package/template/astro.config.mjs +150 -0
  11. package/template/docs/analytics.md +86 -0
  12. package/template/docs/content.md +138 -0
  13. package/template/docs/handover.md +182 -0
  14. package/template/docs/handover.pdf +0 -0
  15. package/template/docs/runbook.md +661 -0
  16. package/template/docs/traps.md +903 -0
  17. package/template/gitignore +31 -0
  18. package/template/package-lock.json +8159 -0
  19. package/template/package.json +53 -0
  20. package/template/public/_headers +61 -0
  21. package/template/public/_redirects +39 -0
  22. package/template/public/site.webmanifest +13 -0
  23. package/template/scripts/a11y-evidence.mjs +258 -0
  24. package/template/scripts/check-console.mjs +125 -0
  25. package/template/scripts/check-env.mjs +99 -0
  26. package/template/scripts/check-reflow.mjs +148 -0
  27. package/template/scripts/check-sitemap.mjs +113 -0
  28. package/template/scripts/dns-snapshot.mjs +267 -0
  29. package/template/scripts/extract.mjs +317 -0
  30. package/template/scripts/indexnow.mjs +154 -0
  31. package/template/scripts/lastmod.mjs +147 -0
  32. package/template/scripts/lib/inventory.mjs +104 -0
  33. package/template/scripts/lib/preserved.mjs +42 -0
  34. package/template/scripts/lib/routes.mjs +92 -0
  35. package/template/scripts/md-to-pdf.mjs +335 -0
  36. package/template/scripts/og-cards.config.mjs +114 -0
  37. package/template/scripts/og-cards.mjs +487 -0
  38. package/template/scripts/optimize-media.mjs +380 -0
  39. package/template/scripts/recon.mjs +480 -0
  40. package/template/scripts/redirects.mjs +298 -0
  41. package/template/scripts/shots.mjs +447 -0
  42. package/template/scripts/staging-headers.mjs +102 -0
  43. package/template/scripts/tells.mjs +268 -0
  44. package/template/scripts/verify.mjs +1069 -0
  45. package/template/src/components/ContactForm.astro +405 -0
  46. package/template/src/components/CtaBand.astro +82 -0
  47. package/template/src/components/EnvBadge.astro +146 -0
  48. package/template/src/components/Footer.astro +210 -0
  49. package/template/src/components/Header.astro +530 -0
  50. package/template/src/components/Icon.astro +56 -0
  51. package/template/src/components/Img.astro +129 -0
  52. package/template/src/components/PageHero.astro +88 -0
  53. package/template/src/components/Seo.astro +119 -0
  54. package/template/src/components/StructuredData.astro +173 -0
  55. package/template/src/content/blog/.gitkeep +5 -0
  56. package/template/src/content/legal/.gitkeep +0 -0
  57. package/template/src/content.config.ts +81 -0
  58. package/template/src/data/areas.ts +31 -0
  59. package/template/src/data/business.ts +121 -0
  60. package/template/src/data/categories.ts +37 -0
  61. package/template/src/data/fonts.ts +25 -0
  62. package/template/src/data/image-manifest.json +1 -0
  63. package/template/src/data/lastmod.json +1 -0
  64. package/template/src/data/nav.ts +49 -0
  65. package/template/src/data/services.ts +39 -0
  66. package/template/src/data/site.ts +136 -0
  67. package/template/src/env.d.ts +28 -0
  68. package/template/src/layouts/Base.astro +223 -0
  69. package/template/src/lib/brevo.ts +96 -0
  70. package/template/src/lib/hast-media.mjs +55 -0
  71. package/template/src/lib/lastmod.mjs +47 -0
  72. package/template/src/lib/lead.ts +92 -0
  73. package/template/src/lib/legal-routes.mjs +31 -0
  74. package/template/src/lib/legal.ts +75 -0
  75. package/template/src/lib/posts.ts +64 -0
  76. package/template/src/lib/runtime.ts +33 -0
  77. package/template/src/pages/404.astro +51 -0
  78. package/template/src/pages/[slug].astro +111 -0
  79. package/template/src/pages/accessibility.astro +128 -0
  80. package/template/src/pages/api/contact.ts +191 -0
  81. package/template/src/pages/api/leads.csv.ts +82 -0
  82. package/template/src/pages/contact.astro +112 -0
  83. package/template/src/pages/index.astro +84 -0
  84. package/template/src/pages/robots.txt.ts +38 -0
  85. package/template/src/pages/rss.xml.ts +27 -0
  86. package/template/src/styles/global.css +463 -0
  87. package/template/src/styles/project.css +14 -0
  88. package/template/src/styles/prose.css +182 -0
  89. package/template/src/styles/tokens.css +218 -0
  90. package/template/tsconfig.json +5 -0
  91. package/template/wrangler.jsonc +63 -0
@@ -0,0 +1,218 @@
1
+ /* ───────────────────────────────────────────────────────────────────────────
2
+ Design tokens — the only place a raw colour, size or duration is written.
3
+ Components reference these and nothing else, so changing one value here
4
+ visibly moves the whole site.
5
+
6
+ THIS FILE SHIPS UNDECIDED ON PURPOSE.
7
+
8
+ Everything in §Undecided below is a placeholder: a greyscale ramp and the
9
+ system font stack. It is legible, it clears contrast and it builds — and it
10
+ looks like nothing, because no design decision has been made yet. That is
11
+ the point. A template that arrives with a palette and a typeface hands every
12
+ project the same one.
13
+
14
+ Clear it in this order, then delete --unset:
15
+ 1. brand ramp + accent (Round 3 direction)
16
+ 2. display and body faces (src/data/fonts.ts + the @font-face block
17
+ in global.css — see docs/design.md)
18
+ 3. radii, shadow tint, motion (the direction decides; defaults are fine)
19
+
20
+ `npm run tells` fails while --unset is present alongside real values — a
21
+ half-cleared file means somebody set a colour and left the typeface.
22
+ ─────────────────────────────────────────────────────────────────────────── */
23
+
24
+ :root {
25
+ /* ══ Undecided ═══════════════════════════════════════════════════════════
26
+ Delete this line once the ramp and the faces below are real. Nothing else
27
+ reads it; `npm run tells` and `build:production` do. */
28
+ --unset: 'brand and type not yet chosen';
29
+
30
+ /* ── Brand ─────────────────────────────────────────────────────────────
31
+ Placeholder: neutral grey. Replace all seven with the real ramp, sampled
32
+ from the brand — not approximated by eye. Keep the 50→900 shape so a
33
+ component that reaches for --brand-100 still gets something sensible. */
34
+ --brand-50: #f4f5f6;
35
+ --brand-100: #e6e8ea;
36
+ --brand-300: #b6bcc2;
37
+ --brand-400: #8d959d;
38
+ --brand-500: #6b747d;
39
+ --brand-600: #545c64;
40
+ --brand-700: #3c434a;
41
+
42
+ /* Signal accent. One, used sparingly: one call-to-action per viewport,
43
+ never two competing. Placeholder tracks the ramp so nothing shouts. */
44
+ --accent-500: #6b747d;
45
+ --accent-600: #545c64;
46
+
47
+ /* ── Neutrals ──────────────────────────────────────────────────────────
48
+ These are usually fine as shipped. Tint them toward the brand hue if the
49
+ direction is warm — pure grey against a warm palette reads as dirt. */
50
+ --ink-900: #14171a;
51
+ --ink-800: #1f2429;
52
+ --ink-700: #2f363d;
53
+ --ink-600: #454e56;
54
+ --ink-500: #5b656e;
55
+ --ink-400: #78838c;
56
+ --ink-300: #a1abb3;
57
+ --ink-200: #c7ced4;
58
+ --ink-100: #e3e8eb;
59
+ --ink-50: #f4f6f7;
60
+ --white: #ffffff;
61
+
62
+ /* ── Semantic surfaces ─────────────────────────────────────────────────
63
+ Components reference THESE, not the ramps above. That is what lets a
64
+ `.on-dark` section invert by redefining six variables. */
65
+ --bg: var(--white);
66
+ --bg-subtle: var(--ink-50);
67
+ --bg-inverse: var(--ink-900);
68
+ --surface: var(--white);
69
+ --surface-raised: var(--white);
70
+ --border: var(--ink-100);
71
+ --border-strong: var(--ink-200);
72
+
73
+ --text: var(--ink-800);
74
+ --text-muted: var(--ink-500);
75
+ --text-inverse: var(--white);
76
+ --text-inverse-muted: color-mix(in oklab, var(--white) 72%, transparent);
77
+
78
+ --focus: var(--ink-900);
79
+ --danger: #c8382e;
80
+ --danger-bg: #fdf1f0;
81
+ --success: #1c6b45;
82
+
83
+ /* ── Type ──────────────────────────────────────────────────────────────
84
+ Placeholder: the system stack for both. Two families, display and body —
85
+ a third is almost always a mistake.
86
+
87
+ To set them: add the woff2 to public/fonts/, an @font-face block to
88
+ global.css, an entry to src/data/fonts.ts so it is preloaded, then name
89
+ them here. Self-hosted and subset, so a preload is the whole story: no
90
+ DNS, no TLS handshake, no third-party round trip on the critical path. */
91
+ --font-display: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
92
+ --font-body: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
93
+ --font-mono: ui-monospace, 'SF Mono', 'Cascadia Mono', monospace;
94
+
95
+ /*
96
+ * Fluid scale, 320px → 1440px viewport.
97
+ *
98
+ * The upper end is deliberately restrained. A pure 1.33 ratio puts the hero
99
+ * headline past 100px at desktop, which reads as a magazine cover rather
100
+ * than a business people trust with money. The top three steps are capped;
101
+ * if a direction genuinely wants a larger hero, raise --step-6 knowingly
102
+ * rather than by swapping in a bigger ratio.
103
+ */
104
+ --step--1: clamp(0.833rem, 0.81rem + 0.11vw, 0.889rem);
105
+ --step-0: clamp(1rem, 0.96rem + 0.2vw, 1.0625rem);
106
+ --step-1: clamp(1.125rem, 1.08rem + 0.22vw, 1.3rem);
107
+ --step-2: clamp(1.32rem, 1.24rem + 0.4vw, 1.65rem);
108
+ --step-3: clamp(1.55rem, 1.4rem + 0.72vw, 2.1rem);
109
+ --step-4: clamp(1.85rem, 1.62rem + 1.13vw, 2.7rem);
110
+ --step-5: clamp(2.1rem, 1.8rem + 1.5vw, 3.25rem);
111
+ --step-6: clamp(2.4rem, 2rem + 2vw, 3.9rem);
112
+
113
+ --leading-tight: 1.05;
114
+ --leading-snug: 1.2;
115
+ --leading-normal: 1.6;
116
+ --leading-relaxed: 1.75;
117
+
118
+ /* Tighten as size increases. Display type set at body tracking is one of
119
+ the cheapest tells of an unconsidered page. */
120
+ --tracking-tighter: -0.04em;
121
+ --tracking-tight: -0.02em;
122
+ --tracking-normal: 0;
123
+ --tracking-wide: 0.02em;
124
+ --tracking-widest: 0.14em;
125
+
126
+ --weight-normal: 400;
127
+ --weight-medium: 500;
128
+ --weight-semibold: 600;
129
+ --weight-bold: 700;
130
+
131
+ /* Optimal measure for long-form reading. Body text that runs the full
132
+ container width is the most common reason a page reads as a document
133
+ rather than a designed thing. */
134
+ --measure: 68ch;
135
+ --measure-narrow: 46ch;
136
+
137
+ /* ── Space ─────────────────────────────────────────────────────────────── */
138
+ --space-3xs: 0.25rem;
139
+ --space-2xs: 0.5rem;
140
+ --space-xs: 0.75rem;
141
+ --space-s: 1rem;
142
+ --space-m: 1.5rem;
143
+ --space-l: 2rem;
144
+ --space-xl: 3rem;
145
+ --space-2xl: 4rem;
146
+ --space-3xl: 6rem;
147
+
148
+ /* Vertical rhythm between sections, fluid.
149
+ Two values, not one. Every section sharing a single padding is the tell
150
+ that nothing about the page rhythm was decided — vary deliberately. */
151
+ --section-y: clamp(3.5rem, 2rem + 7vw, 8rem);
152
+ --section-y-tight: clamp(2.5rem, 1.5rem + 5vw, 5rem);
153
+ --section-y-loose: clamp(5rem, 3rem + 9vw, 11rem);
154
+
155
+ /* ── Layout ────────────────────────────────────────────────────────────── */
156
+ --width-max: 1240px;
157
+ --width-wide: 1440px;
158
+ --width-text: 44rem;
159
+ --gutter: clamp(1.25rem, 4vw, 3rem);
160
+
161
+ /* ── Radii ─────────────────────────────────────────────────────────────
162
+ Scaled off one unit so a direction can go square in a single edit:
163
+ set --radius-unit to 0. */
164
+ --radius-unit: 1;
165
+ --radius-xs: calc(6px * var(--radius-unit));
166
+ --radius-s: calc(10px * var(--radius-unit));
167
+ --radius-m: calc(16px * var(--radius-unit));
168
+ --radius-l: calc(24px * var(--radius-unit));
169
+ --radius-xl: calc(32px * var(--radius-unit));
170
+ --radius-full: calc(999px * var(--radius-unit));
171
+
172
+ /* ── Elevation ─────────────────────────────────────────────────────────
173
+ Tint --shadow-rgb with the brand hue rather than leaving it neutral —
174
+ grey shadows on a warm palette read as dirt. */
175
+ --shadow-rgb: 20 23 26;
176
+ --shadow-xs: 0 1px 2px rgb(var(--shadow-rgb) / 0.06);
177
+ --shadow-s: 0 2px 8px -2px rgb(var(--shadow-rgb) / 0.08), 0 1px 2px rgb(var(--shadow-rgb) / 0.04);
178
+ --shadow-m: 0 8px 24px -8px rgb(var(--shadow-rgb) / 0.12), 0 2px 6px -2px rgb(var(--shadow-rgb) / 0.06);
179
+ --shadow-l: 0 24px 48px -16px rgb(var(--shadow-rgb) / 0.16), 0 4px 12px -4px rgb(var(--shadow-rgb) / 0.06);
180
+
181
+ --ring: 0 0 0 3px color-mix(in oklab, var(--focus) 35%, transparent);
182
+
183
+ /* ── Motion ────────────────────────────────────────────────────────────
184
+ Felt, not watched. Anything past ~400ms makes the visitor wait for
185
+ decoration, which is the most common way a good site starts feeling
186
+ cheap. `npm run tells` flags durations above that. */
187
+ --dur-fast: 120ms;
188
+ --dur-base: 220ms;
189
+ --dur-slow: 420ms;
190
+ --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
191
+ --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
192
+ --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
193
+
194
+ /* ── Chrome ────────────────────────────────────────────────────────────── */
195
+ --header-h: 4.5rem;
196
+ --z-header: 100;
197
+ --z-menu: 110;
198
+ --z-skip: 200;
199
+ }
200
+
201
+ /* Dark sections invert the semantic layer rather than restating raw colours,
202
+ so a component dropped into <section class="on-dark"> just works.
203
+
204
+ Check this inversion separately for contrast. It is where 4.5:1 usually
205
+ breaks, because the brand ramp that passes on white rarely passes on ink. */
206
+ .on-dark {
207
+ --bg: var(--ink-900);
208
+ --bg-subtle: var(--ink-800);
209
+ --surface: color-mix(in oklab, var(--white) 6%, transparent);
210
+ --surface-raised: color-mix(in oklab, var(--white) 9%, transparent);
211
+ --border: color-mix(in oklab, var(--white) 12%, transparent);
212
+ --border-strong: color-mix(in oklab, var(--white) 22%, transparent);
213
+ --text: var(--white);
214
+ --text-muted: color-mix(in oklab, var(--white) 68%, transparent);
215
+ --focus: var(--white);
216
+ color: var(--text);
217
+ background-color: var(--bg);
218
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "astro/tsconfigs/strict",
3
+ "include": [".astro/types.d.ts", "**/*"],
4
+ "exclude": ["dist", "node_modules"]
5
+ }
@@ -0,0 +1,63 @@
1
+ {
2
+ "$schema": "node_modules/wrangler/config-schema.json",
3
+
4
+ // ─────────────────────────────────────────────────────────────────────────
5
+ // ONE worker serves both staging and production. Which one it *is* is
6
+ // decided by the build (PUBLIC_SITE_ENV), never by a wrangler --env flag —
7
+ // the Astro adapter resolves its environment at build time and silently
8
+ // ignores `deploy --env`, which is how a staging deploy lands on production.
9
+ //
10
+ // npm run deploy:staging → builds staging, deploys this worker
11
+ // npm run deploy:production → builds production, deploys this worker
12
+ //
13
+ // Create the namespaces first:
14
+ // wrangler kv namespace create "<site>-leads"
15
+ // wrangler kv namespace create "<site>-leads-staging"
16
+ // then paste the ids below. Never leave a binding without one.
17
+ // ─────────────────────────────────────────────────────────────────────────
18
+ "name": "your-site-name",
19
+ "compatibility_date": "2026-07-01",
20
+ "compatibility_flags": ["nodejs_compat"],
21
+
22
+ "assets": {
23
+ "directory": "./dist",
24
+ "binding": "ASSETS",
25
+ // Every unknown path must reach the worker so it can serve a real 404
26
+ // status rather than a 200 with 404-looking HTML.
27
+ "not_found_handling": "none",
28
+ "html_handling": "auto-trailing-slash"
29
+ },
30
+
31
+ "observability": {
32
+ "enabled": true
33
+ },
34
+
35
+ // Both namespaces are declared explicitly with their existing ids.
36
+ // An undeclared binding gets auto-created on deploy — which works exactly
37
+ // once, then breaks every rebuild on a name the last incarnation left behind.
38
+ // The build environment picks which one the form writes to, so staging test
39
+ // submissions never contaminate real leads.
40
+ "kv_namespaces": [
41
+ {
42
+ "binding": "LEADS",
43
+ "id": "REPLACE-WITH-YOUR-KV-ID"
44
+ },
45
+ {
46
+ "binding": "LEADS_STAGING",
47
+ "id": "REPLACE-WITH-YOUR-KV-ID"
48
+ }
49
+ ],
50
+
51
+ // Custom domains create their own DNS records. Staging is listed first and
52
+ // is removed at go-live (runbook step 6) so the staging host stops answering.
53
+ "routes": [
54
+ {
55
+ "pattern": "new.example.com",
56
+ "custom_domain": true
57
+ }
58
+ ]
59
+
60
+ // Secrets — set with `wrangler secret put <NAME>`, never in this file:
61
+ // BREVO_API_KEY Brevo transactional email key
62
+ // LEADS_EXPORT_TOKEN bearer token for GET /api/leads.csv
63
+ }