From eab05f7659ef3caaad4a22da45101e798d20044d Mon Sep 17 00:00:00 2001 From: LuuSir <123914748+LuuSir@users.noreply.github.com> Date: Fri, 16 Oct 2026 08:29:12 +0000 Subject: [PATCH] Keep published site context during page revalidation --- .github/workflows/ci.yml | 3 + package.json | 1 + src/code.ts | 19 +++++-- test/generated-worker.test.mjs | 101 +++++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 test/generated-worker.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8893ee..c9602c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,9 @@ jobs: - name: Build run: npm run build + - name: Test generated worker + run: npm test + - name: Upload build artifacts uses: actions/upload-artifact@v7 with: diff --git a/package.json b/package.json index 3b73400..187fb3f 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "wrangler": "^4.145.0" }, "scripts": { + "test": "node --test test/*.test.mjs", "start": "vite", "build": "tsc --noEmit && vite build", "preview": "vite preview", diff --git a/src/code.ts b/src/code.ts index 79c08e3..0790be2 100644 --- a/src/code.ts +++ b/src/code.ts @@ -951,16 +951,25 @@ ${ // like "/about"). Without this, all slug pages render the root page (#90). try { const reqJson = JSON.parse(reqBody); - if (!reqJson.blockId) { + // Client revalidation can omit the site context and fall back to + // public-share-link mode, replacing an already rendered page with 404. + if (!reqJson.spaceDomain) reqJson.spaceDomain = NOTION_SPACE_DOMAIN; + reqJson.requestedOnPublicDomain = true; + if (!reqJson.blockId && !reqJson.pageId) { let pageId = ''; const referer = request.headers.get('Referer'); if (referer) { try { - const refPath = new URL(referer).pathname; + const refUrl = new URL(referer); + const refPath = refUrl.pathname; + // A database peek keeps the container in the path and the open + // item in ?p=. Prefer that item when inferring the requested page. + const peekPage = (refUrl.searchParams.get('p') || '').replace(/-/g, ''); + if (/^[0-9a-f]{32}$/i.test(peekPage)) pageId = peekPage; const slug = pathToSlug(refPath); - if (slug !== null) { + if (!pageId && slug !== null) { pageId = SLUG_TO_PAGE[slug]; - } else { + } else if (!pageId) { const match = refPath.match(/[0-9a-f]{32}/); // Any other document path was served as the custom 404 page pageId = match ? match[0] : CUSTOM_404_PAGE_ID; @@ -970,9 +979,9 @@ ${ if (!pageId) pageId = SLUG_TO_PAGE[''] || ''; if (pageId) { reqJson.blockId = pageId.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, '$1-$2-$3-$4-$5'); - reqBody = JSON.stringify(reqJson); } } + reqBody = JSON.stringify(reqJson); } catch (e) {} // Proxy getPublicPageData and rewrite domain info response = await fetch(url.toString(), { diff --git a/test/generated-worker.test.mjs b/test/generated-worker.test.mjs new file mode 100644 index 0000000..49ee048 --- /dev/null +++ b/test/generated-worker.test.mjs @@ -0,0 +1,101 @@ +import assert from 'node:assert/strict'; +import { after, test } from 'node:test'; +import { createServer } from 'vite'; + +const ROOT = '11111111111111111111111111111111'; +const CHAPTER = '22222222222222222222222222222222'; +const OTHER = '33333333333333333333333333333333'; +const dashed = (id) => id.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, '$1-$2-$3-$4-$5'); +const vite = await createServer({ server: { middlewareMode: true }, appType: 'custom', logLevel: 'error' }); +after(() => vite.close()); +const { default: generate } = await vite.ssrLoadModule('/src/code.ts'); +const script = generate({ + myDomain: 'notes.example.com', + notionUrl: `https://source-workspace.notion.site/${ROOT}`, + slugs: [['chapter', `https://source-workspace.notion.site/${OTHER}`]], + pageTitle: '', pageDescription: '', googleFont: '', customScript: '', customCss: '', + optionImage: {}, pageMetadata: {}, structuredData: { enabled: false, schemaType: 'WebPage' }, + branding: {}, socialPreview: {}, seo: {}, analytics: {}, caching: { enabled: false }, + customHtml: {}, custom404: {}, subdomainRedirects: [], redirectRules: [], + rss: { enabled: false }, i18n: { enabled: false, defaultLocale: 'en' }, + ogImageGeneration: { enabled: false }, +}); +const { default: worker } = await import(`data:text/javascript;base64,${Buffer.from(script).toString('base64')}`); + +async function forward(t, body, referer, extraHeaders = {}) { + const upstream = t.mock.method(globalThis, 'fetch', async () => Response.json({ spaceDomain: 'source-workspace' })); + const request = new Request('https://notes.example.com/api/v3/getPublicPageData', { + method: 'POST', + body: typeof body === 'string' ? body : JSON.stringify(body), + headers: { 'Content-Type': 'application/json', ...(referer ? { Referer: referer } : {}), ...extraHeaders }, + }); + const response = await worker.fetch(request, {}, { waitUntil() {} }); + assert.equal(response.status, 200); + assert.equal(upstream.mock.calls.length, 1); + const [url, init] = upstream.mock.calls[0].arguments; + assert.equal(new URL(url).hostname, 'source-workspace.notion.site'); + return { body: init.body, headers: new Headers(init.headers) }; +} + +test('revalidation retains published-site context when the client omits spaceDomain', async (t) => { + const forwarded = await forward(t, { blockId: dashed(CHAPTER), requestedOnPublicDomain: false }, `https://notes.example.com/${CHAPTER}`); + assert.deepEqual(JSON.parse(forwarded.body), { + blockId: dashed(CHAPTER), spaceDomain: 'source-workspace', requestedOnPublicDomain: true, + }); +}); + +test('an existing pageId is not shadowed by an inferred blockId', async (t) => { + const forwarded = await forward(t, { pageId: dashed(CHAPTER), spaceDomain: 'notes' }, 'https://notes.example.com/'); + const body = JSON.parse(forwarded.body); + assert.equal(body.pageId, dashed(CHAPTER)); + assert.equal(body.blockId, undefined); + assert.equal(body.spaceDomain, 'source-workspace'); +}); + +for (const page of [CHAPTER, dashed(CHAPTER)]) { + test(`a database peek targets its ?p= page (${page.length}-character ID)`, async (t) => { + const forwarded = await forward(t, {}, `https://notes.example.com/${ROOT}?v=${OTHER}&p=${page}`); + assert.equal(JSON.parse(forwarded.body).blockId, dashed(CHAPTER)); + }); +} + +test('a database peek takes precedence over a configured pretty slug', async (t) => { + const forwarded = await forward(t, {}, `https://notes.example.com/chapter?p=${CHAPTER}`); + assert.equal(JSON.parse(forwarded.body).blockId, dashed(CHAPTER)); +}); + +test('a malformed peek falls back to the document page rather than the view ID', async (t) => { + const forwarded = await forward(t, {}, `https://notes.example.com/${ROOT}?p=invalid&v=${CHAPTER}`); + assert.equal(JSON.parse(forwarded.body).blockId, dashed(ROOT)); +}); + +test('an explicit blockId wins over the database peek', async (t) => { + const forwarded = await forward(t, { blockId: dashed(OTHER) }, `https://notes.example.com/${ROOT}?p=${CHAPTER}`); + assert.equal(JSON.parse(forwarded.body).blockId, dashed(OTHER)); +}); + +test('slug and root requests still infer a blockId', async (t) => { + const forwarded = await forward(t, {}, 'https://notes.example.com/chapter'); + assert.equal(JSON.parse(forwarded.body).blockId, dashed(OTHER)); +}); + +test('an absent Referer falls back to the configured root', async (t) => { + const forwarded = await forward(t, {}, undefined); + assert.equal(JSON.parse(forwarded.body).blockId, dashed(ROOT)); +}); + +test('normalization keeps note content and Notion fanout routing headers intact', async (t) => { + const forwarded = await forward(t, { + blockId: dashed(CHAPTER), spaceDomain: 'notes', content: 'notes is a word in the page', nested: { domain: 'notes' }, + }, undefined, { 'x-notion-cell': 'cell-test', 'notion-client-version': 'test-version' }); + const body = JSON.parse(forwarded.body); + assert.equal(body.content, 'notes is a word in the page'); + assert.equal(body.nested.domain, 'source-workspace'); + assert.equal(forwarded.headers.get('x-notion-cell'), 'cell-test'); + assert.equal(forwarded.headers.get('notion-client-version'), 'test-version'); +}); + +test('invalid JSON passes through without fabricating a request', async (t) => { + const forwarded = await forward(t, '{invalid', undefined); + assert.equal(forwarded.body, '{invalid'); +});