← 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 →