diff --git a/packages/docusaurus/src/commands/serve/__tests__/requestHandler.test.ts b/packages/docusaurus/src/commands/serve/__tests__/requestHandler.test.ts index 0c8b5056066a..fba669d755d6 100644 --- a/packages/docusaurus/src/commands/serve/__tests__/requestHandler.test.ts +++ b/packages/docusaurus/src/commands/serve/__tests__/requestHandler.test.ts @@ -155,9 +155,9 @@ describe('createServeRequestHandler', () => { /missing 404 404.html /missing/ 404 404.html /missing.html 301 /missing → 404 404.html - /redirect-folder.html 301 /redirect-folder → 404 404.html + /redirect-folder.html 200 redirect-folder.html/index.html /redirect-folder.html/ 200 redirect-folder.html/index.html - /redirect-file.html 301 /redirect-file → 404 404.html + /redirect-file.html 200 redirect-file.html.html /redirect-file.html/ 200 redirect-file.html.html /file?query=1 200 file.html /file/?query=1 200 file.html @@ -219,9 +219,9 @@ describe('createServeRequestHandler', () => { /missing 301 /missing/ → 404 404.html /missing/ 404 404.html /missing.html 301 /missing → 301 /missing/ → 404 404.html - /redirect-folder.html 301 /redirect-folder → 301 /redirect-folder/ → 404 404.html + /redirect-folder.html 301 /redirect-folder.html/ → 200 redirect-folder.html/index.html /redirect-folder.html/ 200 redirect-folder.html/index.html - /redirect-file.html 301 /redirect-file → 301 /redirect-file/ → 404 404.html + /redirect-file.html 301 /redirect-file.html/ → 200 redirect-file.html.html /redirect-file.html/ 200 redirect-file.html.html /file?query=1 301 /file/?query=1 → 200 file.html /file/?query=1 200 file.html @@ -283,10 +283,10 @@ describe('createServeRequestHandler', () => { /missing 404 404.html /missing/ 301 /missing → 404 404.html /missing.html 301 /missing → 404 404.html - /redirect-folder.html 301 /redirect-folder → 404 404.html - /redirect-folder.html/ 301 /redirect-folder.html → 301 /redirect-folder → 404 404.html - /redirect-file.html 301 /redirect-file → 404 404.html - /redirect-file.html/ 301 /redirect-file.html → 301 /redirect-file → 404 404.html + /redirect-folder.html 200 redirect-folder.html/index.html + /redirect-folder.html/ 301 /redirect-folder.html → 200 redirect-folder.html/index.html + /redirect-file.html 200 redirect-file.html.html + /redirect-file.html/ 301 /redirect-file.html → 200 redirect-file.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 @@ -349,9 +349,9 @@ describe('createServeRequestHandler', () => { /base/missing 404 404.html /base/missing/ 404 404.html /base/missing.html 301 /base/missing → 404 404.html - /base/redirect-folder.html 301 /base/redirect-folder → 404 404.html + /base/redirect-folder.html 200 redirect-folder.html/index.html /base/redirect-folder.html/ 200 redirect-folder.html/index.html - /base/redirect-file.html 301 /base/redirect-file → 404 404.html + /base/redirect-file.html 200 redirect-file.html.html /base/redirect-file.html/ 200 redirect-file.html.html /base/file?query=1 200 file.html /base/file/?query=1 200 file.html @@ -413,9 +413,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/redirect-folder.html 301 /base/redirect-folder → 301 /base/redirect-folder/ → 404 404.html + /base/redirect-folder.html 301 /base/redirect-folder.html/ → 200 redirect-folder.html/index.html /base/redirect-folder.html/ 200 redirect-folder.html/index.html - /base/redirect-file.html 301 /base/redirect-file → 301 /base/redirect-file/ → 404 404.html + /base/redirect-file.html 301 /base/redirect-file.html/ → 200 redirect-file.html.html /base/redirect-file.html/ 200 redirect-file.html.html /base/file?query=1 301 /base/file/?query=1 → 200 file.html /base/file/?query=1 200 file.html @@ -477,10 +477,10 @@ 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/redirect-folder.html 301 /base/redirect-folder → 404 404.html - /base/redirect-folder.html/ 301 /base/redirect-folder.html → 301 /base/redirect-folder → 404 404.html - /base/redirect-file.html 301 /base/redirect-file → 404 404.html - /base/redirect-file.html/ 301 /base/redirect-file.html → 301 /base/redirect-file → 404 404.html + /base/redirect-folder.html 200 redirect-folder.html/index.html + /base/redirect-folder.html/ 301 /base/redirect-folder.html → 200 redirect-folder.html/index.html + /base/redirect-file.html 200 redirect-file.html.html + /base/redirect-file.html/ 301 /base/redirect-file.html → 200 redirect-file.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 @@ -837,27 +837,25 @@ describe('createServeRequestHandler', () => { ); }); - // TODO fix: "200 redirect-folder.html/index.html" // plugin-client-redirects writes it for redirects from /redirect-folder.html - it('serves no client redirect folder of .html paths', async () => { + it('serves the client redirect folder of .html paths', async () => { await expect( serveRequest( {baseUrl: '/base/', trailingSlash: undefined}, '/base/redirect-folder.html', ), - ).resolves.toBe('301 /base/redirect-folder → 404 404.html'); + ).resolves.toBe('200 redirect-folder.html/index.html'); }); - // TODO fix: "200 redirect-file.html.html" // plugin-client-redirects writes it for redirects from /redirect-file.html, // with trailingSlash false - 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: false}, '/base/redirect-file.html', ), - ).resolves.toBe('301 /base/redirect-file → 404 404.html'); + ).resolves.toBe('200 redirect-file.html.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..24f238478802 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,27 @@ 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 +// See https://github.com/meta/docusaurus/pull/12920 +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]!; @@ -231,7 +258,7 @@ async function isHtmlFileRequest( if (!filePath?.endsWith('.html')) { return false; } - // Not a folder, like the client redirect file "legacy.html/index.html" + // Not a folder, like the client redirect file "old-page.html/index.html" const stats = await fs.stat(filePath).catch(() => undefined); return stats?.isFile() ?? false; }