From cd9ec46ba44b2d3a02d215d18bbf6ebf40fb305e Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 6 Nov 2026 23:01:03 +0100 Subject: [PATCH 1/2] fix(core): `docusaurus serve` serves the client redirect files of .html paths Co-Authored-By: Claude Opus 5.5 --- .../serve/__tests__/requestHandler.test.ts | 33 +++++++++---------- .../src/commands/serve/requestHandler.ts | 30 +++++++++++++++-- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/packages/docusaurus/src/commands/serve/__tests__/requestHandler.test.ts b/packages/docusaurus/src/commands/serve/__tests__/requestHandler.test.ts index a85298d2b5fc..42be5d0e8394 100644 --- a/packages/docusaurus/src/commands/serve/__tests__/requestHandler.test.ts +++ b/packages/docusaurus/src/commands/serve/__tests__/requestHandler.test.ts @@ -154,9 +154,9 @@ describe('createServeRequestHandler', () => { /missing 404 404.html /missing/ 404 404.html /missing.html 301 /missing → 404 404.html - /legacy.html 301 /legacy → 404 404.html + /legacy.html 200 legacy.html/index.html /legacy.html/ 200 legacy.html/index.html - /legacy-false.html 301 /legacy-false → 404 404.html + /legacy-false.html 200 legacy-false.html.html /file?query=1 200 file.html /file/?query=1 200 file.html /file.html?query=1 200 file.html @@ -217,9 +217,9 @@ describe('createServeRequestHandler', () => { /missing 301 /missing/ → 404 404.html /missing/ 404 404.html /missing.html 301 /missing → 301 /missing/ → 404 404.html - /legacy.html 301 /legacy → 301 /legacy/ → 404 404.html + /legacy.html 301 /legacy.html/ → 200 legacy.html/index.html /legacy.html/ 200 legacy.html/index.html - /legacy-false.html 301 /legacy-false → 301 /legacy-false/ → 404 404.html + /legacy-false.html 301 /legacy-false.html/ → 200 legacy-false.html.html /file?query=1 301 /file/?query=1 → 200 file.html /file/?query=1 200 file.html /file.html?query=1 200 file.html @@ -280,9 +280,9 @@ describe('createServeRequestHandler', () => { /missing 404 404.html /missing/ 301 /missing → 404 404.html /missing.html 301 /missing → 404 404.html - /legacy.html 301 /legacy → 404 404.html - /legacy.html/ 301 /legacy.html → 301 /legacy → 404 404.html - /legacy-false.html 301 /legacy-false → 404 404.html + /legacy.html 200 legacy.html/index.html + /legacy.html/ 301 /legacy.html → 200 legacy.html/index.html + /legacy-false.html 200 legacy-false.html.html /file?query=1 200 file.html /file/?query=1 301 /file?query=1 → 200 file.html /file.html?query=1 200 file.html @@ -345,9 +345,9 @@ describe('createServeRequestHandler', () => { /base/missing 404 404.html /base/missing/ 404 404.html /base/missing.html 301 /base/missing → 404 404.html - /base/legacy.html 301 /base/legacy → 404 404.html + /base/legacy.html 200 legacy.html/index.html /base/legacy.html/ 200 legacy.html/index.html - /base/legacy-false.html 301 /base/legacy-false → 404 404.html + /base/legacy-false.html 200 legacy-false.html.html /base/file?query=1 200 file.html /base/file/?query=1 200 file.html /base/file.html?query=1 200 file.html @@ -408,9 +408,9 @@ describe('createServeRequestHandler', () => { /base/missing 301 /base/missing/ → 404 404.html /base/missing/ 404 404.html /base/missing.html 301 /base/missing → 301 /base/missing/ → 404 404.html - /base/legacy.html 301 /base/legacy → 301 /base/legacy/ → 404 404.html + /base/legacy.html 301 /base/legacy.html/ → 200 legacy.html/index.html /base/legacy.html/ 200 legacy.html/index.html - /base/legacy-false.html 301 /base/legacy-false → 301 /base/legacy-false/ → 404 404.html + /base/legacy-false.html 301 /base/legacy-false.html/ → 200 legacy-false.html.html /base/file?query=1 301 /base/file/?query=1 → 200 file.html /base/file/?query=1 200 file.html /base/file.html?query=1 200 file.html @@ -471,9 +471,9 @@ describe('createServeRequestHandler', () => { /base/missing 404 404.html /base/missing/ 301 /base/missing → 404 404.html /base/missing.html 301 /base/missing → 404 404.html - /base/legacy.html 301 /base/legacy → 404 404.html - /base/legacy.html/ 301 /base/legacy.html → 301 /base/legacy → 404 404.html - /base/legacy-false.html 301 /base/legacy-false → 404 404.html + /base/legacy.html 200 legacy.html/index.html + /base/legacy.html/ 301 /base/legacy.html → 200 legacy.html/index.html + /base/legacy-false.html 200 legacy-false.html.html /base/file?query=1 200 file.html /base/file/?query=1 301 /base/file?query=1 → 200 file.html /base/file.html?query=1 200 file.html @@ -830,15 +830,14 @@ describe('createServeRequestHandler', () => { ); }); - // TODO fix: serve the client redirect file, "200 legacy.html/index.html" // plugin-client-redirects writes it for redirects from /legacy.html - it('serves no client redirect file of .html paths', async () => { + it('serves the client redirect file of .html paths', async () => { await expect( serveRequest( {baseUrl: '/base/', trailingSlash: undefined}, '/base/legacy.html', ), - ).resolves.toBe('301 /base/legacy → 404 404.html'); + ).resolves.toBe('200 legacy.html/index.html'); }); // Like the malformed percent-encodings diff --git a/packages/docusaurus/src/commands/serve/requestHandler.ts b/packages/docusaurus/src/commands/serve/requestHandler.ts index 92c7426a900f..a7e82d12fbb4 100644 --- a/packages/docusaurus/src/commands/serve/requestHandler.ts +++ b/packages/docusaurus/src/commands/serve/requestHandler.ts @@ -74,6 +74,9 @@ export function createServeRequestHandler({ // Requests without url were redirected to the baseUrl req.url = req.url!.replace(baseUrl, '/'); + // serve-handler can't serve these pages: cleanUrls would redirect them + req.url = (await getHtmlPathPageFileUrl(outDir, req.url)) ?? req.url; + await serveHandler(req, res, { // cleanUrls serves /file with file.html, but also redirects /file.html // to /file, even when the file exists, and drops the baseUrl @@ -139,12 +142,15 @@ async function getRedirectLocation( } // Static hosts serve the existing .html files, without clean URL redirects - if (await isHtmlFileRequest(outDir, url.slice(baseUrl.length - 1))) { + const outDirUrl = url.slice(baseUrl.length - 1); + if (await isHtmlFileRequest(outDir, outDirUrl)) { return undefined; } + const isHtmlPathPage = + (await getHtmlPathPageFileUrl(outDir, outDirUrl)) !== undefined; const cleanPathname = getCleanPathname(pathname, baseUrl); - if (pathname !== cleanPathname) { + if (!isHtmlPathPage && pathname !== cleanPathname) { return serializeURLPath({pathname: cleanPathname, search}); } @@ -197,6 +203,26 @@ function hasNullByte(url: string): boolean { return pathname.includes('%00'); } +// The page of a .html path that isn't a file, like the client redirect files +// "old-page.html/index.html", or "old-page.html.html" with trailingSlash false +async function getHtmlPathPageFileUrl( + outDir: string, + url: string, +): Promise { + const pathname = url.split(/[?#]/)[0]!; + if (!pathname.endsWith('.html')) { + return undefined; + } + const queryAndHash = url.slice(pathname.length); + const pageFileUrls = [`${pathname}/index.html`, `${pathname}.html`]; + for (const pageFileUrl of pageFileUrls) { + if (await isHtmlFileRequest(outDir, pageFileUrl)) { + return pageFileUrl + queryAndHash; + } + } + return undefined; +} + // The decoded paths that serve-handler's cleanUrls redirects function hasCleanUrlSuffix(url: string): boolean { const pathname = url.split(/[?#]/)[0]!; From b8195e7394bc94ae36258545bc94cab36cd334d8 Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 6 Nov 2026 23:01:24 +0100 Subject: [PATCH 2/2] link the PR in the comment Co-Authored-By: Claude Opus 5.5 --- packages/docusaurus/src/commands/serve/requestHandler.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/docusaurus/src/commands/serve/requestHandler.ts b/packages/docusaurus/src/commands/serve/requestHandler.ts index a7e82d12fbb4..760f7a15929b 100644 --- a/packages/docusaurus/src/commands/serve/requestHandler.ts +++ b/packages/docusaurus/src/commands/serve/requestHandler.ts @@ -205,6 +205,7 @@ function hasNullByte(url: string): boolean { // The page of a .html path that isn't a file, like the client redirect files // "old-page.html/index.html", or "old-page.html.html" with trailingSlash false +// See https://github.com/meta/docusaurus/pull/12920 async function getHtmlPathPageFileUrl( outDir: string, url: string,