Skip to content

Commit 8975de9

Browse files
Hotfix: absolute URLs on server router (#10112)
Co-authored-by: Matt Brophy <matt@brophy.org>
1 parent 3c6fb46 commit 8975de9

File tree

6 files changed

+70
-17
lines changed

6 files changed

+70
-17
lines changed

.changeset/lovely-cheetahs-sip.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router-dom": patch
3+
---
4+
5+
Fix SSR of absolute Link urls

contributors.yml

+1
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@
163163
- tanayv
164164
- theostavrides
165165
- thisiskartik
166+
- thomasverleye
166167
- ThornWu
167168
- timdorr
168169
- TkDodo

packages/react-router-dom/__tests__/data-static-router-test.tsx

+42
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
15
import * as React from "react";
26
import * as ReactDOMServer from "react-dom/server";
37
import type { StaticHandlerContext } from "@remix-run/router";
@@ -642,6 +646,44 @@ describe("A <StaticRouterProvider>", () => {
642646
`);
643647
});
644648

649+
it("renders absolute links correctly", async () => {
650+
let routes = [
651+
{
652+
path: "/",
653+
element: (
654+
<>
655+
<Link to="/the/path">relative path</Link>
656+
<Link to="http://localhost/the/path">absolute same-origin url</Link>
657+
<Link to="https://remix.run">absolute different-origin url</Link>
658+
<Link to="mailto:foo@baz.com">absolute mailto: url</Link>
659+
</>
660+
),
661+
},
662+
];
663+
let { query } = createStaticHandler(routes);
664+
665+
let context = (await query(
666+
new Request("http://localhost/", {
667+
signal: new AbortController().signal,
668+
})
669+
)) as StaticHandlerContext;
670+
671+
let html = ReactDOMServer.renderToStaticMarkup(
672+
<React.StrictMode>
673+
<StaticRouterProvider
674+
router={createStaticRouter(routes, context)}
675+
context={context}
676+
/>
677+
</React.StrictMode>
678+
);
679+
expect(html).toMatch(
680+
'<a href="/the/path">relative path</a>' +
681+
'<a href="http://localhost/the/path">absolute same-origin url</a>' +
682+
'<a href="https://remix.run">absolute different-origin url</a>' +
683+
'<a href="mailto:foo@baz.com">absolute mailto: url</a>'
684+
);
685+
});
686+
645687
describe("boundary tracking", () => {
646688
it("tracks the deepest boundary during render", async () => {
647689
let routes = [

packages/react-router-dom/__tests__/polyfills/SubmitEvent.submitter.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// Polyfill jsdom SubmitEvent.submitter, until https://github.com/jsdom/jsdom/pull/3481 is merged
22
if (
3-
typeof SubmitEvent === "undefined" ||
4-
!SubmitEvent.prototype.hasOwnProperty("submitter")
3+
typeof window !== "undefined" &&
4+
(typeof SubmitEvent === "undefined" ||
5+
!SubmitEvent.prototype.hasOwnProperty("submitter"))
56
) {
67
const setImmediate = (fn, ...args) => global.setTimeout(fn, 0, ...args);
78

packages/react-router-dom/__tests__/setup.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {
22
TextEncoder as NodeTextEncoder,
33
TextDecoder as NodeTextDecoder,
44
} from "util";
5-
import { fetch, Request, Response } from "@remix-run/web-fetch";
5+
import { fetch, Request, Response, Headers } from "@remix-run/web-fetch";
66
import { AbortController as NodeAbortController } from "abort-controller";
77

88
import "./polyfills/SubmitEvent.submitter";
@@ -22,6 +22,7 @@ if (!globalThis.fetch) {
2222
// web-std/fetch Response does not currently implement Response.error()
2323
// @ts-expect-error
2424
globalThis.Response = Response;
25+
globalThis.Headers = Headers;
2526
}
2627

2728
if (!globalThis.AbortController) {

packages/react-router-dom/index.tsx

+17-14
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,8 @@ const isBrowser =
400400
typeof window.document !== "undefined" &&
401401
typeof window.document.createElement !== "undefined";
402402

403+
const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
404+
403405
/**
404406
* The public API for rendering a history-aware <a>.
405407
*/
@@ -422,21 +424,22 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
422424
let absoluteHref;
423425
let isExternal = false;
424426

425-
if (
426-
isBrowser &&
427-
typeof to === "string" &&
428-
/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(to)
429-
) {
427+
if (typeof to === "string" && ABSOLUTE_URL_REGEX.test(to)) {
428+
// Render the absolute href server- and client-side
430429
absoluteHref = to;
431-
let currentUrl = new URL(window.location.href);
432-
let targetUrl = to.startsWith("//")
433-
? new URL(currentUrl.protocol + to)
434-
: new URL(to);
435-
if (targetUrl.origin === currentUrl.origin) {
436-
// Strip the protocol/origin for same-origin absolute URLs
437-
to = targetUrl.pathname + targetUrl.search + targetUrl.hash;
438-
} else {
439-
isExternal = true;
430+
431+
// Only check for external origins client-side
432+
if (isBrowser) {
433+
let currentUrl = new URL(window.location.href);
434+
let targetUrl = to.startsWith("//")
435+
? new URL(currentUrl.protocol + to)
436+
: new URL(to);
437+
if (targetUrl.origin === currentUrl.origin) {
438+
// Strip the protocol/origin for same-origin absolute URLs
439+
to = targetUrl.pathname + targetUrl.search + targetUrl.hash;
440+
} else {
441+
isExternal = true;
442+
}
440443
}
441444
}
442445

0 commit comments

Comments
 (0)