Files
Josh Story 05ed7c1be2 Omit undeclared children slots from app routes (#97184)
## Summary

Parallel route layouts can be composed entirely from named slots, but
loader tree construction currently synthesizes a `children` fallback
whenever any named slot exists. This makes `children` semantically
required even when no page, default, or ordinary route branch declares
it.

This adds `experimental.explicitParallelRouteChildren` and enables it by
default. When enabled, `children` is included in a layout slot set only
when the filesystem declares an ordinary route that can render at that
level. A layout by itself is only structure and does not declare a route
target. Ordinary descendants are traced until they reach a page or
default, including through deeper named slots, before they cause
`children` to be included. Setting the flag to `false` temporarily
restores the legacy implicit `children` fallback.

This flag only controls whether `children` exists in the loader tree. It
does not prune incomplete matchers; that is the separate
`experimental.strictRouteMatching` behavior in the next PR.

Named slots keep their existing default and soft navigation semantics.
The preceding PR retains the slots owned by an interception host without
treating `children` specially, so an undeclared child is no longer
needed for that behavior. A real `children` branch still uses the
retention marker when it is one of the host layout slots.

## Semantics

For example, this layout declares only named slots:

```text
app/dashboard/layout.tsx
app/dashboard/@left/page.tsx
app/dashboard/@right/page.tsx
```

With `explicitParallelRouteChildren` disabled, Next.js adds a synthetic
`children` branch whose built-in default calls `notFound()`, even though
the layout never declared or rendered it. With the default behavior
enabled, the loader tree contains only `left` and `right`, so
`/dashboard` is matched from the route targets that actually exist.

This is different from an ordinary branch whose route targets are deeper
in the tree:

```text
app/nested/layout.tsx
app/nested/@sidebar/[...slug]/page.tsx
app/nested/content/layout.tsx
app/nested/content/@left/[...slug]/page.tsx
app/nested/content/@right/[...slug]/page.tsx
```

Here `content` really is the `children` branch of `nested`. The scan
follows `content` through its layout and deeper named slots, so
`children` remains required. `/nested/content/anything` can construct
every declared slot, while `/nested/incomplete` only matches `sidebar`
and is still incomplete. The distinction is whether the ordinary
descendant eventually reaches a page or default, not whether a layout
happens to exist along the way.

The focused children detection coverage proves that a layout-only
descendant does not synthesize `children`, while an ordinary branch
whose route targets live inside deeper named slots still does. The
limitation coverage also proves that named-only trees render pages, CSS,
metadata, and regular error boundaries. It intentionally asserts the
current broken behavior for HTTP access fallbacks and metadata or
viewport failures so those expectations can be flipped when renderer
ownership no longer depends on `children`.

## Verification

- `pnpm build-all`
- Turbopack and webpack development and production coverage for
`explicit-parallel-route-children-detection`
- Turbopack and webpack production coverage for
`interception-dynamic-segment`, `parallel-routes-layouts`, and
`explicit-parallel-route-children-legacy`
- The same existing production coverage with Cache Components enabled
- Turbopack and webpack production coverage for the documented
named-only limitations

<!-- NEXT_JS_LLM -->
2026-08-27 12:22:31 -07:00
..