From e529e2f8a5c7b01e90309b02147e7ca3bbfd2a63 Mon Sep 17 00:00:00 2001 From: Vamsi-2581 <245859299+Vamsi-2581@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:22:45 +0530 Subject: [PATCH 1/4] fix: preserve canonical URL after client navigation --- src/code.ts | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/code.ts b/src/code.ts index 2fc0d62..a6f0f1f 100644 --- a/src/code.ts +++ b/src/code.ts @@ -1448,6 +1448,17 @@ ${ const tag = document.querySelector(selector); if (tag && value && tag.getAttribute('content') !== value) tag.setAttribute('content', value); } + function getCanonicalUrl() { + const pageId = getPage() || SLUG_TO_PAGE[getSlug()]; + if (pageId) { + const slug = PAGE_TO_SLUG[pageId]; + const path = slug === undefined ? '/' + pageId : (slug ? '/' + slug : ''); + return 'https://' + MY_DOMAIN + path; + } + const isNotFound = NOT_FOUND_PATH !== null && location.pathname === NOT_FOUND_PATH; + return 'https://' + MY_DOMAIN + (isNotFound ? '/404' : ''); + } + function applyMeta() { const slug = location.pathname === NOT_FOUND_PATH ? '404' : PAGE_TO_SLUG[getPage()] ?? (slugs.includes(getSlug()) ? getSlug() : null); @@ -1464,14 +1475,22 @@ ${ setContent('meta[name="description"]', description); setContent('meta[property="og:description"]', description); setContent('meta[name="twitter:description"]', description); + const canonicalUrl = getCanonicalUrl(); + const canonical = document.querySelector('link[rel="canonical"]'); + if (canonical && canonical.getAttribute('href') !== canonicalUrl) { + canonical.setAttribute('href', canonicalUrl); + } + setContent('meta[property="og:url"]', canonicalUrl); + setContent('meta[name="twitter:url"]', canonicalUrl); } new MutationObserver(applyMeta).observe(document.head, { subtree: true, childList: true, characterData: true, attributes: true, - attributeFilter: ['content'], + attributeFilter: ['content', 'href'], }); + applyMeta(); const observer = new MutationObserver(function() { if (redirected) return; const nav = document.querySelector('.notion-topbar'); @@ -1480,6 +1499,7 @@ ${ || mobileNav && mobileNav.firstChild) { redirected = true; updateSlug(); + applyMeta(); // Notion only exposes ThemeStore via __console in debug sessions if (window.__console?.environment?.ThemeStore) addDarkModeButton(nav ? 'web' : 'mobile'); const onpopstate = window.onpopstate; @@ -1492,6 +1512,7 @@ ${ } if (onpopstate) onpopstate.apply(this, [].slice.call(arguments)); updateSlug(); + applyMeta(); }; } }); @@ -1503,6 +1524,7 @@ ${ if (!redirected) { redirected = true; updateSlug(); + applyMeta(); const onpopstate = window.onpopstate; window.onpopstate = function() { if (slugs.includes(getSlug())) { @@ -1513,6 +1535,7 @@ ${ } if (onpopstate) onpopstate.apply(this, [].slice.call(arguments)); updateSlug(); + applyMeta(); }; } }, 2000); @@ -1529,7 +1552,9 @@ ${ if (id && pages.includes(id)) { arguments[2] = '/' + PAGE_TO_SLUG[id]; } - return pushState.apply(window.history, arguments); + const result = pushState.apply(window.history, arguments); + applyMeta(); + return result; }; const open = window.XMLHttpRequest.prototype.open; window.XMLHttpRequest.prototype.open = function() { From 2da9ecb8f21c0cbdac7c998b44595a00d57415b0 Mon Sep 17 00:00:00 2001 From: Vamsi-2581 <245859299+Vamsi-2581@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:22:51 +0530 Subject: [PATCH 2/4] test: cover client canonical metadata updates --- test/generated-worker.test.mjs | 97 ++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/test/generated-worker.test.mjs b/test/generated-worker.test.mjs index 1bfc461..4cdf8ad 100644 --- a/test/generated-worker.test.mjs +++ b/test/generated-worker.test.mjs @@ -1,4 +1,5 @@ import assert from 'node:assert/strict'; +import { runInNewContext } from 'node:vm'; import { before, test } from 'node:test'; import { gzipSync } from 'node:zlib'; import { createServer } from 'vite'; @@ -310,3 +311,99 @@ test('the home page keeps its canonical URL and metadata', async (t) => { title: 'Home title', }); }); + + +function fakeTag(initialAttributes) { + const attributes = { ...initialAttributes }; + return { + getAttribute: (name) => attributes[name] ?? null, + setAttribute: (name, value) => { attributes[name] = value; }, + }; +} + +async function runClientMetadata(t, path) { + const handlers = await rewriteHandlers(t, path, HOME_METADATA); + const appended = []; + handlers.body.element({ append: (html) => appended.push(html) }); + const script = appended.join('').match(/