diff --git a/package.json b/package.json
index a856ccf..a0a59c7 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
"dev": "vite",
"prebuild": "node scripts/build-feeds.mjs",
"build": "tsc -b && vite build",
- "postbuild": "node scripts/build-feeds.mjs && cp public/feed.xml public/atom.xml public/feed.json public/sitemap.xml dist/ 2>/dev/null || true",
+ "postbuild": "node scripts/build-feeds.mjs && cp public/feed.xml public/atom.xml public/feed.json public/sitemap.xml dist/ 2>/dev/null || true && node scripts/prerender-route-heads.mjs",
"preview": "vite preview",
"build:feeds": "node scripts/build-feeds.mjs",
"typecheck": "tsc -b --noEmit",
diff --git a/scripts/prerender-route-heads.mjs b/scripts/prerender-route-heads.mjs
new file mode 100644
index 0000000..aa4dfcf
--- /dev/null
+++ b/scripts/prerender-route-heads.mjs
@@ -0,0 +1,53 @@
+#!/usr/bin/env node
+// prerender-route-heads.mjs — INTERIM SEO-collapse fix. The app is a client SPA; crawlers read
+// the static index.html shell, so every route shipped the homepage
+canonical=/+desc.
+// This postbuild step writes dist//index.html per static route with a UNIQUE per-route
+// , self-referencing , AND per-route — the last read
+// straight from each page's own useSEO() call (single source of truth, no divergence; only the
+// simple ${brand.business.name}/{BUSINESS_NAME} substitutions are resolved). CF Pages serves
+// these file-based, before the /* → /index.html SPA fallback. Replace with vite-react-ssg later.
+import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
+import { join } from 'node:path';
+
+const DIST = 'dist';
+const shell = readFileSync(join(DIST, 'index.html'), 'utf8');
+const origin = (shell.match(/]+rel=["']canonical["'][^>]*href=["'](https?:\/\/[^/"']+)/i) || [])[1] || '';
+const homeTitle = (shell.match(/([^<]*)<\/title>/i) || [])[1] || '';
+const businessName = homeTitle.split(/\s[—–-]\s/)[0].trim() || homeTitle.trim();
+
+// path → [page label (title), source component for description extraction]
+const ROUTES = {
+ '/about': ['About', 'About'], '/services': ['Services', 'Services'], '/contact': ['Contact', 'Contact'],
+ '/pricing': ['Pricing', 'Pricing'], '/faq': ['FAQ', 'FAQ'], '/team': ['Team', 'Team'],
+ '/gallery': ['Gallery', 'Gallery'], '/case-studies': ['Case studies', 'CaseStudies'],
+ '/blog': ['Blog', 'Blog'], '/privacy': ['Privacy Policy', 'Privacy'], '/terms': ['Terms of Service', 'Terms'],
+ '/accessibility': ['Accessibility', 'Accessibility'], '/studio': ['Brand Studio', 'Studio'],
+};
+
+// pull the description straight from the page's useSEO() — single source of truth.
+function pageDescription(component) {
+ const p = `src/pages/${component}.tsx`;
+ if (!existsSync(p)) return null;
+ const m = readFileSync(p, 'utf8').match(/useSEO\(\{[\s\S]{0,400}?description:\s*[`'"]([^`'"]+)[`'"]/);
+ if (!m) return null;
+ return m[1].replace(/\$\{brand\.business\.name\}/g, businessName).replace(/\{BUSINESS_NAME\}/g, businessName).trim();
+}
+const setMeta = (html, name, attr, val) =>
+ html.replace(new RegExp(`(]+${attr}=["']${name}["'][^>]*content=["'])[^"']*(["'])`, 'i'), `$1${val}$2`);
+
+let n = 0;
+for (const [path, [label, comp]] of Object.entries(ROUTES)) {
+ const title = `${label} — ${businessName}`;
+ const canonical = `${origin}${path}`;
+ const desc = pageDescription(comp);
+ let html = shell
+ .replace(/[^<]*<\/title>/i, `${title}`)
+ .replace(/(]+rel=["']canonical["'][^>]*href=["'])[^"']*(["'])/i, `$1${canonical}$2`);
+ html = setMeta(html, 'og:title', 'property', title);
+ html = setMeta(html, 'og:url', 'property', canonical);
+ if (desc) { html = setMeta(html, 'description', 'name', desc); html = setMeta(html, 'og:description', 'property', desc); }
+ mkdirSync(join(DIST, path), { recursive: true });
+ writeFileSync(join(DIST, path, 'index.html'), html);
+ n++;
+}
+console.log(`[prerender-route-heads] wrote ${n} per-route HTML files (title+canonical+description) · business="${businessName}"`);