diff --git a/packages/vinext/src/entries/pages-server-entry.ts b/packages/vinext/src/entries/pages-server-entry.ts
index 74f897259..267bfd23a 100644
--- a/packages/vinext/src/entries/pages-server-entry.ts
+++ b/packages/vinext/src/entries/pages-server-entry.ts
@@ -312,7 +312,14 @@ export function matchPageRoute(url, request) {
}
function parseQuery(url) {
- const qs = url.split("?")[1];
+ // Per RFC 3986 only the first "?" separates path from query, so additional
+ // "?" chars belong to the query string (e.g. /linker?href=/about?hello=world
+ // has query "href=/about?hello=world"). split("?")[1] would drop everything
+ // after the second "?" and strip embedded query strings from values.
+ const queryIndex = url.indexOf("?");
+ if (queryIndex === -1) return {};
+ const hashIndex = url.indexOf("#", queryIndex + 1);
+ const qs = hashIndex === -1 ? url.slice(queryIndex + 1) : url.slice(queryIndex + 1, hashIndex);
if (!qs) return {};
const p = new URLSearchParams(qs);
const q = {};
diff --git a/packages/vinext/src/utils/query.ts b/packages/vinext/src/utils/query.ts
index f82399888..ea859694e 100644
--- a/packages/vinext/src/utils/query.ts
+++ b/packages/vinext/src/utils/query.ts
@@ -55,9 +55,18 @@ export function mergeRouteParamsIntoQuery(
/**
* Parse a URL's query string into a Record, with multi-value keys promoted to arrays.
+ *
+ * Per RFC 3986 only the first `?` separates path from query; any further `?`
+ * characters are part of the query string itself (e.g. `/linker?href=/about?hello=world`
+ * has the query `href=/about?hello=world`). Using `indexOf("?")` instead of
+ * `split("?")[1]` preserves the rest of the query so values like ``
+ * targets keep their own query strings intact.
*/
export function parseQueryString(url: string): Record {
- const qs = url.split("?")[1];
+ const queryIndex = url.indexOf("?");
+ if (queryIndex === -1) return {};
+ const hashIndex = url.indexOf("#", queryIndex + 1);
+ const qs = hashIndex === -1 ? url.slice(queryIndex + 1) : url.slice(queryIndex + 1, hashIndex);
if (!qs) return {};
const params = new URLSearchParams(qs);
const query: Record = {};
diff --git a/tests/fixtures/pages-basic/pages/linker.tsx b/tests/fixtures/pages-basic/pages/linker.tsx
new file mode 100644
index 000000000..ee1275f1c
--- /dev/null
+++ b/tests/fixtures/pages-basic/pages/linker.tsx
@@ -0,0 +1,32 @@
+import Link from "next/link";
+
+// Regression fixture for issue #1471: when `?href=/path?query=value` is passed
+// through the URL, the Pages Router must preserve the embedded `?query=value`
+// portion when rendering `` and when calling `router.push()`.
+// Ported from Next.js: test/e2e/trailing-slashes/pages/linker.js
+//
+// `getServerSideProps` receives the parsed `query.href` value, which by RFC
+// 3986 contains everything after the first `?`. The rendered `` must
+// then output an `` that matches the original target verbatim.
+
+interface LinkerProps {
+ href: string;
+}
+
+export default function Linker({ href }: LinkerProps) {
+ return (
+