Queue-North-Website/scripts/lib/routes.js

53 lines
1.9 KiB
JavaScript
Raw Normal View History

feat(build): the copy is checked before a single page is built from it src/data is prose in a data structure, and nothing checked it. The long-form service pages make that dangerous in a specific way: their copy arrives as an owner-approved markdown sheet that MIXES DIRECTIONS TO THE WEBSITE MANAGER INTO THE COPY. "Do not promise that every number is always portable." "Keep this factual:" "Place an official 8x8 Work screenshot beside this section." Those lines look exactly like copy, and publishing one puts an internal instruction on a customer-facing page. scripts/lib/content.js decides whether the content layer is publishable, and prerender.js runs it before rendering anything, so every build enforces it: the pre-commit hook, npm run verify, and the Docker image build. It refuses a website-manager direction, an em dash, a U+FFFD, markdown or an HTML tag left in a string, an unknown block type, a section id that is not letter-first, unique and free of the layout's own ids, a section that does not open with its direct answer (unless it declares kind list or faq), a FAQ question with no answer, a link to a route or fragment that does not exist, an image whose src is missing from public/ or has no alt or no dimensions, and the missing benefits or idealFor list that the short layout maps without checking. A description over 160 characters is a note, not a failure: owner-approved copy is published as written. scripts/lib/routes.js is now the one route list. prerender.js built its own while src/routes.jsx built the router's, and nothing compared them: a route in one and not the other is never prerendered, so the server answers it with 404.html while the site's own navigation links to it. entry-server.jsx exports the router table so the build can compare the two. Proven by mutation, seventeen of them, each expecting exactly one finding and getting it: unknown block type, FAQ answer removed, answer moved below its list, duplicate id, digit-leading id, link to /services/contact-centre, #no-such-section, missing image file, image without dimensions, image without alt, em dash, a manager direction, markdown bold, U+FFFD, an HTML tag, missing h1, empty section. Both generated content modules pass unmutated. Against a real build: an em dash added to industries.js failed npm run build naming the field, and a /pricing route added to src/routes.jsx failed it naming the route. Closes #230. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 04:43:01 -05:00
// Every route this site serves, in one place.
//
// `scripts/prerender.js` used to build its own list while `src/routes.jsx` built
// the router's, and nothing compared them. A route added to one and not the
// other is not a small bug: the page is never prerendered, so `server/index.js`
// answers a direct request for it with `dist/404.html`, and every visitor
// following a link and every crawler reading the sitemap gets a 404 on a page
// the site's own navigation points at.
//
// Plain JavaScript with no side effects, because prerender imports it in Node
// before Vite exists, and so does the validator runner.
import { services } from '../../src/data/services.js'
import { industries } from '../../src/data/industries.js'
export const STATIC_ROUTES = [
'/',
'/about',
'/services',
'/industries',
'/contact',
'/support',
'/privacy-policy',
]
export const ROUTES = [
...STATIC_ROUTES,
...services.map((service) => `/services/${service.id}`),
...industries.map((industry) => `/industries/${industry.id}`),
]
const join = (base, path) => `${base}/${path}`.replace(/\/{2,}/g, '/')
/** Flattens the router's nested table into the paths it declares. */
export const routerPaths = (table, base = '') =>
table.flatMap((route) => {
const self = route.index ? base || '/' : route.path === '/' ? '/' : join(base, route.path ?? '')
const children = route.children ? routerPaths(route.children, self === '/' ? '' : self) : []
return [self, ...children]
})
/**
* Routes the router declares that nothing prerenders. A path with a parameter
* (`/services/:slug`) or the catch-all is covered by the data lists above
* rather than by a literal, so neither counts as drift.
*/
export const routeDrift = (table) => [
...new Set(
routerPaths(table)
.filter((path) => !path.includes(':') && !path.includes('*'))
.filter((path) => !ROUTES.includes(path)),
),
]