From 917c8828db75dfb91d646005ce97929482d5b12c Mon Sep 17 00:00:00 2001 From: shigahi Date: Wed, 28 Oct 2026 11:52:37 +0100 Subject: [PATCH] Keep the canonical URL and og:url after the Notion client loads The Notion client rewrites link[rel=canonical], og:url and twitter:url to its own relative /Title- path after load, on every page. The client script already restored the configured titles and descriptions; it now also keeps these three tags on the URL the worker chose: the slug for mapped pages, the page ID for pages without one. The render check verifies this in the browser. Closes #193 Co-Authored-By: Claude Opus 5.5 --- scripts/render-check.mjs | 13 ++++++++++++- src/code.ts | 19 +++++++++++++------ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/scripts/render-check.mjs b/scripts/render-check.mjs index d5a4e6e..cd4468f 100644 --- a/scripts/render-check.mjs +++ b/scripts/render-check.mjs @@ -250,6 +250,17 @@ const CHECKS = { .find((href) => new URL(href).host !== host); return offsite ? `links to notion.site: ${offsite}` : null; }, + // The Notion client rewrites both tags after load; the worker keeps them on the page's own URL + "canonical url": async ({ page, target }) => { + const tags = await page.evaluate(() => ({ + canonical: document.querySelector('link[rel="canonical"]')?.getAttribute("href"), + ogUrl: document.querySelector('meta[property="og:url"]')?.getAttribute("content"), + })); + if (!tags.canonical || !tags.ogUrl) return `missing: ${JSON.stringify(tags)}`; + const canonical = new URL(tags.canonical, target.origin); + if (canonical.origin !== target.origin || !target.isRoot(canonical.pathname)) return `canonical: ${tags.canonical}`; + return new URL(tags.ogUrl, target.origin).href === canonical.href ? null : `og:url: ${tags.ogUrl}`; + }, "no errors": async ({ errors, failures }) => { const problems = [...errors, ...failures]; return problems.length ? problems.slice(0, 5).join("; ") : null; @@ -257,7 +268,7 @@ const CHECKS = { }; const GROUPS = [ - ["page renders", "database views", "database rows and formula", "images load", "pdf and file blocks", "mermaid diagram", "embeds and bookmarks", "anchor link", "crawler content", "no errors"], + ["page renders", "database views", "database rows and formula", "images load", "pdf and file blocks", "mermaid diagram", "embeds and bookmarks", "anchor link", "crawler content", "canonical url", "no errors"], // Checks that navigate run on a page of their own ["database item stays on domain"], ["navigation and history"], diff --git a/src/code.ts b/src/code.ts index 2fc0d62..320f5a6 100644 --- a/src/code.ts +++ b/src/code.ts @@ -1366,8 +1366,9 @@ ${ } } - // Titles and descriptions the client script keeps after Notion rewrites them + // Titles, descriptions and canonical URLs the client script keeps after Notion rewrites them const CLIENT_META = JSON.stringify({ + origin: 'https://' + MY_DOMAIN, title: PAGE_TITLE, description: PAGE_DESCRIPTION, brand: SITE_NAME || BRAND_REPLACEMENT, @@ -1444,15 +1445,21 @@ ${ } // Notion sets its own title and meta tags after load; keep the configured ones const META = \${CLIENT_META}; - function setContent(selector, value) { - const tag = document.querySelector(selector); - if (tag && value && tag.getAttribute('content') !== value) tag.setAttribute('content', value); + function setContent(selector, value, attribute = 'content') { + for (const tag of document.querySelectorAll(selector)) { + if (value && tag.getAttribute(attribute) !== value) tag.setAttribute(attribute, value); + } } function applyMeta() { const slug = location.pathname === NOT_FOUND_PATH ? '404' : PAGE_TO_SLUG[getPage()] ?? (slugs.includes(getSlug()) ? getSlug() : null); - // Like on the server, pages without a slug get no per-page metadata + // Like on the server, pages without a slug get no per-page metadata and + // are canonical at their page ID const page = (slug !== null && META.pages[slug]) || {}; + const path = slug !== null ? (slug ? '/' + slug : '') : (getPage() ? '/' + getPage() : location.pathname); + setContent('link[rel="canonical"]', META.origin + path, 'href'); + setContent('meta[property="og:url"]', META.origin + path); + setContent('meta[name="twitter:url"]', META.origin + path); let title = page.title || META.title; if (!title && META.brand && document.title.endsWith(' | Notion')) { title = document.title.slice(0, -' | Notion'.length) + ' | ' + META.brand; @@ -1470,7 +1477,7 @@ ${ childList: true, characterData: true, attributes: true, - attributeFilter: ['content'], + attributeFilter: ['content', 'href'], }); const observer = new MutationObserver(function() { if (redirected) return;