Nested Layouts
Furin uses _route.tsx files for directory-level layouts and route config. Each _route.tsx can point at a parent route, declare loaders, and wrap child pages.
Basic Layout
src/pages/blog/_route.tsx
import { createRoute } from "@teyik0/furin/client"
import { route as rootRoute } from "../root"
export const route = createRoute({
parent: rootRoute,
layout: ({ children }) => (
<div className="mx-auto max-w-5xl px-4 py-10">
<BlogSidebar />
<main>{children}</main>
</div>
),
})
Pages inside that directory use the same route object:
src/pages/blog/index.tsx
import { route } from "./_route"
export default route.page({
component: () => <BlogList />,
})
Root Layout
src/pages/root.tsx is required. It is the top-level route and should render <html>, <head>, and <body>.
src/pages/root.tsx
import { createRoute } from "@teyik0/furin/client"
export const route = createRoute({
layout: ({ children }) => (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta content="width=device-width, initial-scale=1" name="viewport" />
</head>
<body>{children}</body>
</html>
),
})
Multi-Level Nesting
text
src/pages/
├── root.tsx
├── docs/
│ ├── _route.tsx
│ └── advanced/
│ ├── _route.tsx
│ └── plugins.tsx
src/pages/docs/advanced/_route.tsx
import { createRoute } from "@teyik0/furin/client"
import { route as docsRoute } from "../_route"
export const route = createRoute({
parent: docsRoute,
layout: ({ children }) => <section className="advanced-layout">{children}</section>,
})
Layout Loaders
Layout data flows into the layout itself and all child pages.
src/pages/dashboard/_route.tsx
import { createRoute } from "@teyik0/furin/client"
import { route as rootRoute } from "../root"
export const route = createRoute({
parent: rootRoute,
loader: async ({ request }) => {
const user = await getSession(request)
return { user }
},
layout: ({ children, user }) => (
<div>
<DashboardNav user={user} />
{children}
</div>
),
})
The page below does not need to repeat the auth/session work:
src/pages/dashboard/index.tsx
import { route } from "./_route"
export default route.page({
component: ({ user }) => <DashboardHome user={user} />,
})
Streaming slow data from a layout
A layout loader can return defer({...}) to stream slow fields while the shell renders immediately. The deferred Promise lands on the layout's children-receiving component exactly like a page-level deferred field:
src/pages/dashboard/_route.tsx
import { createRoute, defer, Await } from "@teyik0/furin/client"
import { Suspense } from "react"
export const route = createRoute({
loader: () =>
defer({
user: "alice", // sync — embedded in the initial shell
widgets: fetchSidebarWidgets(), // Promise — streamed when it resolves
}),
layout: ({ children, user, widgets }) => (
<div>
<nav>Hello {user}</nav>
<aside>
<Suspense fallback={<WidgetSkeleton />}>
<Await resolve={widgets}>{(list) => <WidgetList items={list} />}</Await>
</Suspense>
</aside>
<main>{children}</main>
</div>
),
})
See Deferred Data Streaming for the full API.