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
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# create-website-build-kit
|
|
2
|
+
|
|
3
|
+
Scaffold a production marketing site: **Astro, static, on Cloudflare Workers** — with the
|
|
4
|
+
gates, the migration playbook and the accessibility work already wired.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm create website-build-kit@latest my-site
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Node 22.12+. It builds green immediately, with no content, no images and no secrets.
|
|
11
|
+
|
|
12
|
+
## What you get
|
|
13
|
+
|
|
14
|
+
A skeleton, not a theme. It ships **no palette, no typeface and no home page** — deliberately,
|
|
15
|
+
so that two sites built from it cannot look alike. What it does ship is the part that takes
|
|
16
|
+
longest to get right and is easiest to get subtly wrong:
|
|
17
|
+
|
|
18
|
+
- **A form endpoint** with server-side validation, a honeypot, and the lead written to
|
|
19
|
+
durable storage **before** the email provider is called — so a provider outage costs a
|
|
20
|
+
notification, never a lead. It works with JavaScript off.
|
|
21
|
+
- **Environment derived from one build variable** — indexability, analytics, canonical host,
|
|
22
|
+
which store leads land in, who gets notified. Nothing toggled by hand at go-live, and a bare
|
|
23
|
+
build with no environment set is refused rather than quietly publishing `localhost`
|
|
24
|
+
canonicals.
|
|
25
|
+
- **A media pipeline** — AVIF and WebP per width, a dimensions manifest so nothing shifts,
|
|
26
|
+
favicons rendered from the vector.
|
|
27
|
+
- **Verification against the deployed site**, as a gate that exits non-zero: routes, a real
|
|
28
|
+
404, every redirect rule *and whether its target resolves*, security headers, the meta
|
|
29
|
+
sweep, page weight, and the form submissions the API is supposed to refuse.
|
|
30
|
+
- **WCAG 2.2 AA as a build constraint** — a published accessibility statement, `pa11y-ci`
|
|
31
|
+
wired up, a reflow check at 320px and 200% text, and a dated evidence pack.
|
|
32
|
+
- **A migration path**: inventory the old site, extract its content to markdown, propose a
|
|
33
|
+
redirect map, and fail the build if a URL the old site served no longer resolves.
|
|
34
|
+
|
|
35
|
+
## Migrating off WordPress?
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm run recon -- https://old-site.com # URLs, preserved paths, integrations, DNS
|
|
39
|
+
npm run extract # captured HTML → clean markdown
|
|
40
|
+
npm run redirects # proposes a map; never writes it for you
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Run recon **before** you design routes. For any page builder — Elementor, Divi, WPBakery,
|
|
44
|
+
Bricks — the extractable copy is the rendered HTML, never the database.
|
|
45
|
+
|
|
46
|
+
## The method
|
|
47
|
+
|
|
48
|
+
The template is half of it. The other half is a Claude Code skill and the written method
|
|
49
|
+
behind it, including a file of failures that were **green in a build**: clean build, clean
|
|
50
|
+
types, clean deploy, wrong result.
|
|
51
|
+
|
|
52
|
+
**https://github.com/nurkamol/website-build-kit**
|
|
53
|
+
|
|
54
|
+
MIT.
|
package/index.mjs
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* npm create website-build-kit@latest my-site
|
|
4
|
+
*
|
|
5
|
+
* Scaffolds the Astro + Cloudflare Workers template, then gets out of the way.
|
|
6
|
+
*
|
|
7
|
+
* ── WHAT IT REFUSES TO DO ──────────────────────────────────────────────────
|
|
8
|
+
* It will not write into a directory that already has files, and it will not
|
|
9
|
+
* proceed on a Node it knows Astro rejects. Both are failures that otherwise
|
|
10
|
+
* surface much later as something that reads like a different problem: Astro on
|
|
11
|
+
* Node 20 dies with a version notice buried in a build log, and scaffolding
|
|
12
|
+
* over an existing project is unrecoverable without git.
|
|
13
|
+
*
|
|
14
|
+
* No dependencies, deliberately. `npm create` downloads this before it can do
|
|
15
|
+
* anything, so every dependency here is latency on the first command a new user
|
|
16
|
+
* ever runs.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { spawnSync } from 'node:child_process';
|
|
20
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { dirname, join, resolve } from 'node:path';
|
|
22
|
+
import { createInterface } from 'node:readline/promises';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
|
|
25
|
+
const RESET = '\x1b[0m';
|
|
26
|
+
const RED = '\x1b[31m';
|
|
27
|
+
const GREEN = '\x1b[32m';
|
|
28
|
+
const YELLOW = '\x1b[33m';
|
|
29
|
+
const DIM = '\x1b[2m';
|
|
30
|
+
const BOLD = '\x1b[1m';
|
|
31
|
+
|
|
32
|
+
const argv = process.argv.slice(2);
|
|
33
|
+
const flag = (name) => argv.includes(`--${name}`);
|
|
34
|
+
const target = argv.find((a) => !a.startsWith('-'));
|
|
35
|
+
|
|
36
|
+
if (flag('help') || flag('h')) {
|
|
37
|
+
console.log(`
|
|
38
|
+
${BOLD}create-website-build-kit${RESET}
|
|
39
|
+
|
|
40
|
+
npm create website-build-kit@latest ${DIM}<directory>${RESET}
|
|
41
|
+
|
|
42
|
+
${DIM}--no-install${RESET} skip npm install
|
|
43
|
+
${DIM}--no-git${RESET} skip git init
|
|
44
|
+
${DIM}--force${RESET} write into a non-empty directory
|
|
45
|
+
`);
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/*
|
|
50
|
+
* Astro's floor is 22.12. On anything older the build dies with a version
|
|
51
|
+
* notice inside a wall of build output, which is a confusing way to learn it —
|
|
52
|
+
* so it is checked before a single file is written.
|
|
53
|
+
*/
|
|
54
|
+
const MIN = [22, 12];
|
|
55
|
+
const current = process.versions.node.split('.').map(Number);
|
|
56
|
+
if (current[0] < MIN[0] || (current[0] === MIN[0] && current[1] < MIN[1])) {
|
|
57
|
+
console.error(
|
|
58
|
+
`\n${RED}✗${RESET} Node ${process.versions.node} is too old — Astro needs ${MIN.join('.')}+.\n` +
|
|
59
|
+
` ${DIM}nvm install 24 && nvm use 24${RESET}\n`,
|
|
60
|
+
);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(`\n${BOLD}Website Build Kit${RESET}${DIM} — Astro on Cloudflare Workers${RESET}\n`);
|
|
65
|
+
|
|
66
|
+
let dir = target;
|
|
67
|
+
if (!dir) {
|
|
68
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
69
|
+
dir = (await rl.question(` Directory ${DIM}(my-site)${RESET}: `)).trim() || 'my-site';
|
|
70
|
+
rl.close();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const dest = resolve(process.cwd(), dir);
|
|
74
|
+
const name = dest.split('/').pop();
|
|
75
|
+
|
|
76
|
+
if (existsSync(dest) && readdirSync(dest).length && !flag('force')) {
|
|
77
|
+
console.error(
|
|
78
|
+
`\n${RED}✗${RESET} ${dir} is not empty.\n` +
|
|
79
|
+
` ${DIM}Scaffolding over an existing project cannot be undone without git.\n` +
|
|
80
|
+
` Pick another directory, or pass --force if you are sure.${RESET}\n`,
|
|
81
|
+
);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const src = join(dirname(fileURLToPath(import.meta.url)), 'template');
|
|
86
|
+
if (!existsSync(src)) {
|
|
87
|
+
console.error(`\n${RED}✗${RESET} the template is missing from this package — please report it.\n`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
mkdirSync(dest, { recursive: true });
|
|
92
|
+
cpSync(src, dest, { recursive: true });
|
|
93
|
+
|
|
94
|
+
/*
|
|
95
|
+
* npm strips .gitignore from published packages, so it ships as `gitignore`.
|
|
96
|
+
* Restoring it is not cosmetic: it is what keeps .dev.vars — which holds
|
|
97
|
+
* BREVO_API_KEY and the leads export token — out of the repository.
|
|
98
|
+
*/
|
|
99
|
+
const shipped = join(dest, 'gitignore');
|
|
100
|
+
if (existsSync(shipped)) renameSync(shipped, join(dest, '.gitignore'));
|
|
101
|
+
|
|
102
|
+
if (!existsSync(join(dest, '.gitignore'))) {
|
|
103
|
+
console.error(`\n${RED}✗${RESET} no .gitignore was written. Stopping — .dev.vars would be committable.\n`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* The site's own name, so package.json is not "site-name" forever. */
|
|
108
|
+
const pkgPath = join(dest, 'package.json');
|
|
109
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
110
|
+
pkg.name = name.replace(/[^a-z0-9-]+/gi, '-').replace(/^-+|-+$/g, '').toLowerCase() || 'site';
|
|
111
|
+
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
112
|
+
|
|
113
|
+
console.log(` ${GREEN}✓${RESET} template → ${dir}/`);
|
|
114
|
+
|
|
115
|
+
const run = (cmd, args, label) => {
|
|
116
|
+
const r = spawnSync(cmd, args, { cwd: dest, stdio: 'ignore', shell: process.platform === 'win32' });
|
|
117
|
+
console.log(r.status === 0 ? ` ${GREEN}✓${RESET} ${label}` : ` ${YELLOW}!${RESET} ${label} — skipped`);
|
|
118
|
+
return r.status === 0;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
if (!flag('no-git')) {
|
|
122
|
+
if (run('git', ['init', '-q'], 'git initialised')) {
|
|
123
|
+
run('git', ['add', '-A'], 'files staged');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let installed = false;
|
|
128
|
+
if (!flag('no-install')) {
|
|
129
|
+
console.log(` ${DIM}installing…${RESET}`);
|
|
130
|
+
installed = run('npm', ['install', '--silent'], 'dependencies installed');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(`
|
|
134
|
+
${BOLD}Next${RESET}
|
|
135
|
+
|
|
136
|
+
${DIM}cd${RESET} ${dir}${installed ? '' : `\n ${DIM}npm install${RESET}`}
|
|
137
|
+
|
|
138
|
+
${BOLD}1.${RESET} Fill in ${BOLD}src/data/business.ts${RESET} — everything reads from it, and the
|
|
139
|
+
defaults are deliberately neutral rather than correct.
|
|
140
|
+
${BOLD}2.${RESET} ${DIM}npm run tells${RESET} — says what is still undecided. ${DIM}build:production${RESET}
|
|
141
|
+
refuses until the palette, the two typefaces and the home page are yours.
|
|
142
|
+
${BOLD}3.${RESET} Migrating? ${DIM}npm run recon -- https://old-site.com${RESET} first, before you
|
|
143
|
+
design routes. Then ${DIM}npm run extract${RESET}.
|
|
144
|
+
|
|
145
|
+
${DIM}docs/runbook.md §1 is the fill-in order. It builds green right now:${RESET}
|
|
146
|
+
${DIM}npm run build:staging${RESET}
|
|
147
|
+
|
|
148
|
+
${DIM}The method, and the traps: https://github.com/nurkamol/website-build-kit${RESET}
|
|
149
|
+
`);
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-website-build-kit",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"astro",
|
|
7
|
+
"cloudflare-workers",
|
|
8
|
+
"marketing-site",
|
|
9
|
+
"starter",
|
|
10
|
+
"template",
|
|
11
|
+
"wordpress-migration",
|
|
12
|
+
"scaffold",
|
|
13
|
+
"create",
|
|
14
|
+
"accessibility",
|
|
15
|
+
"wcag",
|
|
16
|
+
"seo"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://github.com/nurkamol/website-build-kit#readme",
|
|
19
|
+
"bugs": "https://github.com/nurkamol/website-build-kit/issues",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/nurkamol/website-build-kit.git",
|
|
23
|
+
"directory": "create"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": "Nurkamol Vakhidov",
|
|
27
|
+
"type": "module",
|
|
28
|
+
"bin": {
|
|
29
|
+
"create-website-build-kit": "index.mjs"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"index.mjs",
|
|
33
|
+
"template"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=22.12.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"prepack": "node prepack.mjs",
|
|
40
|
+
"postpack": "node -e \"require('fs').rmSync('template',{recursive:true,force:true})\""
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Runs the gates that do not need a deployed site.
|
|
2
|
+
#
|
|
3
|
+
# This does NOT deploy. Cloudflare Workers Builds does that, running
|
|
4
|
+
# `npm run build:production`, which carries its own gates — tells, astro check,
|
|
5
|
+
# check-env and check-sitemap. This workflow exists so those same failures show
|
|
6
|
+
# up on a pull request instead of at deploy time, and so `build:staging` is
|
|
7
|
+
# proven from a clean checkout on every push.
|
|
8
|
+
#
|
|
9
|
+
# The gates that need a live URL — npm run verify, a11y, reflow — are not here.
|
|
10
|
+
# They run against a deployed site, which this workflow does not have. See
|
|
11
|
+
# docs/runbook.md §2.
|
|
12
|
+
|
|
13
|
+
name: Gates
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
push:
|
|
17
|
+
branches: [main, master]
|
|
18
|
+
pull_request:
|
|
19
|
+
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
gates:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
|
|
29
|
+
# node-version-file, not a literal. A version pinned in two places drifts,
|
|
30
|
+
# and the failure is a build that works locally and not in CI.
|
|
31
|
+
- uses: actions/setup-node@v4
|
|
32
|
+
with:
|
|
33
|
+
node-version-file: .node-version
|
|
34
|
+
cache: npm
|
|
35
|
+
|
|
36
|
+
# `npm ci` not `npm install` — it installs exactly the lockfile and fails
|
|
37
|
+
# if package.json and the lock disagree, which is the point of running it
|
|
38
|
+
# here rather than trusting a local install.
|
|
39
|
+
- run: npm ci
|
|
40
|
+
|
|
41
|
+
# The kit's promise: green from a clean checkout, with no content, no
|
|
42
|
+
# images and no secrets. CI=true so Astro reports as a CI environment.
|
|
43
|
+
- name: Build (staging)
|
|
44
|
+
run: CI=true npm run build:staging
|
|
45
|
+
|
|
46
|
+
- name: Types
|
|
47
|
+
run: PUBLIC_SITE_ENV=staging npm run check
|
|
48
|
+
|
|
49
|
+
# Reports what is undecided and counts the design tells. Not a hard gate
|
|
50
|
+
# on a fresh template by design — `--undecided-only` blocks the
|
|
51
|
+
# HALF-decided state, which is the one that looks finished and is not.
|
|
52
|
+
- name: Design tells
|
|
53
|
+
run: npm run tells
|
|
54
|
+
|
|
55
|
+
# A no-op on staging, which emits no sitemap. Here so the check runs on
|
|
56
|
+
# every push rather than only inside build:production.
|
|
57
|
+
- name: Sitemap and noindex agree
|
|
58
|
+
run: npm run check:sitemap
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
24.2.0
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"defaults": {
|
|
3
|
+
"standard": "WCAG2AA",
|
|
4
|
+
"runners": ["axe", "htmlcs"],
|
|
5
|
+
"timeout": 30000,
|
|
6
|
+
"concurrency": 4,
|
|
7
|
+
"hideElements": "iframe[src*='google.com/maps'], iframe[src*='youtube.com']",
|
|
8
|
+
"chromeLaunchConfig": { "args": ["--no-sandbox"] }
|
|
9
|
+
},
|
|
10
|
+
"//": [
|
|
11
|
+
"One URL per template family, not every page — a homepage-only pass misses",
|
|
12
|
+
"everything the blog does differently. Point these at `wrangler dev`",
|
|
13
|
+
"(localhost:8788) while building, or at the deployed staging host.",
|
|
14
|
+
"Staging emits no sitemap on purpose, which is why these are explicit.",
|
|
15
|
+
"Against production you can instead run:",
|
|
16
|
+
" npx pa11y-ci --sitemap https://example.com/sitemap-index.xml --standard WCAG2AA"
|
|
17
|
+
],
|
|
18
|
+
"urls": [
|
|
19
|
+
"http://localhost:8788/",
|
|
20
|
+
"http://localhost:8788/contact/",
|
|
21
|
+
"http://localhost:8788/accessibility/",
|
|
22
|
+
"http://localhost:8788/404"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Build state
|
|
2
|
+
|
|
3
|
+
⚠ **Written at every gate, not continuously.** A file edited every few minutes is noise and
|
|
4
|
+
nobody reads it. Update it when a phase gate passes — `build.md` §3.
|
|
5
|
+
|
|
6
|
+
**Delete this file at handover**, folding what survived into `docs/handover.md`. It is a
|
|
7
|
+
working file, not a deliverable: its **Integrations** and **Preserve** lines are the source for
|
|
8
|
+
`handover.md` §3 and the "deliberately not built" list in `handover.md` §4.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
## Phase 0 · Recon [in progress]
|
|
12
|
+
|
|
13
|
+
Gates passed —
|
|
14
|
+
Locked —
|
|
15
|
+
Archetype ⚠ from the win, not the industry — archetypes.md
|
|
16
|
+
Open ! ⚠ blocking items go here, with a date
|
|
17
|
+
? ⚠ open questions
|
|
18
|
+
Next ⚠ one line
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Integrations
|
|
22
|
+
|
|
23
|
+
⚠ One line each, from `recon/integrations.md`. **Every line ends in a verified state or an
|
|
24
|
+
explicit dated drop. No third state** — "integrations" as one task is the item that silently
|
|
25
|
+
ships at 80%, and a missing conversion tag is invisible for a month.
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
- [ ] ⚠ GA4 · G-XXXXXXXXXX · not ported · ⚠ confirm the client owns this account
|
|
29
|
+
- [ ] ⚠ vendor · detected in markup · not ported · ⚠ owner unknown
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Preserve
|
|
33
|
+
|
|
34
|
+
⚠ Paths other systems point at, from `recon/preserved.md`. Losing one is silent — see
|
|
35
|
+
`stacks.md` §1d.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
- [ ] ⚠ /sitemap_index.xml → emitted at the old path, or 301 to the new one
|
|
39
|
+
- [ ] ⚠ verification file or DNS TXT carried over
|
|
40
|
+
- [ ] ⚠ /feed/ → the new feed path, never dropped
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Decisions locked
|
|
44
|
+
|
|
45
|
+
⚠ `Locked` means settled — do not reopen without saying so. This is the row that stops a pivot
|
|
46
|
+
quietly becoming a rewrite. Stack, providers, archetype, and the design direction chosen from a
|
|
47
|
+
real comp on staging (`design.md` §1).
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Working on this site
|
|
2
|
+
|
|
3
|
+
Astro (static) on Cloudflare Workers. Built from
|
|
4
|
+
[website-build-kit](https://github.com/nurkamol/website-build-kit).
|
|
5
|
+
|
|
6
|
+
## Commands
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm run dev # localhost:4321 — no bindings, /api/* will not work
|
|
10
|
+
npm run media # after adding or replacing any image
|
|
11
|
+
npm run build:staging && npx wrangler dev # localhost:8788, real KV + secrets
|
|
12
|
+
npm run a11y # accessibility check, one URL per family
|
|
13
|
+
npm run tells # what is undecided, and the design tells
|
|
14
|
+
npm run recon -- https://old-site.com # inventory the old site BEFORE designing routes
|
|
15
|
+
npm run dns -- old-site.com # capture the zone. MX loss kills client email
|
|
16
|
+
npm run seo -- https://old-site.com # optional: SEO baseline to diff after cutover
|
|
17
|
+
npm run verify -- https://new.example.com # the deployed site, not the build. exits non-zero
|
|
18
|
+
npm run console # console errors + failed requests, in a real browser
|
|
19
|
+
npm run shots -- --before https://old-site.com # visual record, WHILE the old site is still up
|
|
20
|
+
npm run extract # captured HTML → recon/extracted/*.md
|
|
21
|
+
npm run deploy:staging # or deploy:production
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Node **22.12+**. `.node-version` pins the version this was built against.
|
|
25
|
+
|
|
26
|
+
**Use `wrangler dev` for anything touching `/api/*`, redirects, or 404 status codes.** The
|
|
27
|
+
Astro dev server models none of them, and they are exactly where things break.
|
|
28
|
+
|
|
29
|
+
## Rules for this codebase
|
|
30
|
+
|
|
31
|
+
**`src/data/business.ts` is the only place business facts live.** Name, phone, address,
|
|
32
|
+
hours, service areas, credentials — plus `locale`, `timeZone` and `schemaTypes`, whose
|
|
33
|
+
defaults are deliberately neutral (`en`, `UTC`, `LocalBusiness`) rather than correct. Neutral
|
|
34
|
+
is a prompt to set them; a plausible wrong value is not. The header, footer, every call-to-action, the notification
|
|
35
|
+
emails and the JSON-LD all read from it. Never hard-code a phone number in a component.
|
|
36
|
+
|
|
37
|
+
**`src/styles/tokens.css` is the only place a raw colour, size, radius or duration is
|
|
38
|
+
written.** Components reference tokens and nothing else. If you are typing a hex or a px in a
|
|
39
|
+
component, add a token instead.
|
|
40
|
+
|
|
41
|
+
**This template has no design, and adding one to it is not the job.** It ships a grey
|
|
42
|
+
placeholder ramp, the system stack for both faces and a scaffold home page — deliberately, so
|
|
43
|
+
that two sites built from it cannot look alike. Decide the palette, the two typefaces and the
|
|
44
|
+
page shapes *for this project*, in this project. Do not add a card style, a hero treatment or
|
|
45
|
+
a component library to the shared layer; `global.css` carries the interactive **states**, not
|
|
46
|
+
a look. Run `npm run tells` before showing anyone a page.
|
|
47
|
+
|
|
48
|
+
**Legal pages are markdown, never `.astro` files.** `src/content/legal/<slug>.md` is served at
|
|
49
|
+
`/<slug>/` by one route and linked in the footer from the collection itself, so the link cannot
|
|
50
|
+
exist without the page. Do not write `privacy.astro`. The root-level `[slug].astro` refuses to
|
|
51
|
+
build if a legal slug shadows a real page — Astro would give the page precedence and the legal
|
|
52
|
+
one would be built, linked and unreachable.
|
|
53
|
+
|
|
54
|
+
**`src/data/nav.ts` is the only place routes and the primary call to action live.** The
|
|
55
|
+
header, the mobile menu, the footer columns and the 404's onward links all read it, so the
|
|
56
|
+
404 can never offer a page that no longer exists.
|
|
57
|
+
|
|
58
|
+
**Every non-production build shows a badge**, driven by `site.indexable` — it cannot be left
|
|
59
|
+
on in production and cannot be turned on by hand. It reads the live DOM rather than printing
|
|
60
|
+
the build variable, so it alarms when the page disagrees with the environment (`NOT NOINDEX`,
|
|
61
|
+
`ANALYTICS LIVE`). `?nobadge=1` hides it for a session; never make that persistent.
|
|
62
|
+
|
|
63
|
+
**`src/data/site.ts` is the only environment switch.** `PUBLIC_SITE_ENV` derives
|
|
64
|
+
indexability, analytics, canonical host, which KV namespace leads land in, and who gets
|
|
65
|
+
notified. Do not add a second flag — extend this one.
|
|
66
|
+
|
|
67
|
+
**Images are AVIF + WebP.** `optimize-media.mjs` emits both per width and `<Img />` renders a
|
|
68
|
+
`<picture>`; AVIF is 26% smaller at better quality (measured — see `stacks.md` §3). WebP is the
|
|
69
|
+
fallback and is never dropped. Turning it off is `FORMATS = ['webp']` and nothing else. Note
|
|
70
|
+
that a `.parent > img` selector stops matching once there is a `<picture>` — see `docs/traps.md`.
|
|
71
|
+
|
|
72
|
+
**Images go through the manifest.** Put the original in `media/source/`, run `npm run media`,
|
|
73
|
+
reference it by key (`photos/hero-home`) via `<Img />`. Never an external URL, never a raw
|
|
74
|
+
path. This is what keeps `width`/`height` on every image so nothing shifts.
|
|
75
|
+
|
|
76
|
+
**Forms write to storage before calling anyone.** In `src/pages/api/contact.ts` the KV write
|
|
77
|
+
happens *before* the email provider. A provider outage should cost a notification, not a
|
|
78
|
+
lead. Keep that ordering. The cross-origin refusal runs *before* validation, for the same
|
|
79
|
+
reason: ordered after, it only ever fires on submissions that were being rejected anyway.
|
|
80
|
+
|
|
81
|
+
**Stored leads expire.** `site.leadRetentionDays` becomes a KV `expirationTtl`, because KV
|
|
82
|
+
keeps a value forever otherwise and "indefinitely" is not a retention period any regulator
|
|
83
|
+
accepts. Keep the number and the privacy notice in step, and keep personal data out of KV
|
|
84
|
+
metadata — `list()` returns metadata without reading values.
|
|
85
|
+
|
|
86
|
+
**Everything works without JavaScript.** The page renders, the form submits, stores and
|
|
87
|
+
notifies. Anything that hides an element must be the same thing that reveals it — CSS that JS
|
|
88
|
+
is expected to undo shows a blank page whenever a bundle fails.
|
|
89
|
+
|
|
90
|
+
**Analytics IDs are the client's own, or empty.** `src/data/site.ts` emits no tag unless both
|
|
91
|
+
are set, so an unset ID cannot fall back to another project's container. Never copy one in.
|
|
92
|
+
The rules whose failure looks like success — double-counted pageviews, a conversion sent down
|
|
93
|
+
two pipes, a trigger on `sent=1` that catches almost nothing — are in `docs/analytics.md`.
|
|
94
|
+
Read it before adding any tag.
|
|
95
|
+
|
|
96
|
+
**Evidence, not assertions.** `npm run a11y:evidence` writes a dated pack to
|
|
97
|
+
`docs/a11y-evidence/` — commit it. It fills in the machine half and leaves the keyboard,
|
|
98
|
+
screen-reader and forms passes **blank**, because no tool does those; a pack with them still
|
|
99
|
+
blank is an incomplete pack, not a passing one. It also warns when `/accessibility` claims a
|
|
100
|
+
review date older than the run.
|
|
101
|
+
|
|
102
|
+
**`/accessibility` is a published artefact, not filler.** Required under the EAA and PSBAR,
|
|
103
|
+
footer-linked from every page. Keep its dates current and its known-gaps list honest — a
|
|
104
|
+
documented gap is worth more than a clean claim. Never write "fully compliant".
|
|
105
|
+
|
|
106
|
+
**Astro scoped styles do not reach a class passed *into* a component.** If you write
|
|
107
|
+
`<Icon class="thing" />` and then `.thing { … }` in the same file, it will not match. Use
|
|
108
|
+
`:global()`. This has caused real bugs; see `docs/traps.md`.
|
|
109
|
+
|
|
110
|
+
## Before debugging anything strange
|
|
111
|
+
|
|
112
|
+
Read **`docs/traps.md`**. Every entry failed silently — clean build, clean deploy, wrong
|
|
113
|
+
result. The recurring ones:
|
|
114
|
+
|
|
115
|
+
- Scoped styles not reaching a class passed into a component
|
|
116
|
+
- Handlers bound behind a `transition:persist` guard holding stale, replaced elements
|
|
117
|
+
- `justify-content: center` making overflowing content unreachable — use `margin: auto`
|
|
118
|
+
- `100vh` being taller than the visible area on mobile — use `100dvh`
|
|
119
|
+
- DNS negative caching: if `dig` and `getaddrinfo` disagree, it is your cache
|
|
120
|
+
|
|
121
|
+
## Verifying
|
|
122
|
+
|
|
123
|
+
Against the **deployed** site, not the build. A green build proves the bundler ran.
|
|
124
|
+
|
|
125
|
+
The checklist is in `docs/runbook.md`: every route 200, unknown paths a real 404, legacy URLs
|
|
126
|
+
301 to their specific equivalent, preserved paths still resolving, staging noindex with zero
|
|
127
|
+
analytics references, form valid/empty/honeypot/cross-origin, and a submission producing both a
|
|
128
|
+
stored record and a delivered email. It also carries the **go-live order** and the first-week
|
|
129
|
+
watch list.
|
|
130
|
+
|
|
131
|
+
## Handover
|
|
132
|
+
|
|
133
|
+
`docs/handover.md` is the only doc written for the **client**, not a developer. Fill in every
|
|
134
|
+
⚠ and render with `npm run handover`. Its "what we deliberately did not build" section is not
|
|
135
|
+
optional — the difference between a decision and an oversight is whether it was written down.
|
|
136
|
+
|
|
137
|
+
## Content
|
|
138
|
+
|
|
139
|
+
Editing guide in `docs/content.md`. In short: posts are markdown in `src/content/blog/` with
|
|
140
|
+
typed frontmatter — a bad edit fails the build rather than the page.
|
|
141
|
+
|
|
142
|
+
## Do not
|
|
143
|
+
|
|
144
|
+
- Hard-code a colour, size, phone number or business fact in a component — including inside a
|
|
145
|
+
JavaScript error string, which is where one hides longest
|
|
146
|
+
- Ship with `--unset` still in `tokens.css`, or the scaffold home page still in place
|
|
147
|
+
- Add a card, hero or section style to `global.css` — it belongs to this project, not the kit
|
|
148
|
+
- Add a second environment flag
|
|
149
|
+
- Reference an image by URL instead of manifest key
|
|
150
|
+
- Call a third-party API before writing the submission down
|
|
151
|
+
- Describe unverified work as done — check it against the deployed site
|
|
152
|
+
- Copy an analytics ID from another project
|
|
153
|
+
- Claim the site is "fully accessible" or "fully compliant"
|