Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
19 changes: 14 additions & 5 deletions src/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(), {
Expand Down
101 changes: 101 additions & 0 deletions test/generated-worker.test.mjs
Original file line number Diff line number Diff line change
@@ -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');
});