[object Object]

← back to Prestige Car Wash

SEO: server-inject AutoWash/LocalBusiness JSON-LD from places.json (single source, no drift) — dtd:B, contrarian-gated (cache dropped)

cbe52ebfc13fff7e4039a3499c243bcfb9cc89be · 2026-07-26 14:30:28 -0700 · steve@designerwallcoverings.com

Files touched

Diff

commit cbe52ebfc13fff7e4039a3499c243bcfb9cc89be
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date:   Sun Jul 26 14:30:28 2026 -0700

    SEO: server-inject AutoWash/LocalBusiness JSON-LD from places.json (single source, no drift) — dtd:B, contrarian-gated (cache dropped)
---
 server.js | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 67 insertions(+)

diff --git a/server.js b/server.js
index 3d255f1..5e024bb 100644
--- a/server.js
+++ b/server.js
@@ -357,6 +357,73 @@ app.get('/favicon.svg', (req, res) => res.type('image/svg+xml').send(
 app.get('/favicon.ico', (req, res) => res.redirect(302, '/favicon.svg'));
 app.use('/media', express.static(path.join(__dirname, 'media')));
 app.get(/^\/(.+)\.html$/, (req, res) => res.redirect(301, '/' + req.params[0]));
+
+// LocalBusiness (AutoWash) JSON-LD for local SEO — built LIVE from data/places.json so it
+// can never drift from the visible name/address/phone/hours (Google cross-checks NAP).
+// Server-rendered into <head> so crawlers see it without executing JS. dtd:B 2026-07-26.
+// We only emit fields we can verify — NO aggregateRating (unverified) and NO geo (no lat/lng).
+const DOW = { Mon: 'Monday', Tue: 'Tuesday', Wed: 'Wednesday', Thu: 'Thursday', Fri: 'Friday', Sat: 'Saturday', Sun: 'Sunday' };
+const DOW_ORDER = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
+function to24(t) {
+  const m = String(t).match(/(\d{1,2}):(\d{2})\s*(AM|PM)/i);
+  if (!m) return null;
+  let h = +m[1]; const ap = m[3].toUpperCase();
+  if (ap === 'PM' && h !== 12) h += 12;
+  if (ap === 'AM' && h === 12) h = 0;
+  return String(h).padStart(2, '0') + ':' + m[2];
+}
+function parseHoursLine(line) {
+  const times = String(line).match(/\d{1,2}:\d{2}\s*[AP]M/gi);
+  if (!times || times.length < 2) return null;
+  const opens = to24(times[0]), closes = to24(times[1]);
+  const dayPart = String(line).split(/\s+\d/)[0].trim();
+  const dr = dayPart.split(/[–—-]/).map(s => s.trim());
+  let days = [];
+  if (dr.length === 2) { const a = DOW_ORDER.indexOf(dr[0]), b = DOW_ORDER.indexOf(dr[1]); if (a >= 0 && b >= 0) days = DOW_ORDER.slice(a, b + 1); }
+  else if (DOW[dr[0]]) days = [dr[0]];
+  if (!days.length || !opens || !closes) return null;
+  return { '@type': 'OpeningHoursSpecification', dayOfWeek: days.map(d => DOW[d]), opens, closes };
+}
+function buildJsonLd() {
+  const p = places();
+  if (!p || !p.name) return null;
+  const ld = { '@context': 'https://schema.org', '@type': 'AutoWash', name: p.name };
+  if (p.legal_name && p.legal_name !== p.name) ld.alternateName = p.legal_name;
+  if (p.website) ld.url = p.website;
+  if (p.phone) ld.telephone = p.phone;
+  if (p.address) {
+    const parts = p.address.split(',').map(s => s.trim());
+    const addr = { '@type': 'PostalAddress', addressCountry: 'US' };
+    if (parts[0]) addr.streetAddress = parts[0];
+    if (parts[1]) addr.addressLocality = parts[1];
+    if (parts[2]) { const sz = parts[2].match(/([A-Z]{2})\s*(\d{5})/); if (sz) { addr.addressRegion = sz[1]; addr.postalCode = sz[2]; } }
+    ld.address = addr;
+  }
+  const hrs = (p.hours || []).map(parseHoursLine).filter(Boolean);
+  if (hrs.length) ld.openingHoursSpecification = hrs;
+  const ig = p.instagram ? (/^https?:\/\//.test(p.instagram) ? p.instagram : `https://instagram.com/${String(p.instagram).replace(/^@/, '')}`) : null;
+  const same = [ig, p.yelp_url].filter(Boolean);
+  if (same.length) ld.sameAs = same;
+  return ld;
+}
+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');
+  const ld = buildJsonLd();
+  if (ld) {
+    // escape "<" so the JSON can't break out of the <script> tag
+    const json = JSON.stringify(ld).replace(/</g, '\\u003c');
+    html = html.replace('</head>', `<script type="application/ld+json">${json}</script>\n</head>`);
+  }
+  return html;
+}
+app.get('/', (req, res) => {
+  try { res.type('html').send(homepageHtml()); }
+  catch { res.sendFile(path.join(__dirname, 'public', 'index.html')); }
+});
+
 app.use(express.static(path.join(__dirname, 'public'), { extensions: ['html'] }));
 
 app.listen(PORT, () => {

← 57d8ba0 slideshow: sync sanitized deck (XSS-safe cover); render veri  ·  back to Prestige Car Wash  ·  SEO infra: robots.txt + generated /sitemap.xml + homepage OG 82b259c →