Route Groups
A folder name in parentheses is a route group: it organizes pages without being part of their URL.
app/
├─ (marketing)/
│ ├─ layout.jsx # marketing shell: big header, footer
│ ├─ page.jsx # /
│ └─ pricing/page.jsx # /pricing
└─ (app)/
├─ layout.jsx # app shell: sidebar
├─ _middleware.js # sign-in check for this group only
├─ dashboard/page.jsx # /dashboard
└─ settings/page.jsx # /settingsRoute groups ship in the next releases of @opentf/afw, @opentf/afw-build and @opentf/afw-docs.
What a group can hold
Everything a folder can hold, applied to the pages inside it:
| File in the group | Applies to |
|---|---|
layout.jsx | The group's pages. /pricing and /dashboard sit side by side in the URL but get different shells |
_middleware.js | The group's pages, their loader data, API routes and server functions; nothing outside the group |
layout.loader.js | The group's pages, through its layout (layout loaders) |
loading.jsx, error.jsx | The group's pages (loading and errors) |
route.js folders | API routes at URLs without the group |
*.server.js | Server functions whose endpoints have no group in their URL |
Rules
Never in the URL.
/(marketing)/pricingis not a URL; the page answers/pricing.One page per URL. Two groups holding the same path (
(a)/aboutand(b)/about) is a build error that names both files. So is a page in one group and an API route in another at the same URL.Groups nest.
(app)/(admin)/users/page.jsxanswers/users, inside both groups' layouts and middleware.Ranking ignores groups. A route ranks by its URL, so a group adds no specificity.
Groups in docs sidebars. With
@opentf/afw-docs, a group folder is a sidebar heading over its pages (labelled from_meta, e.g."(guides)": "Guides"), and their links have no group in them.
Middleware in a group
// app/(app)/_middleware.js
export default async function requireUser(request, context, next) {
if (!context.locals.user) return Response.redirect(new URL("/login", request.url), 302);
return next();
}Covers exactly the group. It runs for the group's pages (and anything nested under them), their
__data.json, its API routes and its server functions./pricingin(marketing)is not affected, even though both groups sit at the root URL.Composes as usual.
app/_middleware.jsstill runs first, then the group's.
Related
Dynamic Routes: optional segments.