// 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 { execFileSync } from 'child_process' import path from 'path' import { fileURLToPath } from 'url' import { services } from '../../src/data/services.js' import { industries } from '../../src/data/industries.js' const repoRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '../..') 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((route) => !route.includes(':') && !route.includes('*')) .filter((route) => !ROUTES.includes(route)), ), ] // --- sitemap dates ----------------------------------------------------------- // // The files that produce each page. A page's `lastmod` is the newest commit // date among them, never the build timestamp: a sitemap that marks every page // as changed on every deploy is one search engines learn to ignore. const ROUTE_SOURCES = { '/': ['src/pages/Home.jsx'], '/about': ['src/pages/About.jsx'], '/services': ['src/pages/Services.jsx', 'src/data/services.js'], '/industries': ['src/pages/Industries.jsx', 'src/data/industries.js'], '/contact': ['src/pages/Contact.jsx'], '/support': ['src/pages/Support.jsx'], '/privacy-policy': ['src/pages/PrivacyPolicy.jsx', 'src/data/privacyPolicy.js'], } export const sourcesFor = (url) => { if (ROUTE_SOURCES[url]) return ROUTE_SOURCES[url] if (url.startsWith('/services/')) { // A page with owner-approved copy has its own content file, so editing that // copy moves that page's date and no other. const slug = url.slice('/services/'.length) return ['src/pages/ServiceDetail.jsx', 'src/data/services.js', `src/data/serviceContent/${slug}.js`] } return ['src/pages/IndustryDetail.jsx', 'src/data/industries.js'] } const gitLastModified = (files) => { let newest = null for (const file of files) { try { const iso = execFileSync('git', ['log', '-1', '--format=%cI', '--', file], { cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], }).trim() if (iso && (!newest || iso > newest)) newest = iso } catch { // git is unavailable, or the file is untracked. Either way, no date from // this file. Whether that leaves the ROUTE undated is the caller's // problem to report, and it must not pass silently: the production // sitemap carried no dates at all for months because this catch was the // end of the story. } } return newest ? newest.slice(0, 10) : null } /** * Route to YYYY-MM-DD, for the sitemap. * * The image build has no git: `.dockerignore` excludes `.git` and node:alpine * ships no git binary. So a map computed where git DOES exist can be injected * through SITEMAP_LASTMOD, which is what scripts/release.sh does. */ export const lastModByRoute = () => { const injected = process.env.SITEMAP_LASTMOD if (injected) { try { const parsed = JSON.parse(injected) if (parsed && typeof parsed === 'object') return parsed console.warn('routes: SITEMAP_LASTMOD is not an object, so falling back to git.') } catch (error) { console.warn(`routes: SITEMAP_LASTMOD is not valid JSON (${error.message}), so falling back to git.`) } } const dates = {} for (const route of ROUTES) { const date = gitLastModified(sourcesFor(route)) if (date) dates[route] = date } return dates }