← 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
M lib/render.jsM server.js
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 →