Skip to content

Pages and Layouts

Houdini uses a filesystem-based router. Routes live in src/routes, and the path of a file maps directly to the URL it handles.

Pages and Layouts

A route is defined by a +page.tsx+page.jsx file. The default export is the component that renders at that URL:

src/routes/+page.tsx → /
src/routes/shows/+page.tsx → /shows
src/routes/show/[id]/+page.tsx → /show/:id
src/routes/+page.jsx → /
src/routes/shows/+page.jsx → /shows
src/routes/show/[id]/+page.jsx → /show/:id
src/routes/+page.tsx
export default () => <div>Hello Houdini!</div>
src/routes/+page.jsx
export default () => <div>Hello Houdini!</div>

Layouts wrap routes. A +layout.tsx+layout.jsx applies to every page in its directory and all subdirectories, and receives the nested content as children:

src/routes/+layout.tsx
export default ({ children }) => (
<>
<NavBar />
{children}
</>
)
src/routes/+layout.jsx
export default ({ children }) => (<>
<NavBar />
{children}
</>)

A page at /shows/1 will render inside any layout files found at src/routes/ and src/routes/shows/, nested outermost-first.

Route Groups

Routes can be grouped into a shared layout without affecting the URL structure by wrapping the directory name in parentheses:

src/routes/(app)/shows/+page.tsx → /shows
src/routes/(app)/show/+page.tsx → /show
src/routes/(auth)/login/+page.tsx → /login
src/routes/(app)/shows/+page.jsx → /shows
src/routes/(app)/show/+page.jsx → /show
src/routes/(auth)/login/+page.jsx → /login

The (app) and (auth) segments are invisible to the router; they exist purely to let you share a +layout.tsx+layout.jsx across a subset of routes without that grouping appearing in the URL.

Route Parameters

Dynamic segments are marked with square brackets:

  • src/routes/show/[id]/+page.tsxsrc/routes/show/[id]/+page.jsx matches /show/1, /show/abc, etc.
  • src/routes/assets/[...filepath]/+page.tsxsrc/routes/assets/[...filepath]/+page.jsx uses rest syntax, matching any number of segments
  • src/routes/[[lang]]/home/+page.tsxsrc/routes/[[lang]]/home/+page.jsx uses double brackets to mark an optional parameter

Optional parameters cannot follow rest parameters, because the rest segment is greedy and will consume everything after it.

A query’s variables can come from the URL’s query string too, not just its path. See Search Params for how a route’s nullable variables map onto ?key=value.

Response Headers

A +page.tsx+page.jsx or +layout.tsx+layout.jsx can export a headers() function to set HTTP response headers for the route. This is the place for cache directives, security headers, and anything else you’d otherwise have to configure at the CDN or adapter level:

src/routes/+page.tsx
export function headers() {
return {
'Cache-Control': 'public, max-age=3600',
'X-Frame-Options': 'DENY',
}
}
export default () => <div>Hello Houdini!</div>
src/routes/+page.jsx
export function headers() {
return {
'Cache-Control': 'public, max-age=3600',
'X-Frame-Options': 'DENY',
}
}
export default () => <div>Hello Houdini!</div>

When a page renders, Houdini evaluates the headers() exports of the page and every layout in its chain, then merges the results. On a conflict the page wins over its layouts, and an inner layout wins over an outer one.

Because layouts apply to everything beneath them, the root +layout.tsx+layout.jsx is the natural place for app-wide headers like Content-Security-Policy, Strict-Transport-Security, or X-Frame-Options that you want on every document response:

src/routes/+layout.tsx
export function headers() {
return {
'Content-Security-Policy': "default-src 'self'",
'Strict-Transport-Security': 'max-age=63072000',
}
}
src/routes/+layout.jsx
export function headers() {
return {
'Content-Security-Policy': "default-src 'self'",
'Strict-Transport-Security': 'max-age=63072000',
}
}

These headers apply to the page (document) responses Houdini renders. They do not affect responses from your GraphQL endpoint, which is handled separately.

headers() only ever runs on the server (it is stripped from the client bundle), so it’s safe to read server-only secrets or environment variables inside it. It runs before the response is streamed, so it can’t depend on query data; headers must be determinable from the request alone.