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.
- package/README.md +54 -0
- package/index.mjs +149 -0
- package/package.json +42 -0
- package/template/.dev.vars.example +3 -0
- package/template/.github/workflows/gates.yml +58 -0
- package/template/.node-version +1 -0
- package/template/.pa11yci.json +24 -0
- package/template/BUILD-STATE.md +47 -0
- package/template/CLAUDE.md +153 -0
- package/template/astro.config.mjs +150 -0
- package/template/docs/analytics.md +86 -0
- package/template/docs/content.md +138 -0
- package/template/docs/handover.md +182 -0
- package/template/docs/handover.pdf +0 -0
- package/template/docs/runbook.md +661 -0
- package/template/docs/traps.md +903 -0
- package/template/gitignore +31 -0
- package/template/package-lock.json +8159 -0
- package/template/package.json +53 -0
- package/template/public/_headers +61 -0
- package/template/public/_redirects +39 -0
- package/template/public/site.webmanifest +13 -0
- package/template/scripts/a11y-evidence.mjs +258 -0
- package/template/scripts/check-console.mjs +125 -0
- package/template/scripts/check-env.mjs +99 -0
- package/template/scripts/check-reflow.mjs +148 -0
- package/template/scripts/check-sitemap.mjs +113 -0
- package/template/scripts/dns-snapshot.mjs +267 -0
- package/template/scripts/extract.mjs +317 -0
- package/template/scripts/indexnow.mjs +154 -0
- package/template/scripts/lastmod.mjs +147 -0
- package/template/scripts/lib/inventory.mjs +104 -0
- package/template/scripts/lib/preserved.mjs +42 -0
- package/template/scripts/lib/routes.mjs +92 -0
- package/template/scripts/md-to-pdf.mjs +335 -0
- package/template/scripts/og-cards.config.mjs +114 -0
- package/template/scripts/og-cards.mjs +487 -0
- package/template/scripts/optimize-media.mjs +380 -0
- package/template/scripts/recon.mjs +480 -0
- package/template/scripts/redirects.mjs +298 -0
- package/template/scripts/shots.mjs +447 -0
- package/template/scripts/staging-headers.mjs +102 -0
- package/template/scripts/tells.mjs +268 -0
- package/template/scripts/verify.mjs +1069 -0
- package/template/src/components/ContactForm.astro +405 -0
- package/template/src/components/CtaBand.astro +82 -0
- package/template/src/components/EnvBadge.astro +146 -0
- package/template/src/components/Footer.astro +210 -0
- package/template/src/components/Header.astro +530 -0
- package/template/src/components/Icon.astro +56 -0
- package/template/src/components/Img.astro +129 -0
- package/template/src/components/PageHero.astro +88 -0
- package/template/src/components/Seo.astro +119 -0
- package/template/src/components/StructuredData.astro +173 -0
- package/template/src/content/blog/.gitkeep +5 -0
- package/template/src/content/legal/.gitkeep +0 -0
- package/template/src/content.config.ts +81 -0
- package/template/src/data/areas.ts +31 -0
- package/template/src/data/business.ts +121 -0
- package/template/src/data/categories.ts +37 -0
- package/template/src/data/fonts.ts +25 -0
- package/template/src/data/image-manifest.json +1 -0
- package/template/src/data/lastmod.json +1 -0
- package/template/src/data/nav.ts +49 -0
- package/template/src/data/services.ts +39 -0
- package/template/src/data/site.ts +136 -0
- package/template/src/env.d.ts +28 -0
- package/template/src/layouts/Base.astro +223 -0
- package/template/src/lib/brevo.ts +96 -0
- package/template/src/lib/hast-media.mjs +55 -0
- package/template/src/lib/lastmod.mjs +47 -0
- package/template/src/lib/lead.ts +92 -0
- package/template/src/lib/legal-routes.mjs +31 -0
- package/template/src/lib/legal.ts +75 -0
- package/template/src/lib/posts.ts +64 -0
- package/template/src/lib/runtime.ts +33 -0
- package/template/src/pages/404.astro +51 -0
- package/template/src/pages/[slug].astro +111 -0
- package/template/src/pages/accessibility.astro +128 -0
- package/template/src/pages/api/contact.ts +191 -0
- package/template/src/pages/api/leads.csv.ts +82 -0
- package/template/src/pages/contact.astro +112 -0
- package/template/src/pages/index.astro +84 -0
- package/template/src/pages/robots.txt.ts +38 -0
- package/template/src/pages/rss.xml.ts +27 -0
- package/template/src/styles/global.css +463 -0
- package/template/src/styles/project.css +14 -0
- package/template/src/styles/prose.css +182 -0
- package/template/src/styles/tokens.css +218 -0
- package/template/tsconfig.json +5 -0
- 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,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
|
+
}
|