[object Object]

← back to Thesetdecorator

TSD SEO fix: dedupe canonical in renderWithSEO (DTD verdict B)

6c2469ceccc056fccbf2dd7492c91ff154eecb7e · 2026-06-03 09:19:56 -0700 · Steve

Strip any static <link rel=canonical> from the .html template before injecting
the clean-URL one. 16 of 17 PUBLIC_PAGES carried a static .html-form canonical;
their clean-URL routes (/best-sellers etc.) were serving TWO conflicting
canonical tags (/slug vs /slug.html). Now exactly one, in the preferred
no-.html form. Verified on free port: all 17 clean routes -> 1 canonical;
static .html paths unaffected (still 1). Self-heals for future PUBLIC_PAGES.

Local only; needs deploy to affect prod (gated).

Files touched

Diff

commit 6c2469ceccc056fccbf2dd7492c91ff154eecb7e
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jun 3 09:19:56 2026 -0700

    TSD SEO fix: dedupe canonical in renderWithSEO (DTD verdict B)
    
    Strip any static <link rel=canonical> from the .html template before injecting
    the clean-URL one. 16 of 17 PUBLIC_PAGES carried a static .html-form canonical;
    their clean-URL routes (/best-sellers etc.) were serving TWO conflicting
    canonical tags (/slug vs /slug.html). Now exactly one, in the preferred
    no-.html form. Verified on free port: all 17 clean routes -> 1 canonical;
    static .html paths unaffected (still 1). Self-heals for future PUBLIC_PAGES.
    
    Local only; needs deploy to affect prod (gated).
---
 server.js | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/server.js b/server.js
index c11f7da..2715d20 100644
--- a/server.js
+++ b/server.js
@@ -299,6 +299,16 @@ function renderWithSEO(slug, req) {
   } else {
     injected = `\n<link rel="canonical" href="${escAttr(pageUrl)}">\n`;
   }
+  // Strip any static <link rel="canonical"> already in the .html template before
+  // injecting ours. Several PUBLIC_PAGES carry a static canonical in the .html
+  // form (e.g. /best-sellers.html); without this strip the clean-URL route would
+  // emit TWO canonical tags pointing at conflicting URLs (/slug vs /slug.html).
+  // This makes renderWithSEO the single source of truth — exactly one canonical,
+  // in the preferred no-.html form — and self-heals for any future page added to
+  // PUBLIC_PAGES. Only canonical <link> tags are removed; JSON-LD/other head
+  // content is untouched. (The .html files served directly via express.static
+  // keep their single static canonical and are unaffected.)
+  html = html.replace(/[ \t]*<link\b[^>]*\brel=["']?canonical["']?[^>]*>[ \t]*\n?/gi, '');
   const rendered = html.replace('</head>', injected + '</head>');
   SEO_CACHE.set(cacheKey, { ts: Date.now(), html: rendered });
   return rendered;

← 1ec82e5 backlog: mark loop #3 done (heading-order) + record audited-  ·  back to Thesetdecorator  ·  backlog: loop #4 (canonical-dedup fix) + queue deploy for St 619da85 →