diff --git a/.gitignore b/.gitignore
index 24fa201..174fd64 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@ node_modules/
# Build output
dist/
+dist-ssr/
# Runtime/database artifacts
db/*.db
diff --git a/index.html b/index.html
index f452469..208ed65 100644
--- a/index.html
+++ b/index.html
@@ -29,8 +29,13 @@
+
+
diff --git a/package.json b/package.json
index 8b77e43..2c9c1a9 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,8 @@
"type": "module",
"scripts": {
"dev": "concurrently \"vite\" \"node server/index.js\"",
- "build": "vite build",
+ "build": "vite build && vite build --ssr src/entry-server.jsx --outDir dist-ssr && node scripts/prerender.js",
+ "build:client": "vite build",
"preview": "vite preview",
"start": "node server/index.js",
"server": "node server/index.js",
diff --git a/public/assets/cabling.webp b/public/assets/cabling.webp
deleted file mode 100644
index 5243ddf..0000000
Binary files a/public/assets/cabling.webp and /dev/null differ
diff --git a/public/sitemap.xml b/public/sitemap.xml
deleted file mode 100644
index 38e480d..0000000
--- a/public/sitemap.xml
+++ /dev/null
@@ -1,88 +0,0 @@
-
-
-
- https://queuenorth.com
- weekly
- 1.0
-
-
- https://queuenorth.com/about
- monthly
- 0.8
-
-
- https://queuenorth.com/services
- monthly
- 0.9
-
-
- https://queuenorth.com/services/unified-communications
- monthly
- 0.7
-
-
- https://queuenorth.com/services/contact-center
- monthly
- 0.7
-
-
- https://queuenorth.com/services/managed-support
- monthly
- 0.7
-
-
- https://queuenorth.com/services/consulting-training
- monthly
- 0.7
-
-
- https://queuenorth.com/services/infrastructure-cabling
- monthly
- 0.7
-
-
- https://queuenorth.com/services/wireless-access
- monthly
- 0.7
-
-
- https://queuenorth.com/services/local-networking
- monthly
- 0.7
-
-
- https://queuenorth.com/industries
- monthly
- 0.8
-
-
- https://queuenorth.com/industries/healthcare
- monthly
- 0.7
-
-
- https://queuenorth.com/industries/retail
- monthly
- 0.7
-
-
- https://queuenorth.com/industries/manufacturing
- monthly
- 0.7
-
-
- https://queuenorth.com/industries/education-finance
- monthly
- 0.7
-
-
- https://queuenorth.com/contact
- monthly
- 0.9
-
-
- https://queuenorth.com/support
- monthly
- 0.8
-
-
\ No newline at end of file
diff --git a/scripts/prerender.js b/scripts/prerender.js
new file mode 100644
index 0000000..3179070
--- /dev/null
+++ b/scripts/prerender.js
@@ -0,0 +1,201 @@
+// Build-time prerenderer.
+//
+// Renders every route to static HTML so crawlers that do not execute JavaScript
+// (Meta, LinkedIn, Slack, Bing) receive real content, correct per-route ,
+// meta description, canonical, and Open Graph tags. Google also benefits: pages
+// no longer sit in its deferred JS-rendering queue.
+//
+// Output layout (consumed by server/index.js):
+// dist/index.html -> /
+// dist/about/index.html -> /about
+// dist/services//index.html
+// dist/404.html -> served with a real 404 status
+//
+// Run automatically as part of `npm run build`.
+
+import { execFileSync } from 'child_process'
+import { mkdirSync, readFileSync, writeFileSync } from 'fs'
+import path from 'path'
+import { fileURLToPath } from 'url'
+
+import { render } from '../dist-ssr/entry-server.js'
+import { services } from '../src/data/services.js'
+import { industries } from '../src/data/industries.js'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const distDir = path.join(__dirname, '../dist')
+
+const STATIC_ROUTES = [
+ '/',
+ '/about',
+ '/services',
+ '/industries',
+ '/contact',
+ '/support',
+ '/privacy-policy',
+]
+
+const routes = [
+ ...STATIC_ROUTES,
+ ...services.map((s) => `/services/${s.id}`),
+ ...industries.map((i) => `/industries/${i.id}`),
+]
+
+// Tags the SEO component owns per-route. They are stripped from the template so
+// Helmet's values replace them instead of duplicating them.
+const TEMPLATE_TAGS_TO_STRIP = [
+ /[\s\S]*?<\/title>\s*/,
+ /]*>\s*/,
+ /]*>\s*/g,
+ /]*>\s*/g,
+ /\s*/,
+ /\s*/,
+]
+
+// Metadata React renders inside the component tree. React 19 hoists these into
+// in the browser and in its streaming renderer, but renderToString leaves
+// them inline, so the prerenderer performs the same hoist. Leaving them in
+// would put every title, canonical, and og: tag somewhere crawlers ignore.
+const HOISTABLE_TAGS =
+ /]*>[\s\S]*?<\/title>|]*?\/?>|]*?\/?>|