diff --git a/.changeset/silent-oranges-pay.md b/.changeset/silent-oranges-pay.md
new file mode 100644
index 0000000000..e1dd391960
--- /dev/null
+++ b/.changeset/silent-oranges-pay.md
@@ -0,0 +1,11 @@
+---
+"react-router": patch
+"react-router-dom": patch
+---
+
+allow using `` with external URLs
+
+```tsx
+
+
+```
diff --git a/package.json b/package.json
index 037d859131..bd5f2beedc 100644
--- a/package.json
+++ b/package.json
@@ -120,7 +120,7 @@
"none": "11 kB"
},
"packages/react-router-dom/dist/umd/react-router-dom.production.min.js": {
- "none": "16.5 kB"
+ "none": "16.7 kB"
}
}
}
diff --git a/packages/react-router-dom/__tests__/link-href-test.tsx b/packages/react-router-dom/__tests__/link-href-test.tsx
index 36a5d9ed6d..92b2e828e9 100644
--- a/packages/react-router-dom/__tests__/link-href-test.tsx
+++ b/packages/react-router-dom/__tests__/link-href-test.tsx
@@ -89,6 +89,45 @@ describe(" href", () => {
["/about", "/about"]
);
});
+
+ test(' is treated as external link', () => {
+ let renderer: TestRenderer.ReactTestRenderer;
+ TestRenderer.act(() => {
+ renderer = TestRenderer.create(
+
+
+
+ }
+ />
+
+
+
+ );
+ });
+
+ expect(renderer.root.findByType("a").props.href).toEqual(
+ "https://remix.run"
+ );
+ });
+
+ test(' is treated as external link', () => {
+ let renderer: TestRenderer.ReactTestRenderer;
+ TestRenderer.act(() => {
+ renderer = TestRenderer.create(
+
+
+
+ } />
+
+
+
+ );
+ });
+
+ expect(renderer.root.findByType("a").props.href).toEqual("//remix.run");
+ });
});
describe("in a dynamic route", () => {
diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx
index cf0fd14240..9d630a7da9 100644
--- a/packages/react-router-dom/index.tsx
+++ b/packages/react-router-dom/index.tsx
@@ -391,6 +391,11 @@ export interface LinkProps
to: To;
}
+const isBrowser =
+ typeof window !== "undefined" &&
+ typeof window.document !== "undefined" &&
+ typeof window.document.createElement !== "undefined";
+
/**
* The public API for rendering a history-aware .
*/
@@ -409,8 +414,32 @@ export const Link = React.forwardRef(
},
ref
) {
- let href = useHref(to, { relative });
- let internalOnClick = useLinkClickHandler(to, {
+ // `location` is the unaltered href we will render in the tag for absolute URLs
+ let location = typeof to === "string" ? to : createPath(to);
+ let isAbsolute =
+ /^[a-z+]+:\/\//i.test(location) || location.startsWith("//");
+
+ // Location to use in the click handler
+ let navigationLocation = location;
+ let isExternal = false;
+ if (isBrowser && isAbsolute) {
+ let currentUrl = new URL(window.location.href);
+ let targetUrl = location.startsWith("//")
+ ? new URL(currentUrl.protocol + location)
+ : new URL(location);
+ if (targetUrl.origin === currentUrl.origin) {
+ // Strip the protocol/origin for same-origin absolute URLs
+ navigationLocation =
+ targetUrl.pathname + targetUrl.search + targetUrl.hash;
+ } else {
+ isExternal = true;
+ }
+ }
+
+ // `href` is what we render in the tag for relative URLs
+ let href = useHref(navigationLocation, { relative });
+
+ let internalOnClick = useLinkClickHandler(navigationLocation, {
replace,
state,
target,
@@ -430,8 +459,8 @@ export const Link = React.forwardRef(
// eslint-disable-next-line jsx-a11y/anchor-has-content