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,150 @@
1
+ // @ts-check
2
+ import { defineConfig, sessionDrivers } from 'astro/config';
3
+ import cloudflare from '@astrojs/cloudflare';
4
+ import sitemap from '@astrojs/sitemap';
5
+ import { satteri } from '@astrojs/markdown-satteri';
6
+ import { hastMedia } from './src/lib/hast-media.mjs';
7
+ import { lastmodByRoute } from './src/lib/lastmod.mjs';
8
+ import { noindexLegalRoutes } from './src/lib/legal-routes.mjs';
9
+
10
+ // The build environment is the single source of truth for what this deploy
11
+ // *is*. Nothing downstream re-derives it, and nothing is toggled by hand.
12
+ const SITE_ENV = process.env.PUBLIC_SITE_ENV ?? 'development';
13
+
14
+ /*
15
+ * Refuse to BUILD for a deploy without saying which deploy it is.
16
+ *
17
+ * `development` resolves the site URL to http://localhost:4321, so a build with
18
+ * no environment emits canonical tags pointing at localhost. It builds clean,
19
+ * deploys clean, and is wrong.
20
+ *
21
+ * ── WHY THIS KEYS ON THE COMMAND AND NOT ON `CI` ───────────────────────────
22
+ * It used to be `if (process.env.CI && …)`, which left the hole wide open on
23
+ * the machine where it is most likely to be walked into. `npm run build` is the
24
+ * most reflexive command in the npm ecosystem, and here it produced a
25
+ * development build in silence — verified: canonical `http://localhost:4321/`,
26
+ * exit 0, no warning. `npx astro build` did the same. The kit's own README
27
+ * claimed this was already guarded.
28
+ *
29
+ * Keying on the argv command closes both, and leaves `astro dev` alone, where
30
+ * `development` is the correct answer rather than a missing one.
31
+ */
32
+ const isBuilding = process.argv.some((a) => a === 'build');
33
+
34
+ if (isBuilding && SITE_ENV === 'development') {
35
+ throw new Error(
36
+ 'PUBLIC_SITE_ENV is not set.\n\n' +
37
+ 'A build must declare its environment, or it emits localhost canonical\n' +
38
+ 'URLs — cleanly, with no error, and wrong. Use one of:\n\n' +
39
+ ' npm run build:staging → new.example.com, noindex\n' +
40
+ ' npm run build:production → example.com, indexable\n\n' +
41
+ 'On Cloudflare Workers Builds, that is the build command in the dashboard.\n' +
42
+ 'For a one-off local build, set it inline:\n\n' +
43
+ ' PUBLIC_SITE_ENV=staging npx astro build\n',
44
+ );
45
+ }
46
+ const SITE_URL =
47
+ process.env.PUBLIC_SITE_URL ??
48
+ (SITE_ENV === 'production'
49
+ ? 'https://example.com'
50
+ : SITE_ENV === 'staging'
51
+ ? 'https://new.example.com'
52
+ : 'http://localhost:4321');
53
+
54
+ const isProduction = SITE_ENV === 'production';
55
+
56
+ /*
57
+ * Per-page `lastmod` for the sitemap, from src/data/lastmod.json.
58
+ *
59
+ * Generated by `npm run lastmod` and COMMITTED — never read from git at build
60
+ * time. CI shallow-clones, so `git log` there returns one grafted commit for
61
+ * every file and every page would claim the same date. An empty map means no
62
+ * `lastmod` is emitted at all, which is the correct outcome: a sitemap saying
63
+ * "I don't know" is honest, one claiming every page changed today is not, and
64
+ * Google discounts a file whose dates are not consistently accurate.
65
+ */
66
+ const LASTMOD = lastmodByRoute();
67
+
68
+ /** Legal pages that must exist and must not be indexed. See legal-routes.mjs. */
69
+ const NOINDEX_LEGAL = noindexLegalRoutes();
70
+
71
+ export default defineConfig({
72
+ site: SITE_URL,
73
+ output: 'static',
74
+
75
+ /*
76
+ * This site has no sessions. Left unset, the Cloudflare adapter helpfully
77
+ * configures a KV session driver and writes a `SESSION` kv_namespace binding
78
+ * with NO id into the generated wrangler.json — which wrangler then *creates*
79
+ * on deploy. That works exactly once: the namespace outlives the worker, so
80
+ * tearing the deployment down and recreating it from git fails on a name the
81
+ * previous incarnation left behind. The null driver keeps the binding out.
82
+ */
83
+ // @ts-ignore - `null` exists at runtime (verified: `'null' in sessionDrivers`)
84
+ // but is missing from Astro's exported driver types. Not @ts-expect-error:
85
+ // when upstream adds it, expect-error would start failing every build.
86
+ session: { driver: sessionDrivers.null() },
87
+
88
+ /*
89
+ * `platformProxy` is NOT set here. It was, for two projects, and the
90
+ * Cloudflare adapter stopped accepting it — `Options` now derives from
91
+ * auxiliaryWorkers | configPath | inspectorPort | persistState |
92
+ * remoteBindings. An unknown key in an options object is dropped in silence,
93
+ * so it read as configuration for a build that had never once applied it.
94
+ *
95
+ * Nothing was lost: it existed to give `astro dev` real bindings, and the
96
+ * answer to that is `wrangler dev`, which is what the runbook already says.
97
+ */
98
+ adapter: cloudflare({
99
+ imageService: 'passthrough',
100
+ }),
101
+ trailingSlash: 'always',
102
+ build: { format: 'directory' },
103
+ prefetch: { prefetchAll: true, defaultStrategy: 'hover' },
104
+ integrations: [
105
+ // Staging must never advertise itself. No sitemap at all off production.
106
+ ...(isProduction
107
+ ? [
108
+ sitemap({
109
+ /*
110
+ * ⚠ EVERY PAGE THAT CARRIES `noindex` MUST BE EXCLUDED HERE.
111
+ *
112
+ * A URL in the sitemap is a request to index it; the same URL
113
+ * serving `noindex` is a refusal. Search Console reports the pair
114
+ * as "Submitted URL marked 'noindex'" — an ERROR, not a warning,
115
+ * counted against the whole submission.
116
+ *
117
+ * Seen on a live site: a /search/ page was in the sitemap and
118
+ * noindex from the day it was built. Nothing compares the two
119
+ * lists, so it stayed invisible until the sitemap was submitted.
120
+ *
121
+ * Keep this list and the pages' own `noindex` in step. The 404 is
122
+ * never emitted here. Add a page to BOTH or to NEITHER.
123
+ */
124
+ filter: (page) =>
125
+ !page.includes('/thank-you') &&
126
+ !page.includes('/search') &&
127
+ /* Legal pages carry their own `noindex`, and unlike a rendered
128
+ page it is readable here — so this half derives instead of
129
+ being a second list to keep in step. */
130
+ !NOINDEX_LEGAL.some((route) => new URL(page).pathname === route),
131
+ serialize(item) {
132
+ if (item.url === `${SITE_URL}/`) item.priority = 1.0;
133
+ const lastmod = LASTMOD.get(new URL(item.url).pathname);
134
+ if (lastmod) item.lastmod = lastmod;
135
+ return item;
136
+ },
137
+ }),
138
+ ]
139
+ : []),
140
+ ],
141
+ markdown: {
142
+ // Markdown images ship as root-relative paths the dev server can serve
143
+ // directly; this plugin adds srcset and intrinsic dimensions at build time.
144
+ processor: satteri({ hastPlugins: [hastMedia] }),
145
+ shikiConfig: { theme: 'github-dark' },
146
+ },
147
+ vite: {
148
+ build: { cssMinify: 'lightningcss' },
149
+ },
150
+ });
@@ -0,0 +1,86 @@
1
+ # Analytics
2
+
3
+ What is already measured, what is genuinely missing, and the rules whose failure
4
+ looks like success.
5
+
6
+ All IDs live in `src/data/site.ts`. They are **the client's own or empty** — an unset ID must
7
+ never fall back to another project's container. Nothing is emitted off production, so staging
8
+ HTML contains zero references rather than a disabled snippet.
9
+
10
+ ---
11
+
12
+ ## 1. What is already captured — do not rebuild it
13
+
14
+ GA4's **enhanced measurement** is on by default and covers more than people expect. Building
15
+ a custom event for any of these produces two numbers that disagree:
16
+
17
+ | Already captured | Event |
18
+ | --- | --- |
19
+ | Outbound link clicks | `click` (with `outbound: true`) |
20
+ | Site search | `view_search_results` |
21
+ | Scroll depth (90%) | `scroll` |
22
+ | File downloads (pdf, doc, xlsx, zip…) | `file_download` |
23
+ | Video engagement (embedded YouTube) | `video_start`, `video_progress`, `video_complete` |
24
+ | Page views, including history-API routes | `page_view` |
25
+
26
+ Check Admin → Data streams → the stream → Enhanced measurement before writing any tag.
27
+
28
+ ## 2. What is genuinely missing
29
+
30
+ **`tel:` and `mailto:` clicks are not captured.** They are not outbound links in GA4's sense
31
+ and no enhanced-measurement category covers them. For a business whose phone number sits in
32
+ every header and footer, that is a primary conversion going unmeasured.
33
+
34
+ If you add it, the announcer belongs next to the one in `ContactForm.astro` — one function,
35
+ both pipes — and the same no-double-count rule applies.
36
+
37
+ ## 3. The rules whose failure looks like success
38
+
39
+ **Never add a GA4 configuration tag inside Tag Manager alongside a direct `gtag.js`.**
40
+ Both fire `page_view`. Sessions halve, bounce rate collapses, and the numbers look *better*,
41
+ which is why it survives review. `Base.astro` loads `gtag.js` directly and GTM as a
42
+ container — do not add a GA4 Configuration tag inside the container.
43
+
44
+ **Never send the same conversion down both pipes.** `announceLead()` in `ContactForm.astro`
45
+ pushes to `dataLayer` *and* calls `gtag`. If the container also has a trigger creating a GA4
46
+ event from `lead_submitted`, every lead counts twice.
47
+
48
+ **Never trigger a conversion on "URL contains `sent=1`".** It is the obvious implementation
49
+ and it is wrong: the enhanced path intercepts the submit and never changes the URL, so the
50
+ trigger only catches visitors without JavaScript. It fires correctly when you test by hand and
51
+ then under-reports for the life of the site. See the long comment in `ContactForm.astro`.
52
+
53
+ **Verify the container is the client's own.** Fetch it and read it — do not reason about it:
54
+
55
+ ```bash
56
+ curl -s https://example.com/ | grep -o 'GTM-[A-Z0-9]*' | sort -u
57
+ curl -s "https://www.googletagmanager.com/gtm.js?id=GTM-XXXX" | grep -oE 'G-[A-Z0-9]{8,}' | sort -u
58
+ ```
59
+
60
+ A site shipped once with a container inherited from the *previous* site's Google account:
61
+ invisible, un-editable and un-revokable by the client, executing on every page. It was empty.
62
+ Reading it is what found it.
63
+
64
+ **Confirm one pageview per visit in Realtime.** Nothing in a build catches a double-count.
65
+
66
+ ## 4. Cloudflare Web Analytics
67
+
68
+ Set `CF_BEACON_TOKEN` in `src/data/site.ts`. Independent of the Google tags — set either,
69
+ both or neither. Worth having on every site:
70
+
71
+ - **Real-user Core Web Vitals from the first visitor.** CrUX needs months of traffic before it
72
+ reports on a new domain, so field data is otherwise unavailable for exactly the period after
73
+ launch when it matters.
74
+ - **A control group for ad blockers.** Cookieless and first-party, so the gap between it and
75
+ GA4 measures what the Google tags are losing instead of leaving you to guess.
76
+
77
+ Cookieless also means it raises no consent-banner obligation of its own — see `compliance.md`
78
+ before assuming the same of anything else.
79
+
80
+ ## 5. Before go-live
81
+
82
+ - [ ] Both IDs are the client's own, from their own property
83
+ - [ ] No GA4 Configuration tag inside the GTM container
84
+ - [ ] No conversion trigger on `sent=1`
85
+ - [ ] One submission produces exactly one `generate_lead` in Realtime
86
+ - [ ] Staging HTML contains zero analytics references (`curl -s https://new.example.com/ | grep -c gtag` → 0)
@@ -0,0 +1,138 @@
1
+ # Editing content
2
+
3
+ Everything here is a file in the repo. A bad edit **fails the build** rather than the page —
4
+ that is deliberate, and it means a broken page can never reach visitors.
5
+
6
+ ---
7
+
8
+ ## Where things live
9
+
10
+ | Change | File |
11
+ | --- | --- |
12
+ | Phone, address, hours, service areas, credentials | `src/data/business.ts` |
13
+ | What you sell | `src/data/services.ts` |
14
+ | Service-area pages | `src/data/areas.ts` |
15
+ | Blog categories | `src/data/categories.ts` |
16
+ | A blog post | `src/content/blog/<slug>.md` |
17
+ | Privacy, terms, house rules | `src/content/legal/<slug>.md` |
18
+ | Images | `media/source/…`, then `npm run media` |
19
+ | Colours, fonts, spacing | `src/styles/tokens.css` |
20
+
21
+ **`business.ts` is the single source.** The header, footer, every call to action, the
22
+ notification emails and the structured data all read from it. Change the phone number there and
23
+ it changes everywhere — including what Google reads. Never type a phone number into a page.
24
+
25
+ **Legal pages write themselves into the footer.** A file at
26
+ `src/content/legal/privacy.md` is served at `/privacy/` and linked in the footer automatically —
27
+ add the file and the link appears; delete it and the link goes. There is no list to keep in
28
+ step, which is the point: a footer link to a page nobody wrote yet is a 404 on *every* page of
29
+ the site and nothing reports it.
30
+
31
+ Each one needs `title`, `description` and `effective` (the date the terms took effect, as
32
+ `YYYY-MM-DD`). Add `updated` only when it has genuinely changed, `navLabel` when the title is
33
+ too long for a footer, and `order` to move it in the row. Changing an effective date is editing
34
+ one line of frontmatter — no developer.
35
+
36
+ ---
37
+
38
+ ## Writing a post
39
+
40
+ Create `src/content/blog/my-post.md`. The filename is the URL.
41
+
42
+ ```markdown
43
+ ---
44
+ title: 'What a pre-purchase survey actually covers'
45
+ description: 'One sentence that would make sense as a search result. ~155 characters.'
46
+ pubDate: 2026-08-02
47
+ category: 'Tips'
48
+ image: 'blog/example-post' # a manifest key, never a URL or a path
49
+ imageAlt: 'A surveyor recording a reading'
50
+ draft: false
51
+ ---
52
+
53
+ Body in markdown. Start with the answer, then explain it.
54
+
55
+ ## Real headings, in order
56
+
57
+ Never skip a level to get a size — heading level is structure, size is a token. `h4` because
58
+ it "looked right" breaks the document outline for anyone using a screen reader.
59
+ ```
60
+
61
+ **The frontmatter is typed and validated at build time** (`src/content.config.ts`). A missing
62
+ `title`, a malformed date or a category that does not exist in `categories.ts` fails the build
63
+ with the file and field named. That is the system working.
64
+
65
+ - `draft: true` keeps it out of production entirely
66
+ - `category` must match a `name` in `categories.ts` exactly
67
+ - `image` is a manifest key — see below
68
+
69
+ ---
70
+
71
+ ## Images
72
+
73
+ ```bash
74
+ cp ~/Desktop/new-photo.jpg media/source/blog/example-post.jpg
75
+ npm run media
76
+ ```
77
+
78
+ Then reference the **key**, not a path: `image: 'blog/example-post'`.
79
+
80
+ The pipeline emits a modern format at several widths, plus a JPEG social twin for anything used
81
+ as an `og:image`, plus a dimensions manifest so every `<img>` carries `width` and `height` and
82
+ nothing shifts as the page loads.
83
+
84
+ - **Never reference an external URL.** Hotlinked stock images rot — two Pexels URLs referenced
85
+ by articles had already 404'd at source on the last migration
86
+ - **Brand assets copy byte-for-byte.** Do not run logos through the photo pipeline
87
+ - **Cap source images at ~2400px.** True camera originals belong outside version control
88
+ - **Write real alt text**, or `alt=""` if the image is decorative. An empty alt is correct far
89
+ more often than people expect; a filename never is
90
+ - **Check for duplicate covers.** Ten of seventeen posts sharing one photo reads as a broken
91
+ page, and nobody notices while writing
92
+
93
+ ---
94
+
95
+ ## Adding a service or a service area
96
+
97
+ Both are template + data: add an entry, the page generates.
98
+
99
+ **A service area is different.** Only add a place you can describe **distinctly** — which
100
+ authority issues the permit, what the housing stock is, what actually drives demand there.
101
+ Pages differing by nothing but the town name are doorway pages and can be penalised. A place
102
+ you cannot write about distinctly still belongs in `business.serviceAreas`; it just does not
103
+ get a page.
104
+
105
+ ---
106
+
107
+ ## Accessibility, when writing
108
+
109
+ The build cannot check these. They are the ones that matter and the ones that slip.
110
+
111
+ - **Headings in order.** Structure, not size
112
+ - **Link text that makes sense alone.** "Read more" is meaningless in a screen reader's link
113
+ list; "read the compliance guide" is not
114
+ - **Alt text that says what the image conveys**, not what it depicts. If it conveys nothing,
115
+ `alt=""`
116
+ - **Do not describe by position or colour.** "The button on the right", "the green box"
117
+ - **Expand an abbreviation on first use.** Assume no prior knowledge
118
+ - **Update `/accessibility`** — the statement is dated, and an undated statement reads as
119
+ abandoned. If you know of a gap, name it there. A documented gap is worth more than a clean
120
+ claim
121
+
122
+ ---
123
+
124
+ ## Publishing
125
+
126
+ ```bash
127
+ git add . && git commit -m "Add post: what a pre-purchase survey covers"
128
+ git push
129
+ ```
130
+
131
+ Deployment is a push to the tracked branch. To preview first:
132
+
133
+ ```bash
134
+ npm run build:staging && npx wrangler dev # localhost:8788
135
+ ```
136
+
137
+ If the build fails, **read the error** — it names the file and the field. It has caught a real
138
+ mistake, not invented one.
@@ -0,0 +1,182 @@
1
+ # Handover — Business Name
2
+
3
+ ⚠ **THIS IS A TEMPLATE. Fill it in and delete this block before sending.**
4
+ >
5
+ > This is the only document in the repo written for the **client**, not for whoever maintains
6
+ > the code. `runbook.md`, `content.md` and `traps.md` are for a developer; this one is for the
7
+ > person who owns the business and will be asked, in two years, who hosts their website.
8
+ >
9
+ > Render it with `npm run handover`, which writes `docs/handover.pdf` beside it. Send the PDF;
10
+ > keep the markdown in the repo so the next change updates the same document instead of
11
+ > starting a new one.
12
+ >
13
+ > Every ⚠ below marks something you must supply. A blank left in is worse than the section
14
+ > being absent, because it reads as a completed answer.
15
+ >
16
+ > Cross-check against `BUILD-STATE.md` before deleting that file — its **Integrations** and
17
+ > **Preserve** lines are the source for §3 and the "deliberately not built" list in §4.
18
+
19
+ **Prepared:** ⚠ date · **By:** ⚠ name · **Site:** ⚠ https://example.com
20
+
21
+ ---
22
+
23
+ ## 1. What you own, and where
24
+
25
+ You own all of it. Nothing here is held in anyone else's name, and you can move any of it
26
+ without our involvement.
27
+
28
+ | Thing | Where it lives | Who has admin |
29
+ | --- | --- | --- |
30
+ | Domain name | ⚠ registrar | ⚠ |
31
+ | Website hosting | ⚠ Cloudflare account | ⚠ |
32
+ | The site's code | ⚠ repository URL | ⚠ |
33
+ | Email sending | ⚠ provider | ⚠ |
34
+ | Analytics | ⚠ Google account | ⚠ |
35
+
36
+ ⚠ **Attach `recon/dns.md`** if this was a migration. It is the record of what the domain
37
+ published before the move — the thing to compare against if mail or a verification ever stops
38
+ working, and the thing a future supplier will ask for first.
39
+
40
+ ⚠ **Name a real person against each row, not a company.** "The agency" is not an answer when
41
+ the agency has moved on. If any row is owned by someone unreachable, say so here in plain
42
+ words — it is the single thing most likely to block an urgent change.
43
+
44
+ **Your domain is the one that matters.** As long as you control the domain registration, you
45
+ can move the site anywhere. Keep the registrar login somewhere you will still have it in five
46
+ years, and turn on auto-renew.
47
+
48
+ ---
49
+
50
+ ## 2. What it costs to run
51
+
52
+ ⚠ Fill in real figures. A client who does not know the running cost assumes it is zero and is
53
+ alarmed by the first invoice.
54
+
55
+ | Service | What it does | Cost |
56
+ | --- | --- | --- |
57
+ | Domain renewal | Your address on the internet | ⚠ /year |
58
+ | Hosting | Serves every page | ⚠ /month |
59
+ | Email sending | Enquiry notifications | ⚠ /month |
60
+ | ⚠ | ⚠ | ⚠ |
61
+
62
+ **What happens if a payment fails**, in order of how bad it is:
63
+
64
+ 1. **Domain lapses** — the site goes dark and email may stop. Recoverable for a short grace
65
+ period, then someone else can buy your address. This is the one to protect.
66
+ 2. **Hosting lapses** — the site goes dark, but nothing is lost. Restored by paying.
67
+ 3. **Email sending lapses** — the site keeps working and enquiries are still *saved*, but
68
+ nobody gets notified. See §5: this is why they are stored before the email is sent.
69
+
70
+ ---
71
+
72
+ ## 3. What is connected
73
+
74
+ ⚠ One row per integration, from `recon/integrations.md` and the `Integrations` lines in
75
+ `BUILD-STATE.md`. Include the ones you did not build, and say so.
76
+
77
+ | Connected | What it does | Whose account |
78
+ | --- | --- | --- |
79
+ | ⚠ | ⚠ | ⚠ |
80
+
81
+ ⚠ **Flag anything inherited.** A tag or booking widget set up by a previous supplier, in an
82
+ account nobody at the business can log into, keeps running and cannot be changed or switched
83
+ off. It is better to say this now than to discover it during an urgent request.
84
+
85
+ ---
86
+
87
+ ## 4. What we deliberately did not build
88
+
89
+ ⚠ List it. This section matters as much as everything above.
90
+
91
+ The difference between a decision and an oversight is whether it was written down. If a
92
+ feature was discussed and dropped — for cost, for speed, because it duplicated something you
93
+ already have — it belongs here with the reason.
94
+
95
+ | Not built | Why | What to do if you want it |
96
+ | --- | --- | --- |
97
+ | ⚠ | ⚠ | ⚠ |
98
+
99
+ ---
100
+
101
+ ## 5. Your data, and how long it is kept
102
+
103
+ **Enquiries from the website are saved before the notification email is sent.** If the email
104
+ provider has an outage you lose a notification, never the enquiry itself.
105
+
106
+ - **Where:** stored with the hosting, in your own account
107
+ - **How long:** ⚠ days, then automatically deleted — see `leadRetentionDays` in
108
+ `src/data/site.ts`
109
+ - **How to get it out:** ⚠ the export URL and where the token is kept
110
+
111
+ ⚠ **Keep this number and your privacy notice in step.** If the privacy notice says one
112
+ retention period and the site enforces another, the published one is the promise you are
113
+ judged against.
114
+
115
+ If someone asks you to delete their data, they are entitled to that. ⚠ Name who does it and
116
+ how.
117
+
118
+ ---
119
+
120
+ ## 6. Making changes
121
+
122
+ | Change | Who can do it |
123
+ | --- | --- |
124
+ | ⚠ Text on an existing page | ⚠ |
125
+ | ⚠ Adding a blog post | ⚠ |
126
+ | ⚠ Prices, opening hours, phone number | ⚠ |
127
+ | A new page or section | A developer |
128
+ | Anything about how it looks | A developer |
129
+
130
+ **Opening hours, address and phone number live in one place** and update the whole site at
131
+ once — the header, the footer, every button, the notification emails, and what Google reads.
132
+ Ask for them to be changed in that one file rather than page by page, or they will drift.
133
+
134
+ ---
135
+
136
+ ## 7. Accessibility
137
+
138
+ ⚠ Do not describe the site as "fully accessible" or "fully compliant". Nobody can claim that
139
+ honestly, and the claim is what gets challenged.
140
+
141
+ - **Published statement:** ⚠ https://example.com/accessibility
142
+ - **Standard targeted:** WCAG 2.2 AA
143
+ - **Last tested:** ⚠ date · **Tested how:** ⚠ automated tool + which pages by hand
144
+ - **Evidence:** ⚠ attach the newest pack from `docs/a11y-evidence/`
145
+ - **Known gaps:** ⚠ list them, with who owns each
146
+
147
+ **This needs re-testing whenever the site's layout changes**, not on a calendar. The published
148
+ statement carries a date and a claim; a redesign that leaves the date alone turns it into a
149
+ false statement about a site that no longer exists.
150
+
151
+ ⚠ Run `npm run a11y:evidence` after any layout change — it runs the automated sweep and the
152
+ reflow pass, writes a dated pack, and tells you when the statement's date has fallen behind.
153
+ Update the statement when you do.
154
+
155
+ ---
156
+
157
+ ## 8. If something looks wrong
158
+
159
+ In order — the first two resolve most of it:
160
+
161
+ 1. **Hard-refresh the page.** Browsers hold on to old copies. `Cmd+Shift+R` or `Ctrl+F5`.
162
+ 2. **Try it on a phone and on another network.** If it works there, it is your connection or
163
+ your browser, not the site.
164
+ 3. **Check the enquiry form** by sending yourself one. It should arrive within a minute.
165
+ 4. ⚠ **Contact:** name, email, and what response time to expect.
166
+
167
+ ⚠ Tell them what is *not* an emergency. A page that looks slightly different in one browser is
168
+ not the same as the site being down, and knowing the difference saves a weekend call.
169
+
170
+ ---
171
+
172
+ ## 9. Current state
173
+
174
+ ⚠ What is actually live, and what is outstanding, on the day you send this. Be specific and
175
+ dated. This section is why the document is trusted in six months.
176
+
177
+ **Live:** ⚠
178
+
179
+ **Outstanding:** ⚠ with an owner and a date against each
180
+
181
+ **Watch in the first month:** ⚠ e.g. enquiries arriving, search rankings settling after the
182
+ address change, the 404 log for URLs the migration missed
Binary file