← back to Philipperomano
philipperomano SEO: add canonical + Open Graph + Twitter card + listing h1 (DTD verdict B)
8aa8581e4a0f3d82303a120cff54be6f63176719 · 2026-06-03 20:20:22 -0700 · Steve
Storefront had ZERO canonical/og/twitter and no h1 on the listing page.
Extended layout() with canonical/ogImage/ogTitle/ogDesc params injecting:
canonical, og:type/site_name/title/description/url/image, twitter:card.
Listing route: canonical = path + cat/type only (DTD-B, drops volatile
sort/page/q to consolidate faceted duplicates), og:image = first product;
added an sr-only h1. Detail route: per-product canonical /p/<handle> +
og:image (detail already had a visible h1). Added .sr-only utility CSS.
Verified on a free port: homepage + faceted + detail all correct, grid/sort/
density intact, JS unaffected. Local — needs deploy.
Files touched
Diff
commit 8aa8581e4a0f3d82303a120cff54be6f63176719
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Jun 3 20:20:22 2026 -0700
philipperomano SEO: add canonical + Open Graph + Twitter card + listing h1 (DTD verdict B)
Storefront had ZERO canonical/og/twitter and no h1 on the listing page.
Extended layout() with canonical/ogImage/ogTitle/ogDesc params injecting:
canonical, og:type/site_name/title/description/url/image, twitter:card.
Listing route: canonical = path + cat/type only (DTD-B, drops volatile
sort/page/q to consolidate faceted duplicates), og:image = first product;
added an sr-only h1. Detail route: per-product canonical /p/<handle> +
og:image (detail already had a visible h1). Added .sr-only utility CSS.
Verified on a free port: homepage + faceted + detail all correct, grid/sort/
density intact, JS unaffected. Local — needs deploy.
---
server.js | 28 ++++++++++++++++++++++++----
1 file changed, 24 insertions(+), 4 deletions(-)
diff --git a/server.js b/server.js
index 84ad35e..a05e5b6 100644
--- a/server.js
+++ b/server.js
@@ -426,13 +426,20 @@ function memoMailto(p) {
return `mailto:${PR_EMAIL}?subject=${sub}&body=${body}`;
}
-const layout = (title, body, { q='', type='', cat='' } = {}) => `<!doctype html>
+const PR_ORIGIN = 'https://philipperomano.com';
+const PR_DESC = 'Phillipe Romano commercial wallcoverings, naturals, and fabrics. Order memo samples free.';
+const layout = (title, body, { q='', type='', cat='', canonical='', ogImage='', ogTitle='', ogDesc='' } = {}) => `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>${esc(title)} — Phillipe Romano</title>
-<meta name="description" content="Phillipe Romano commercial wallcoverings, naturals, and fabrics. Order memo samples free.">
+<meta name="description" content="${esc(PR_DESC)}">
+${canonical ? `<link rel="canonical" href="${esc(canonical)}">\n` : ''}<meta property="og:type" content="website">
+<meta property="og:site_name" content="Phillipe Romano">
+<meta property="og:title" content="${esc(ogTitle || (title + ' — Phillipe Romano'))}">
+<meta property="og:description" content="${esc(ogDesc || PR_DESC)}">
+${canonical ? `<meta property="og:url" content="${esc(canonical)}">\n` : ''}${ogImage ? `<meta property="og:image" content="${esc(ogImage)}">\n` : ''}<meta name="twitter:card" content="${ogImage ? 'summary_large_image' : 'summary'}">
<style>
:root {
--bg:#faf7f2; --fg:#1a1714; --muted:#7c736a; --rule:#d8cec0;
@@ -443,6 +450,7 @@ const layout = (title, body, { q='', type='', cat='' } = {}) => `<!doctype html>
*{box-sizing:border-box;margin:0;padding:0}
html,body{font-family:var(--sans);background:var(--bg);color:var(--fg);line-height:1.5}
a{color:inherit;text-decoration:none}
+ .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
header.top{padding:24px 32px;border-bottom:1px solid var(--rule);background:#fff;display:flex;justify-content:space-between;align-items:flex-end;gap:24px;flex-wrap:wrap}
.brand{font-family:var(--serif);font-weight:600;font-size:32px;letter-spacing:.04em;text-transform:uppercase}
.brand small{display:block;font-family:var(--sans);font-size:11px;letter-spacing:.18em;font-weight:500;color:var(--muted);margin-top:4px}
@@ -653,6 +661,7 @@ app.get('/', (req, res) => {
const body = `
<main>
+ <h1 class="sr-only">Phillipe Romano — ${esc(cat === 'naturals' ? 'Naturals' : (type || 'All Products'))}</h1>
<div class="meta-line">
${q || type ? `Showing ${total.toLocaleString()} ${type ? `<strong style="color:var(--fg)">${esc(type)}</strong>` : ''} ${q ? `matching "<strong style="color:var(--fg)">${esc(q)}</strong>"` : ''}` : `${total.toLocaleString()} products available`}
</div>
@@ -715,7 +724,15 @@ app.get('/', (req, res) => {
})();
</script>
`;
- res.set('Content-Type','text/html').send(layout(`${cat === 'naturals' ? 'Naturals' : (type || 'All Products')}${q ? ` · ${q}` : ''}`, body, { q, type, cat }));
+ // Canonical (DTD verdict B): keep content filters (cat, type), drop volatile
+ // view params (sort, page, q) so all sorted/paginated/searched views of a
+ // listing consolidate to one canonical. og:image = first product on the page.
+ const canonParams = new URLSearchParams();
+ if (cat) canonParams.set('cat', cat);
+ if (type) canonParams.set('type', type);
+ const canonical = PR_ORIGIN + '/' + (canonParams.toString() ? '?' + canonParams.toString() : '');
+ const ogImage = (items[0] && items[0].image_url) || '';
+ res.set('Content-Type','text/html').send(layout(`${cat === 'naturals' ? 'Naturals' : (type || 'All Products')}${q ? ` · ${q}` : ''}`, body, { q, type, cat, canonical, ogImage }));
});
app.get('/p/:handle', (req, res) => {
@@ -740,7 +757,10 @@ app.get('/p/:handle', (req, res) => {
</div>
</div>
`;
- res.set('Content-Type','text/html').send(layout(p.title, body));
+ res.set('Content-Type','text/html').send(layout(p.title, body, {
+ canonical: PR_ORIGIN + '/p/' + encodeURIComponent(p.handle || ''),
+ ogImage: p.image_url || '',
+ }));
});
app.get('/health', (_req, res) => res.json({ ok: true, products: ENRICHED.length, types: TYPES }));
← 6f1ad72 gitignore: add full backup-file pattern set (*.bak-*, *.pre-
·
back to Philipperomano
·
Scrub residual vendor names from products.json (title/vendor 9ddbf64 →