Skip to content

Routing

A page with no route export derives its URL and name from its location beneath src/web/.

| File | Derived path | Derived name | | -------------------------------------- | --------------- | ------------- | | src/web/index.page.tsx | / | index | | src/web/about.page.tsx | /about | about | | src/web/blog/index.page.tsx | /blog | blog | | src/web/products/[id].page.tsx | /products/:id | products.id | | src/web/(marketing)/pricing.page.tsx | /pricing | pricing |

The rules are:

  • index.page.tsx claims its directory. It is the only special ordinary-page filename; home.page.tsx serves /home, not /.
  • Every directory and non-index filename contributes a path segment.
  • A plain [id] directory or filename becomes a whole-segment :id parameter.
  • A (group) directory contributes nothing to the path or route name; it only organizes files. Bracket syntax inside a group name is a boot-time error because a group can never contribute a dynamic segment. Write (marketing)/[id]/page.page.tsx, not (marketing[id])/page.page.tsx.
  • The route name is the dotted sequence of contributing segments.

Export a bare literal path or a literal object:

src/web/products/[id].page.tsx
export const route = {
path: "/catalog/:id",
name: "products.show",
} as const;
export default function ProductPage() {
return <main>Product</main>;
}

An explicit route wins over filesystem derivation for both path and name. A bare string is also valid:

export const route = "/products";

Warlock reads this export without executing application code. Variables, function calls, computed keys, spreads, and indirect export { route } forms are refused.

Two pages that resolve to the same effective path or name fail discovery and name both files, instead of leaving one page silently unreachable.

Page documents and their x-warlock-data representations are no-store by default. Opt a public page into shared caching on its route:

src/web/products/index.page.tsx
export const route = {
path: "/products",
name: "products.index",
cache: { public: true, maxAge: 60 },
} as const;

maxAge is seconds. Both keys are required: cache: { public: true } fails at boot with InvalidPageCacheOptInError because Warlock will not guess a freshness window. Remove cache entirely to keep the safe default.

The declaration is an opt-in, not permission to cache every response:

| Request or response state | Final Cache-Control | | --------------------------------------------------------------------- | -------------------------- | | No cache declaration | no-store | | Opted in, provably unauthenticated, no cookie written | public, max-age=<maxAge> | | Authenticated state was used, or the response sets or clears a cookie | private, no-store | | Warlock cannot determine whether authenticated state was used | no-store |

The final decision is applied once after loaders finish, to both the document and data response. A loader cannot bypass it by setting Cache-Control manually.

Page routes support static segments, whole-segment parameters, the exact * wildcard, and a terminal wildcard such as /docs/*.

They do not support regex parameters, optional parameters, multiple parameters inside one segment, parameters mixed with text, doubled or trailing slashes, or a non-terminal wildcard. Write two pages for an optional segment and validate constrained values in the loader.

An unsupported declared path fails at boot with PageRoutePathNotSupportedError; Warlock does not normalize it or register it literally. Rejected examples include /users/:id?, /users/:id(\\d+), /near/:lat-:lng, /a//b, and /users/.

Use an explicit terminal wildcard instead:

src/web/docs/catch-all.page.tsx
export const route = {
path: "/docs/*",
name: "docs.catchAll",
} as const;

Every discovered page must default-export a runtime component. A page with only named exports is a hard build failure naming the file. A type-only default export does not count because it disappears at runtime.

Every URL segment is written either in the page route/filesystem path or in a layout prefix above it. A prefix replaces its own directory segment for a derived page route and prepends an explicit page path.

See layout prefix and middleware composition →