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 → /showssrc/routes/show/[id]/+page.tsx → /show/:idsrc/routes/+page.jsx → /src/routes/shows/+page.jsx → /showssrc/routes/show/[id]/+page.jsx → /show/:idexport default () => <div>Hello Houdini!</div>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:
export default ({ children }) => ( <> <NavBar /> {children} </>)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 → /showssrc/routes/(app)/show/+page.tsx → /showsrc/routes/(auth)/login/+page.tsx → /loginsrc/routes/(app)/shows/+page.jsx → /showssrc/routes/(app)/show/+page.jsx → /showsrc/routes/(auth)/login/+page.jsx → /loginThe (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.jsxmatches/show/1,/show/abc, etc.src/routes/assets/[...filepath]/+page.tsxsrc/routes/assets/[...filepath]/+page.jsxuses rest syntax, matching any number of segmentssrc/routes/[[lang]]/home/+page.tsxsrc/routes/[[lang]]/home/+page.jsxuses 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:
export function headers() { return { 'Cache-Control': 'public, max-age=3600', 'X-Frame-Options': 'DENY', }}
export default () => <div>Hello Houdini!</div>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:
export function headers() { return { 'Content-Security-Policy': "default-src 'self'", 'Strict-Transport-Security': 'max-age=63072000', }}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.