[object Object]

← back to Prestige Car Wash

auto-save: 2026-07-26T16:14:26 (1 files) — server.js

1b0a067dbdcc09ea0abbfec9f57912af7495544f · 2026-07-26 16:14:33 -0700 · Steve Abrams

Files touched

Diff

commit 1b0a067dbdcc09ea0abbfec9f57912af7495544f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Jul 26 16:14:33 2026 -0700

    auto-save: 2026-07-26T16:14:26 (1 files) — server.js
---
 server.js | 40 +++++++++++++++++++++-------------------
 1 file changed, 21 insertions(+), 19 deletions(-)

diff --git a/server.js b/server.js
index dfae8b7..887b37f 100644
--- a/server.js
+++ b/server.js
@@ -444,19 +444,20 @@ function buildJsonLd() {
 }
 // Open Graph / Twitter tags for the homepage — reuses the page's own <title> and meta
 // description so social previews stay in sync with on-page SEO. og:image is a real asset.
-function buildSocialTags(html) {
+function buildSocialTags(html, pagePath) {
   // Bail if the HTML already carries og: tags (e.g. added directly to the file) so we
   // never emit duplicates that make Facebook's debugger flag the markup. (contrarian gate)
   if (/property=["']og:title["']/i.test(html)) return '';
   const p = places() || {};
-  const url = (p.website || '').replace(/\/$/, '');
+  const base = (p.website || '').replace(/\/$/, '');
+  const url = base ? base + (pagePath || '') : '';   // page-specific canonical + og:url
   const title = (html.match(/<title>([^<]*)<\/title>/i) || [])[1] || p.name || 'Prestige Car Wash';
   // Attribute-order-independent description extraction, with a real fallback (never a
   // blank social card). Handles both name-first and content-first, single or double quotes.
   const descMatch = html.match(/<meta\b[^>]*\bname=["']description["'][^>]*\bcontent=["']([^"']*)["']/i)
                  || html.match(/<meta\b[^>]*\bcontent=["']([^"']*)["'][^>]*\bname=["']description["']/i);
   const desc = (descMatch || [])[1] || `${p.name || 'Prestige Car Wash'} — no-pressure flat pricing, two-bucket hand wash, detailing & ceramic coating in the San Fernando Valley.`;
-  const img = url ? url + '/media/svc-full-service.png' : '';
+  const img = base ? base + '/media/svc-full-service.png' : '';
   const esc = s => String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
   const tags = [
     ['og:type', 'website'], ['og:site_name', p.name || 'Prestige Car Wash'],
@@ -464,27 +465,28 @@ function buildSocialTags(html) {
     ['twitter:card', img ? 'summary_large_image' : 'summary'],
     ['twitter:title', title], ['twitter:description', desc], img && ['twitter:image', img]
   ].filter(Boolean);
-  return tags.map(([k, v]) => k.startsWith('twitter')
+  const metas = tags.map(([k, v]) => k.startsWith('twitter')
     ? `<meta name="${k}" content="${esc(v)}">`
     : `<meta property="${k}" content="${esc(v)}">`).join('\n');
+  return (url ? `<link rel="canonical" href="${esc(url)}">\n` : '') + metas;
 }
-function homepageHtml() {
-  // Built fresh per request (sync file read + one string replace = microseconds) so the
-  // JSON-LD NEVER drifts from live places.json — a stale NAP is the exact thing Google
-  // penalizes, which would defeat the point of injecting it. (contrarian gate, 2026-07-26)
-  let html = fs.readFileSync(path.join(__dirname, 'public', 'index.html'), 'utf8');
-  let inject = buildSocialTags(html);
-  const ld = buildJsonLd();
-  if (ld) {
-    // escape "<" so the JSON can't break out of the <script> tag
-    inject += `\n<script type="application/ld+json">${JSON.stringify(ld).replace(/</g, '\\u003c')}</script>`;
-  }
+// Built fresh per request (sync file read + one string replace = microseconds) so the
+// injected JSON-LD/OG NEVER drift from live places.json — a stale NAP is the exact thing
+// Google penalizes, which would defeat the point of injecting it. (contrarian gate, 2026-07-26)
+function pageHtml(file, pagePath, ld) {
+  const html = fs.readFileSync(path.join(__dirname, 'public', file), 'utf8');
+  let inject = buildSocialTags(html, pagePath);
+  // escape "<" so the JSON can't break out of the <script> tag
+  if (ld) inject += `\n<script type="application/ld+json">${JSON.stringify(ld).replace(/</g, '\\u003c')}</script>`;
   return html.replace('</head>', inject + '\n</head>');
 }
-app.get('/', (req, res) => {
-  try { res.type('html').send(homepageHtml()); }
-  catch { res.sendFile(path.join(__dirname, 'public', 'index.html')); }
-});
+const sendPage = (res, file, pagePath, ld) => {
+  try { res.type('html').send(pageHtml(file, pagePath, ld)); }
+  catch { res.sendFile(path.join(__dirname, 'public', file)); }
+};
+app.get('/', (req, res) => sendPage(res, 'index.html', '', buildJsonLd()));
+app.get('/services', (req, res) => sendPage(res, 'services.html', '/services', null));
+app.get('/contact', (req, res) => sendPage(res, 'contact.html', '/contact', null));
 
 // XML sitemap generated from the public URL — kept in one place, no static file to drift.
 app.get('/sitemap.xml', (req, res) => {

← f74fd9f Admin: flag unworked leads going cold (>48h) on cards + funn  ·  back to Prestige Car Wash  ·  SEO: broaden OG/Twitter + per-page canonical to /services + aa47ccf →