Shipping & Infra5 min read

One line in a parent layout was 404'ing every dated page

In Next.js, dynamicParams inherits downward. If the parent says false, the child saying true loses. Dated URLs my sitemap published every day were dying three days later.

#nextjs#app-router#seo#routing#debugging
Diagram: parent segment dynamicParams overriding the child setting
A diagram summarizing the post.

While going through traffic logs I found something odd: URLs that people had actually landed on, which now returned 404.

/ko/date/2026-08-29   404
/ko/date/2026-09-01   404
/ko/date/2026-09-06   200
/ko/date/2026-09-08   200

Same route, only the date differs. Some alive, some dead.

Do you have routes that "worked until yesterday"?

The alive window matched exactly

I probed the boundaries. The living range was September 6 through September 21. That day was September 8.

So: yesterday through 13 days out. And this app's sitemap generator published exactly that range.

// Rolling window: yesterday .. +13d. The sitemap publishes new URLs daily -> freshness signal.
for (let d = -2; d <= 13; d++) { ... }

The intent was fine — put fresh URLs in the sitemap every day to signal recency. The problem is that those indexed URLs 404'd three days later. From a crawler's point of view, this was manufacturing new 404s daily.

The code said it should work

The route was written to accept dates outside the window.

export const dynamicParams = true;   // generate outside the window on demand
 
export function generateStaticParams() {
  return rollingDates().map(...)     // prerender only inside it
}

Validation was generous too: any year from 2020 to 2035 passes, and the calendar calculation is a pure function that returns a value for any date. There was no reason for notFound() to fire.

I suspected the deployment, and I was wrong

My first hypothesis was "the deployed build is older than the source." Source allows every date, production blocks outside the window — a natural guess.

I checked the deployment history. The newest one was five hours old. Hypothesis rejected.

So I ran a production build locally and sent the same requests.

2026-09-01  404
2026-09-05  404
2026-09-06  200

Identical. Not a deployment problem — a source problem. Without that one step I would have kept digging on the deploy side. The dev server does not show this bug at all.

The culprit was the parent layout

I grepped every notFound() call site. All the checks inside the route pass, yet it 404s. That leaves the segment above it.

// app/[locale]/layout.tsx
// Prerender supported locales only — unsupported paths 404 instead of 500.
export const dynamicParams = false;

There it is. dynamicParams is segment config, and it inherits downward. If the parent is false, the child's dynamicParams = true is ignored. The child cannot win.

The intent was to keep dead /ja links from throwing 500s. That intent was legitimate; the blast radius was every route beneath it.

Where would you fix it?

Work around it in the child? Move the date route outside [locale], or widen the window to a year?

I deleted the line in the parent. After deleting it, I checked what actually blocks unsupported locales, and it was already handled a few lines below in the same layout:

const parsed = localeSchema.safeParse(raw);
if (!parsed.success) notFound();

/xx and /zz/date/2026-09-08 still 404. Still not 500. Which means dynamicParams = false was never needed. There were two guards for the same goal, and one of them carried an unintended side effect.

After deploying, past dates return 200 in production and unsupported locales still 404.

A test holds the fix in place

Bugs like this come back quietly. Someone says "I don't think unsupported locales are blocked" and puts the line back.

it('parent layout does not set dynamicParams to false', () => {
  expect(localeLayout).not.toMatch(/dynamicParams\s*=\s*false/);
});
 
it('notFound() is what blocks unsupported locales', () => {
  expect(localeLayout).toMatch(/notFound\(\)/);
});

The second test matters more than it looks. With only the first, the next person asking "then how are locales blocked?" has no answer and reverts the line. Pin the reason for the deletion, not just the deletion.

I also put false back to confirm the test actually fails. A guard that does not fail is not a guard.

Three checks

  1. Are you setting route segment config in both parent and child? dynamicParams, revalidate, dynamic all inherit. Do not assume the child wins by declaring.
  2. Are you only checking on the dev server? This bug appears only under next build && next start.
  3. Are you publishing date or time based URLs? If a URL's lifetime is shorter than its indexing lifetime, you are producing 404s on a schedule.

The honest part

The scale was small: eight visits hit this 404 over 28 days. On that number alone, fixing it is marginal.

The value was elsewhere. This app publishes new dated URLs to its sitemap every single day to manufacture a freshness signal, and those URLs kept dying. Eight visits is the symptom, not the size.

Also, 31 apps in my repo share this pattern. When I checked, only this one was actually harmed: where the child segment is a finite set, generateStaticParams prerenders all of it, so the same setting acts as a guard against unbounded generation. The identical config is a shield on one route and a landmine on another.

Open your parent layout. Do you know what the config in there is doing to the routes below it?


Related: One Next.js line that almost deindexed 5 apps · Those Search Console 404s were noise

Related