[object Object]

← back to Interiordesignershowroom

seo: honest Offer schema (data-driven itemCondition + conditional availability) + complete article OG tags (Cody-hardened)

6c5123a3249691d146c9fdc4aa1544c4ae3075c9 · 2026-08-03 11:47:33 -0700 · Steve Abrams

Files touched

Diff

commit 6c5123a3249691d146c9fdc4aa1544c4ae3075c9
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 3 11:47:33 2026 -0700

    seo: honest Offer schema (data-driven itemCondition + conditional availability) + complete article OG tags (Cody-hardened)
---
 lib/render.js | 16 +++++++++++++---
 server.js     |  2 +-
 2 files changed, 14 insertions(+), 4 deletions(-)

diff --git a/lib/render.js b/lib/render.js
index bd3e895..b7f7a03 100644
--- a/lib/render.js
+++ b/lib/render.js
@@ -131,13 +131,23 @@ function orgWebsiteJsonld() {
 // ItemList of Product schema for the listing pages (/shop, /rooms/:room). This is
 // the structured data that lets the commercial pages surface in rich results — the
 // gap that previously left only editorial guides with schema.
+// Honest itemCondition from the title (affiliate feeds can carry refurb/used items);
+// default New. Keeps the schema factual rather than blanket-asserting New on everything.
+function conditionOf(title) {
+  const t = String(title || '').toLowerCase();
+  if (/refurbish|renewed/.test(t)) return 'https://schema.org/RefurbishedCondition';
+  if (/\bused\b|pre-owned|open box/.test(t)) return 'https://schema.org/UsedCondition';
+  return 'https://schema.org/NewCondition';
+}
 function productListJsonld(products = [], listUrl) {
+  // Google recommends priceValidUntil on Offer; a rolling 30-day window is the feed norm.
+  const priceValidUntil = new Date(Date.now() + 30 * 86400e3).toISOString().slice(0, 10);
   const items = products.slice(0, 60).map((p, i) => {
     const price = p.sale_price ?? p.price;
     const prod = { '@type': 'Product', name: p.title, url: `${SITE.url}/go/${p.id}` };
     if (p.image_url) prod.image = absUrl(p.image_url);
     if (p.brand || p.advertiser) prod.brand = { '@type': 'Brand', name: p.brand || p.advertiser };
-    if (price != null) prod.offers = { '@type': 'Offer', price: Number(price).toFixed(2), priceCurrency: 'USD', availability: 'https://schema.org/InStock' };
+    if (price != null) prod.offers = { '@type': 'Offer', price: Number(price).toFixed(2), priceCurrency: 'USD', availability: p.in_stock === false ? 'https://schema.org/OutOfStock' : 'https://schema.org/InStock', itemCondition: conditionOf(p.title), priceValidUntil };
     return { '@type': 'ListItem', position: i + 1, item: prod };
   });
   return { '@context': 'https://schema.org', '@type': 'ItemList', url: listUrl, numberOfItems: items.length, itemListElement: items };
@@ -221,7 +231,7 @@ function breadcrumbJsonld(items = []) {
   };
 }
 
-function layout({ title, description, canonical, jsonld, image, body, activeNav }) {
+function layout({ title, description, canonical, jsonld, image, body, activeNav, ogType, ogArticle }) {
   const ogImage = absUrl(image) || `${SITE.url}/img/og.png`;
   const nav = [
     ['/shop', 'Shop'],
@@ -260,7 +270,7 @@ function layout({ title, description, canonical, jsonld, image, body, activeNav
 <meta property="og:site_name" content="${esc(SITE.name)}">
 <meta property="og:title" content="${esc(title)}">
 <meta property="og:description" content="${esc(description || SITE.tagline)}">
-<meta property="og:type" content="website">
+<meta property="og:type" content="${esc(ogType || 'website')}">${ogType === 'article' && ogArticle ? `<meta property="article:published_time" content="${esc(ogArticle.published)}"><meta property="article:modified_time" content="${esc(ogArticle.modified)}">` : ''}
 <meta property="og:image" content="${esc(ogImage)}">
 <meta name="twitter:card" content="summary_large_image">
 <meta name="twitter:image" content="${esc(ogImage)}">
diff --git a/server.js b/server.js
index f143d3f..8a6b9a0 100644
--- a/server.js
+++ b/server.js
@@ -273,7 +273,7 @@ app.get('/guides/:slug', async (req, res, next) => {
       ${picks.length ? `<h2>Shop this guide</h2><div class="grid">${picks.map(productCard).join('')}</div>` : ''}
     </article>`;
     const jsonld = [article, breadcrumbJsonld(crumbs)].filter(Boolean);
-    res.send(layout({ title: g.title, description: g.dek, canonical: `${SITE.url}/guides/${g.slug}`, jsonld, image: g.hero_image, body, activeNav: '/guides' }));
+    res.send(layout({ title: g.title, description: g.dek, canonical: `${SITE.url}/guides/${g.slug}`, jsonld, image: g.hero_image, body, activeNav: '/guides', ogType: 'article', ogArticle: { published: new Date(g.created_at).toISOString(), modified: new Date(g.updated_at).toISOString() } }));
   } catch (e) { next(e); }
 });
 

← f1385c9 perf: correct static caching — content-hashed images immutab  ·  back to Interiordesignershowroom  ·  auto-save: 2026-08-03T11:53:46 (3 files) — lib/render.js ser 3b62dda →