[object Object]

← 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

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 →