Skip to content

Commit 5f5394c

Browse files
authored
Merge pull request #153 from classmethod/fix/soft-404
Return 404 for unknown page paths
2 parents e0ed74f + 7bfafc9 commit 5f5394c

2 files changed

Lines changed: 80 additions & 19 deletions

File tree

‎src/App.tsx‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ export default function App() {
176176
});
177177
const [seo, setSeo] = useState<SeoOptions>({
178178
aiAttribution: "",
179+
robotsRules: "",
179180
});
180181
const [slugMetadataExpanded, setSlugMetadataExpanded] = useState<
181182
Record<number, boolean>
@@ -1156,6 +1157,19 @@ export default function App() {
11561157
variant="outlined"
11571158
size="small"
11581159
/>
1160+
<TextField
1161+
fullWidth
1162+
multiline
1163+
minRows={2}
1164+
label="robots.txt Rules"
1165+
margin="dense"
1166+
placeholder={"User-agent: *\nAllow: /"}
1167+
helperText="Custom robots.txt rules. The sitemap line is added automatically."
1168+
onChange={(e) => handleSeoChange("robotsRules", e.target.value)}
1169+
value={seo.robotsRules}
1170+
variant="outlined"
1171+
size="small"
1172+
/>
11591173
</Box>
11601174

11611175
<Box sx={{ mt: 3, pt: 2, borderTop: 1, borderColor: "grey.300" }}>

‎src/code.ts‎

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export interface SocialPreviewOptions {
4747

4848
export interface SeoOptions {
4949
aiAttribution?: string;
50+
robotsRules?: string;
5051
}
5152

5253
export interface AnalyticsOptions {
@@ -239,8 +240,10 @@ ${slugs
239240
/*
240241
* Step 3.4: SEO configuration (optional)
241242
* AI attribution for proper citation in AI-generated content
243+
* robots.txt rules (the sitemap line is added automatically)
242244
*/
243245
const AI_ATTRIBUTION = ${str(seo?.aiAttribution)};
246+
const ROBOTS_RULES = ${tpl(seo?.robotsRules?.trim())};
244247
245248
/*
246249
* Step 3.5: analytics configuration (optional)
@@ -679,7 +682,7 @@ ${
679682
// Use the original Notion site domain instead of www.notion.so
680683
url.hostname = NOTION_SITE_DOMAIN;
681684
if (url.pathname === '/robots.txt') {
682-
return new Response('Sitemap: https://' + MY_DOMAIN + '/sitemap.xml');
685+
return new Response((ROBOTS_RULES || 'User-agent: *\\nAllow: /') + '\\n\\nSitemap: https://' + MY_DOMAIN + '/sitemap.xml\\n');
683686
}
684687
if (url.pathname === '/sitemap.xml') {
685688
let response = new Response(generateSitemap());
@@ -696,6 +699,9 @@ ${
696699
const slug = url.pathname.replace('/og-image/', '');
697700
return generateOgImage(slug);
698701
}
702+
if (isUnknownPageNavigation(request, url.pathname)) {
703+
return notFoundResponse(url);
704+
}
699705
let response;
700706
const isAppJs = url.pathname.startsWith('/app') && url.pathname.endsWith('js');
701707
const isAssetJs = url.pathname.startsWith('/_assets/') && url.pathname.endsWith('.js');
@@ -725,7 +731,8 @@ ${
725731
pageId = SLUG_TO_PAGE[slug];
726732
} else {
727733
const match = refPath.match(/[0-9a-f]{32}/);
728-
if (match) pageId = match[0];
734+
// Any other document path was served as the custom 404 page
735+
pageId = match ? match[0] : CUSTOM_404_PAGE_ID;
729736
}
730737
} catch (e) {}
731738
}
@@ -803,20 +810,7 @@ ${
803810
804811
// Handle 404 with custom page if configured (Issue #12)
805812
if (response.status === 404 && CUSTOM_404_PAGE_ID !== '') {
806-
const notFoundUrl = new URL(url);
807-
notFoundUrl.pathname = '/' + CUSTOM_404_PAGE_ID;
808-
const notFoundResponse = await fetch(notFoundUrl.toString(), {
809-
headers: PAGE_FETCH_HEADERS,
810-
});
811-
// Return custom 404 page content with 404 status
812-
response = new Response(notFoundResponse.body, {
813-
status: 404,
814-
statusText: 'Not Found',
815-
headers: notFoundResponse.headers,
816-
});
817-
response.headers.delete('Content-Security-Policy');
818-
response.headers.delete('X-Content-Security-Policy');
819-
return appendJavascript(response, SLUG_TO_PAGE, '404', url);
813+
return notFoundResponse(url);
820814
}
821815
822816
// Get current slug from page ID for canonical URL
@@ -1066,8 +1060,9 @@ ${
10661060
}
10671061
10681062
class BodyRewriter {
1069-
constructor(SLUG_TO_PAGE) {
1063+
constructor(SLUG_TO_PAGE, notFound) {
10701064
this.SLUG_TO_PAGE = SLUG_TO_PAGE;
1065+
this.notFound = notFound;
10711066
}
10721067
element(element) {
10731068
// Add custom header HTML at the top of body if configured (Issue #20)
@@ -1078,6 +1073,8 @@ ${
10781073
<script>
10791074
if (window.CONFIG) window.CONFIG.domainBaseUrl = 'https://\${MY_DOMAIN}';
10801075
const SLUG_TO_PAGE = \${JSON.stringify(this.SLUG_TO_PAGE)};
1076+
// Keep the requested URL when the custom 404 page is shown
1077+
const NOT_FOUND_PATH = \${this.notFound} ? location.pathname : null;
10811078
const PAGE_TO_SLUG = {};
10821079
const slugs = [];
10831080
const pages = [];
@@ -1172,7 +1169,7 @@ ${
11721169
}, 2000);
11731170
const replaceState = window.history.replaceState;
11741171
window.history.replaceState = function(state) {
1175-
if (arguments[1] !== 'bypass' && slugs.includes(getSlug())) return;
1172+
if (arguments[1] !== 'bypass' && (slugs.includes(getSlug()) || location.pathname === NOT_FOUND_PATH)) return;
11761173
return replaceState.apply(window.history, arguments);
11771174
};
11781175
const pushState = window.history.pushState;
@@ -1196,6 +1193,56 @@ ${
11961193
}
11971194
}
11981195
1196+
// Notion answers 200 with its app shell for any path, so decide here whether a
1197+
// page navigation points at a real page: the root, a slug, or a page ID.
1198+
function isPageNavigation(request) {
1199+
if (request.method !== 'GET' && request.method !== 'HEAD') return false;
1200+
const dest = request.headers.get('Sec-Fetch-Dest');
1201+
return dest ? dest === 'document' : (request.headers.get('Accept') || '').includes('text/html');
1202+
}
1203+
1204+
// Paths opened directly in the browser that are not pages (images, files)
1205+
const NON_PAGE_PATH = /^\\/(image|images|f|signed|api|_assets)\\//;
1206+
1207+
function isUnknownPageNavigation(request, pathname) {
1208+
if (!isPageNavigation(request) || NON_PAGE_PATH.test(pathname)) return false;
1209+
if (/[0-9a-f]{32}/.test(pathname) || /\\.[a-z0-9]+$/i.test(pathname)) return false;
1210+
let path = pathname.slice(1);
1211+
try {
1212+
path = decodeURIComponent(path);
1213+
} catch (e) {}
1214+
return !SLUG_TO_PAGE.hasOwnProperty(path) && !SLUG_TO_PAGE.hasOwnProperty(pathname.slice(1));
1215+
}
1216+
1217+
async function notFoundResponse(url) {
1218+
if (CUSTOM_404_PAGE_ID === '') {
1219+
return new Response(
1220+
\`<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Page not found</title></head>
1221+
<body style="font-family:system-ui;text-align:center;padding:60px 20px">
1222+
<h1>404</h1>
1223+
<p>This page could not be found.</p>
1224+
<p><a href="/">Go to the home page</a></p>
1225+
</body></html>\`,
1226+
{ status: 404, headers: { 'Content-Type': 'text/html;charset=UTF-8' } }
1227+
);
1228+
}
1229+
const notFoundUrl = new URL(url);
1230+
notFoundUrl.hostname = NOTION_SITE_DOMAIN;
1231+
notFoundUrl.pathname = '/' + CUSTOM_404_PAGE_ID;
1232+
const page = await fetch(notFoundUrl.toString(), {
1233+
headers: PAGE_FETCH_HEADERS,
1234+
});
1235+
// Return custom 404 page content with 404 status
1236+
const response = new Response(page.body, {
1237+
status: 404,
1238+
statusText: 'Not Found',
1239+
headers: page.headers,
1240+
});
1241+
response.headers.delete('Content-Security-Policy');
1242+
response.headers.delete('X-Content-Security-Policy');
1243+
return appendJavascript(response, SLUG_TO_PAGE, '404', url);
1244+
}
1245+
11991246
async function appendJavascript(res, SLUG_TO_PAGE, slug, url) {
12001247
const metaRewriter = new MetaRewriter(slug);
12011248
const headRewriter = new HeadRewriter(slug);
@@ -1206,7 +1253,7 @@ ${
12061253
.on('meta', metaRewriter)
12071254
.on('link', linkRewriter)
12081255
.on('head', headRewriter)
1209-
.on('body', new BodyRewriter(SLUG_TO_PAGE))
1256+
.on('body', new BodyRewriter(SLUG_TO_PAGE, res.status === 404))
12101257
.transform(res);
12111258
return applyCacheHeaders(transformed, url, contentType);
12121259
}`;

0 commit comments

Comments
 (0)