← back to Interiordesignershowroom
seo+security: guides CollectionPage schema + Article url/dateModified + escape </script> in ALL JSON-LD (fleet-wide injection fix, Cody-hardened)
cdb2c85769225eed6910fd997807205b0ca4f029 · 2026-08-03 11:25:08 -0700 · Steve Abrams
Files touched
M lib/render.jsM server.js
Diff
commit cdb2c85769225eed6910fd997807205b0ca4f029
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 3 11:25:08 2026 -0700
seo+security: guides CollectionPage schema + Article url/dateModified + escape </script> in ALL JSON-LD (fleet-wide injection fix, Cody-hardened)
---
lib/render.js | 2 +-
server.js | 7 ++++---
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/lib/render.js b/lib/render.js
index ed86576..bd3e895 100644
--- a/lib/render.js
+++ b/lib/render.js
@@ -271,7 +271,7 @@ function layout({ title, description, canonical, jsonld, image, body, activeNav
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500&family=DM+Serif+Display&display=swap">
<link rel="stylesheet" href="/css/site.css">
-${(Array.isArray(jsonld) ? jsonld : [jsonld]).filter(Boolean).map((j) => `<script type="application/ld+json">${JSON.stringify(j)}</script>`).join('')}
+${(Array.isArray(jsonld) ? jsonld : [jsonld]).filter(Boolean).map((j) => `<script type="application/ld+json">${JSON.stringify(j).replace(/</g, '\\u003c')}</script>`).join('')}
</head>
<body>
<a class="skip-link" href="#main-content">Skip to content</a>
diff --git a/server.js b/server.js
index b8d4e01..592bd7f 100644
--- a/server.js
+++ b/server.js
@@ -230,10 +230,11 @@ app.get('/guides', async (_req, res, next) => {
const body = `<section>${breadcrumbNav(crumbs)}<h1>Buying Guides</h1><div class="guide-grid">${cards || '<p>Guides coming soon.</p>'}</div></section>`;
// ItemList of the published guides so the index itself is a structured collection
// Google can surface (values are raw here — JSON.stringify escapes them, not esc()).
- const itemList = { '@context': 'https://schema.org', '@type': 'ItemList', name: 'Interior Design Buying Guides',
+ const itemList = { '@type': 'ItemList', name: 'Interior Design Buying Guides',
numberOfItems: rows.length,
itemListElement: rows.map((g, i) => ({ '@type': 'ListItem', position: i + 1, url: `${SITE.url}/guides/${g.slug}`, name: g.title })) };
- const jsonld = [itemList, breadcrumbJsonld(crumbs)].filter(Boolean);
+ const collection = { '@context': 'https://schema.org', '@type': 'CollectionPage', name: 'Interior Design Buying Guides', url: `${SITE.url}/guides`, mainEntity: itemList };
+ const jsonld = [collection, breadcrumbJsonld(crumbs)].filter(Boolean);
res.send(layout({ title: 'Interior Design Buying Guides', description: 'Editorial guides: how to choose, shop-the-look, and the best of every category.', canonical: `${SITE.url}/guides`, jsonld, body, activeNav: '/guides' }));
} catch (e) { next(e); }
});
@@ -248,7 +249,7 @@ app.get('/guides/:slug', async (req, res, next) => {
const r = await db.query(`SELECT ${COLS.PRODUCT} FROM products WHERE id = ANY($1) AND NOT suppressed`, [g.product_ids]);
picks = r.rows;
}
- const article = { '@context': 'https://schema.org', '@type': 'Article', headline: g.title, description: g.dek, image: g.hero_image ? absUrl(g.hero_image) : undefined, datePublished: g.created_at };
+ const article = { '@context': 'https://schema.org', '@type': 'Article', headline: g.title, description: g.dek, image: g.hero_image ? absUrl(g.hero_image) : undefined, url: `${SITE.url}/guides/${g.slug}`, datePublished: g.created_at, dateModified: g.updated_at };
const crumbs = [{ name: 'Home', url: `${SITE.url}/` }, { name: 'Guides', url: `${SITE.url}/guides` },
{ name: g.title, url: `${SITE.url}/guides/${g.slug}` }];
const body = `<article class="guide">
← 41049f5 auto-save: 2026-08-03T11:23:33 (1 files) — server.js
·
back to Interiordesignershowroom
·
perf: correct static caching — content-hashed images immutab f1385c9 →