From 36af558a4a91da70416dddc584c60f254d5ca9a3 Mon Sep 17 00:00:00 2001 From: Jack Carter <128555021+SunsetDrifter@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:32:38 +0200 Subject: [PATCH 1/3] fix: serve props JSON for /api data requests via middleware rewrite The /api/:path* -> /ipa/:path* rewrite in next.config.mjs is applied by Vercel's routing to client-side props fetches (/_next/data//api/....json), but the data-request context is lost and the prerendered page HTML is returned instead of JSON (vercel/next.js#39669). The router then never receives pageProps, so the API sidebar stays collapsed and the tab title shows undefined until a full reload. This half of the bug only occurs on Vercel infrastructure; the dev server and next start resolve rewrites for data requests correctly, and #900 fixed only the /ipa redirect half. Middleware rewrites preserve data-request semantics, so rewrite /api/* to /ipa/* in middleware for data requests only (x-nextjs-data header). Regular page requests fall through to the existing config rewrites, and the /ipa -> /api canonical redirect is unchanged. --- src/middleware.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/middleware.js diff --git a/src/middleware.js b/src/middleware.js new file mode 100644 index 00000000..02d889d9 --- /dev/null +++ b/src/middleware.js @@ -0,0 +1,43 @@ +import { NextResponse } from 'next/server' + +// Client-side navigations fetch page props from /_next/data//api/....json. +// Vercel's routing applies the /api/:path* -> /ipa/:path* rewrite from +// next.config.mjs to those requests but loses the data-request context and +// serves the page HTML instead of the props JSON, so the router never receives +// pageProps (https://github.com/vercel/next.js/issues/39669). Middleware +// rewrites keep data-request semantics, so map /api/* to /ipa/* here for data +// requests only; regular page requests fall through to the config rewrites. +export const config = { + matcher: [ + '/api/:path*', + // Raw data-request paths, in case the runtime matches them un-normalized. + '/_next/data/:build/api.json', + '/_next/data/:build/api/:path*', + ], +} + +export function middleware(request) { + const isDataRequest = + request.headers.get('x-nextjs-data') !== null || + request.nextUrl.pathname.startsWith('/_next/data/') + if (!isDataRequest) { + return NextResponse.next() + } + + const url = request.nextUrl.clone() + if (url.pathname.startsWith('/_next/data/')) { + url.pathname = url.pathname.replace( + /^(\/_next\/data\/[^/]+)\/api(\/|\.json$)/, + '$1/ipa$2' + ) + } else if (url.pathname === '/api') { + url.pathname = '/ipa/introduction' + } else { + url.pathname = url.pathname.replace(/^\/api\//, '/ipa/') + } + + if (url.pathname === request.nextUrl.pathname) { + return NextResponse.next() + } + return NextResponse.rewrite(url) +} From 72db67e55ac8646a2d33875905bdd3646decc9e6 Mon Sep 17 00:00:00 2001 From: Jack Carter <128555021+SunsetDrifter@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:37:56 +0200 Subject: [PATCH 2/3] fix: move /api data-request rewrite into existing proxy.js Next 16 uses proxy.js and rejects builds where both middleware.js and proxy.js exist; this repo already had src/proxy.js for the /docs-static/_next asset rewrite. Fold the /api -> /ipa data-request rewrite into it and drop middleware.js. Verified the proxy intercepts: data responses now carry x-middleware-rewrite: /ipa/... and JSON bodies. --- src/middleware.js | 43 ------------------------------------------- src/proxy.js | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 45 deletions(-) delete mode 100644 src/middleware.js diff --git a/src/middleware.js b/src/middleware.js deleted file mode 100644 index 02d889d9..00000000 --- a/src/middleware.js +++ /dev/null @@ -1,43 +0,0 @@ -import { NextResponse } from 'next/server' - -// Client-side navigations fetch page props from /_next/data//api/....json. -// Vercel's routing applies the /api/:path* -> /ipa/:path* rewrite from -// next.config.mjs to those requests but loses the data-request context and -// serves the page HTML instead of the props JSON, so the router never receives -// pageProps (https://github.com/vercel/next.js/issues/39669). Middleware -// rewrites keep data-request semantics, so map /api/* to /ipa/* here for data -// requests only; regular page requests fall through to the config rewrites. -export const config = { - matcher: [ - '/api/:path*', - // Raw data-request paths, in case the runtime matches them un-normalized. - '/_next/data/:build/api.json', - '/_next/data/:build/api/:path*', - ], -} - -export function middleware(request) { - const isDataRequest = - request.headers.get('x-nextjs-data') !== null || - request.nextUrl.pathname.startsWith('/_next/data/') - if (!isDataRequest) { - return NextResponse.next() - } - - const url = request.nextUrl.clone() - if (url.pathname.startsWith('/_next/data/')) { - url.pathname = url.pathname.replace( - /^(\/_next\/data\/[^/]+)\/api(\/|\.json$)/, - '$1/ipa$2' - ) - } else if (url.pathname === '/api') { - url.pathname = '/ipa/introduction' - } else { - url.pathname = url.pathname.replace(/^\/api\//, '/ipa/') - } - - if (url.pathname === request.nextUrl.pathname) { - return NextResponse.next() - } - return NextResponse.rewrite(url) -} diff --git a/src/proxy.js b/src/proxy.js index e83805e3..57d1f305 100644 --- a/src/proxy.js +++ b/src/proxy.js @@ -1,10 +1,42 @@ import { NextResponse } from 'next/server'; +// Client-side navigations fetch page props from /_next/data//api/....json. +// Vercel's routing applies the /api/:path* -> /ipa/:path* rewrite from +// next.config.mjs to those requests but loses the data-request context and +// serves the page HTML instead of the props JSON +// (https://github.com/vercel/next.js/issues/39669), so the router never +// receives pageProps. Proxy rewrites keep data-request semantics, so map +// /api/* to /ipa/* here for data requests only; regular page requests fall +// through to the config rewrites, and the /ipa -> /api canonical redirect +// is unchanged. +function rewriteApiDataRequest(req) { + const isDataRequest = + req.headers.get('x-nextjs-data') !== null || + req.nextUrl.pathname.startsWith('/_next/data/'); + if (!isDataRequest) return null; + + const url = req.nextUrl.clone(); + if (url.pathname.startsWith('/_next/data/')) { + // Raw data-request path, in case the runtime matches it un-normalized. + url.pathname = url.pathname.replace( + /^(\/_next\/data\/[^/]+)\/api(\/|\.json$)/, + '$1/ipa$2', + ); + } else if (url.pathname === '/api') { + url.pathname = '/ipa/introduction'; + } else if (url.pathname.startsWith('/api/')) { + url.pathname = url.pathname.replace(/^\/api\//, '/ipa/'); + } + + if (url.pathname === req.nextUrl.pathname) return null; + return NextResponse.rewrite(url); +} + export function proxy(req) { if (req.nextUrl.href.includes('/docs-static/_next/')) return NextResponse.rewrite( req.nextUrl.href.replace('/docs-static/_next/', '/_next/'), ); - return null; -} \ No newline at end of file + return rewriteApiDataRequest(req); +} From 037534b69a053153f778ba942f2101a058346b05 Mon Sep 17 00:00:00 2001 From: Jack Carter <128555021+SunsetDrifter@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:40:42 +0200 Subject: [PATCH 3/3] fix: map bare api.json data requests to ipa/introduction.json The raw data-path fallback rewrote /_next/data//api.json to /_next/data//ipa.json, but there is no /ipa index page; mirror the /api -> /ipa/introduction rewrite instead. --- src/proxy.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/proxy.js b/src/proxy.js index 57d1f305..eebb7926 100644 --- a/src/proxy.js +++ b/src/proxy.js @@ -18,10 +18,14 @@ function rewriteApiDataRequest(req) { const url = req.nextUrl.clone(); if (url.pathname.startsWith('/_next/data/')) { // Raw data-request path, in case the runtime matches it un-normalized. - url.pathname = url.pathname.replace( - /^(\/_next\/data\/[^/]+)\/api(\/|\.json$)/, - '$1/ipa$2', - ); + // Bare api.json maps to ipa/introduction.json to mirror the /api -> + // /ipa/introduction rewrite; there is no /ipa index page. + url.pathname = url.pathname + .replace( + /^(\/_next\/data\/[^/]+)\/api\.json$/, + '$1/ipa/introduction.json', + ) + .replace(/^(\/_next\/data\/[^/]+)\/api\//, '$1/ipa/'); } else if (url.pathname === '/api') { url.pathname = '/ipa/introduction'; } else if (url.pathname.startsWith('/api/')) {