Skip to content

Commit 477032f

Browse files
fix(app-router): pass searchParams to layout generateMetadata
Layout `generateMetadata()` was always called with `undefined` for `searchParams` because `spObj` (the URLSearchParams-to-plain-object conversion) was constructed after the layout metadata loop. Hoist the `spObj` construction to before the loop so every layout receives the real query parameters — matching the behaviour already correct for the page segment. Adds a regression fixture (`layout-metadata-search`) and a test that requests `/layout-metadata-search?tab=settings` and asserts the layout's `generateMetadata` title reflects the query param value rather than the undefined-fallback default.
1 parent 7c5ca2f commit 477032f

4 files changed

Lines changed: 61 additions & 17 deletions

File tree

packages/vinext/src/entries/app-rsc-entry.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,23 @@ async function buildPageElements(route, params, routePath, opts, searchParams) {
955955
// route it to the nearest error.tsx boundary (or global-error.tsx).
956956
const layoutMods = route.layouts.filter(Boolean);
957957
958+
// Convert URLSearchParams → plain object so we can pass it to
959+
// resolveModuleMetadata (which expects Record<string, string | string[]>).
960+
// Must be built before the layout loop so layouts receive the real searchParams.
961+
// This same object is reused for pageProps.searchParams below.
962+
const spObj = {};
963+
let hasSearchParams = false;
964+
if (searchParams && searchParams.forEach) {
965+
searchParams.forEach(function(v, k) {
966+
hasSearchParams = true;
967+
if (k in spObj) {
968+
spObj[k] = Array.isArray(spObj[k]) ? spObj[k].concat(v) : [spObj[k], v];
969+
} else {
970+
spObj[k] = v;
971+
}
972+
});
973+
}
974+
958975
// Build the parent promise chain and kick off metadata resolution in one pass.
959976
// Each layout module is called exactly once. layoutMetaPromises[i] is the
960977
// promise for layout[i]'s own metadata result.
@@ -966,7 +983,7 @@ async function buildPageElements(route, params, routePath, opts, searchParams) {
966983
for (let i = 0; i < layoutMods.length; i++) {
967984
const parentForThisLayout = accumulatedMetaPromise;
968985
// Kick off this layout's metadata resolution now (concurrent with others).
969-
const metaPromise = resolveModuleMetadata(layoutMods[i], params, undefined, parentForThisLayout)
986+
const metaPromise = resolveModuleMetadata(layoutMods[i], params, spObj, parentForThisLayout)
970987
.catch((err) => { console.error("[vinext] Layout generateMetadata() failed:", err); return null; });
971988
layoutMetaPromises.push(metaPromise);
972989
// Advance accumulator: resolves to merged(layouts[0..i]) once layout[i] is done.
@@ -977,22 +994,6 @@ async function buildPageElements(route, params, routePath, opts, searchParams) {
977994
// Page's parent is the fully-accumulated layout metadata.
978995
const pageParentPromise = accumulatedMetaPromise;
979996
980-
// Convert URLSearchParams → plain object so we can pass it to
981-
// resolveModuleMetadata (which expects Record<string, string | string[]>).
982-
// This same object is reused for pageProps.searchParams below.
983-
const spObj = {};
984-
let hasSearchParams = false;
985-
if (searchParams && searchParams.forEach) {
986-
searchParams.forEach(function(v, k) {
987-
hasSearchParams = true;
988-
if (k in spObj) {
989-
spObj[k] = Array.isArray(spObj[k]) ? spObj[k].concat(v) : [spObj[k], v];
990-
} else {
991-
spObj[k] = v;
992-
}
993-
});
994-
}
995-
996997
const [layoutMetaResults, layoutVpResults, pageMeta, pageVp] = await Promise.all([
997998
Promise.all(layoutMetaPromises),
998999
Promise.all(layoutMods.map((mod) => resolveModuleViewport(mod, params).catch((err) => { console.error("[vinext] Layout generateViewport() failed:", err); return null; }))),

tests/app-router.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -877,6 +877,19 @@ describe("App Router integration", () => {
877877
expect(html).toMatch(/name="description".*content="Read about my-post"/);
878878
});
879879

880+
it("layout generateMetadata() receives searchParams from URL query string", async () => {
881+
// Regression test: layout generateMetadata() was always passed `undefined` for
882+
// searchParams. Only page generateMetadata() received the real value.
883+
const res = await fetch(`${baseUrl}/layout-metadata-search?tab=settings`);
884+
expect(res.status).toBe(200);
885+
886+
const html = await res.text();
887+
// The layout's generateMetadata reads searchParams.tab — should produce
888+
// "Layout Section: settings", not "Layout Section: home" (the fallback for
889+
// undefined searchParams).
890+
expect(html).toContain("<title>Layout Section: settings</title>");
891+
});
892+
880893
it("renders catch-all routes with multiple segments", async () => {
881894
const res = await fetch(`${baseUrl}/docs/getting-started/install`);
882895
expect(res.status).toBe(200);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Regression fixture for layout generateMetadata searchParams bug.
3+
*
4+
* Before the fix, layout generateMetadata() always received `undefined`
5+
* for searchParams — only page generateMetadata() received the real value.
6+
*/
7+
8+
export async function generateMetadata({
9+
searchParams,
10+
}: {
11+
searchParams: Promise<{ tab?: string }>;
12+
}) {
13+
const sp = await searchParams;
14+
const tab = sp?.tab ?? "home";
15+
return {
16+
title: `Layout Section: ${tab}`,
17+
};
18+
}
19+
20+
export default function LayoutMetadataSearchLayout({ children }: { children: React.ReactNode }) {
21+
return <div data-testid="layout-metadata-search-layout">{children}</div>;
22+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default function LayoutMetadataSearchPage() {
2+
return (
3+
<main data-testid="layout-metadata-search-page">
4+
<h1>Layout Metadata Search Test</h1>
5+
<p>This page tests that layout generateMetadata receives searchParams.</p>
6+
</main>
7+
);
8+
}

0 commit comments

Comments
 (0)