Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/polite-fans-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-router": patch
---

Support unencoded UTF-8 routes in prerender config with `ssr` set to `false`
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@
- rtzll
- rubeonline
- ruidi-huang
- rururux
- ryanflorence
- ryanhiebert
- saengmotmi
Expand Down
80 changes: 80 additions & 0 deletions integration/vite-prerender-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2370,6 +2370,86 @@ test.describe("Prerendering", () => {
expect(requests).toEqual(["/page.data"]);
});

test("Navigates prerendered multibyte path routes", async ({ page }) => {
fixture = await createFixture({
prerender: true,
files: {
"react-router.config.ts": reactRouterConfig({
ssr: false, // turn off fog of war since we're serving with a static server
prerender: ["/", "/page", "/ページ"],
}),
"vite.config.ts": files["vite.config.ts"],
"app/root.tsx": js`
import * as React from "react";
import { Link, Outlet, Scripts } from "react-router";

export function Layout({ children }) {
return (
<html lang="en">
<head />
<body>
<nav>
<Link to="/page">Page</Link><br/>
<Link to="/ページ">ページ</Link><br/>
</nav>
{children}
<Scripts />
</body>
</html>
);
}

export default function Root({ loaderData }) {
return <Outlet />
}
`,
"app/routes/_index.tsx": js`
export default function Index() {
return <h1 data-index>Index</h1>
}
`,
"app/routes/page.tsx": js`
export function loader() {
return "PAGE DATA"
}
export default function Page({ loaderData }) {
return <h1 data-page>{loaderData}</h1>;
}
`,
"app/routes/ページ.tsx": js`
export function loader() {
return "ページ データ";
}
export default function Page({ loaderData }) {
return <h1 data-multibyte-page>{loaderData}</h1>;
}
`,
},
});
appFixture = await createAppFixture(fixture);

let encodedMultibytePath = encodeURIComponent("ページ");
let requests = captureRequests(page);
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/", true);
await page.waitForSelector("[data-index]");

await app.clickLink("/page");
await page.waitForSelector("[data-page]");
expect(await (await page.$("[data-page]"))?.innerText()).toBe(
"PAGE DATA"
);
expect(requests).toEqual(["/page.data"]);
clearRequests(requests);

await app.clickLink("/ページ");
await page.waitForSelector("[data-multibyte-page]");
expect(await (await page.$("[data-multibyte-page]"))?.innerText()).toBe(
"ページ データ"
);
expect(requests).toEqual([`/${encodedMultibytePath}.data`]);
})

test("Returns a 404 if navigating to a non-prerendered param value", async ({
page,
}) => {
Expand Down
9 changes: 6 additions & 3 deletions packages/react-router/lib/server-runtime/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,22 +172,25 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
// When runtime SSR is disabled, make our dev server behave like the deployed
// pre-rendered site would
if (!_build.ssr) {
// Decode the URL path before checking against the prerender config
let decodedPath = decodeURI(normalizedPath);

// When SSR is disabled this, file can only ever run during dev because we
// delete the server build at the end of the build
if (_build.prerender.length === 0) {
// ssr:false and no prerender config indicates "SPA Mode"
isSpaMode = true;
} else if (
!_build.prerender.includes(normalizedPath) &&
!_build.prerender.includes(normalizedPath + "/")
!_build.prerender.includes(decodedPath) &&
!_build.prerender.includes(decodedPath + "/")
) {
if (url.pathname.endsWith(".data")) {
// 404 on non-pre-rendered `.data` requests
errorHandler(
new ErrorResponseImpl(
404,
"Not Found",
`Refusing to SSR the path \`${normalizedPath}\` because \`ssr:false\` is set and the path is not included in the \`prerender\` config, so in production the path will be a 404.`
`Refusing to SSR the path \`${decodedPath}\` because \`ssr:false\` is set and the path is not included in the \`prerender\` config, so in production the path will be a 404.`
),
{
context: loadContext,
Expand Down