Routing & Layouts
Master Dinou's file-system router: nested layouts with independent Horizontal ISR, parallel slots with error isolation, route parameters, and layout control flags.
Looking for Type-Safe Routing?
Dinou v7 introduces zero-runtime type safety with auto-generated route definitions, IDE autocomplete, and typed PageProps & <Link>.
1. Overview & Directory Conventions
Every public route in Dinou maps directly to a folder inside src/ containing an entrypoint file (page.tsx, page.jsx, page.ts, or page.js). By default, pages are React Server Components (RSC) rendered natively on the server with zero client bundle overhead.
1.1. Core File Conventions
src/page.tsx → /src/blog/page.tsx → /blogsrc/blog/[slug]/page.tsx → /blog/hello-worldsrc/dashboard/settings/page.tsx → /dashboard/settings1.2. In-Memory Route Resolution
You never write central routing tables or configure URL maps manually. The folder structure inside src/ is your single source of truth.
When a request arrives (for example, /blog/hello-world), Dinou matches it entirely in memory:
- Instant matching: Directly locates
src/blog/[slug]/page.tsxin memory with zero subprocess overhead or disk I/O latency. - Parameter extraction: Automatically parses dynamic segments into typed
params(e.g.{ slug: "hello-world" }). - Layout nesting: Automatically wraps the page inside its parent layouts (
src/layout.tsx→src/blog/layout.tsx).
You can use .tsx, .ts, .jsx, or .js interchangeably across all pages and layouts.
2. Route Types & Pattern Matching
Dinou provides expressive pattern matching for static, dynamic, optional, and catch-all URL segments:
2.1. Static & Dynamic Segments
| Pattern | Folder Structure | URL Match Example | Parsed params |
|---|---|---|---|
| Static | src/about/page.tsx | /about | {} |
| Dynamic | src/blog/[slug]/page.tsx | /blog/hello | { slug: "hello" } |
| Optional Single | src/shop/[[category]]/page.tsx | /shop /shop/shoes | { category: undefined }{ category: "shoes" } |
2.2. Catch-All & Optional Catch-All
| Pattern | Folder Structure | URL Match Example | Parsed params |
|---|---|---|---|
| Catch-all (Required) | src/docs/[...slug]/page.tsx | /docs/routing/params | { slug: ["routing", "params"] } |
| Optional Catch-all | src/docs/[[...slug]]/page.tsx | /docs /docs/a/b | { slug: [] }{ slug: ["a", "b"] } |
Catch-all segments ([...slug]) consume all remaining path segments into a string array. Because they consume the remainder of the URL, Catch-all segments must always be the terminal (last) segment of a path definition.
2.3. The Strict No-Gap Rule
Dinou enforces a strict No-Gap Rule when resolving nested optional segments such as /inventory/[[warehouse]]/[[aisle]]:
You can omit optional segments only if they are omitted from the tail end of the URL:
/inventory/main ✅ (aisle omitted)
/inventory ✅ (both omitted)
You cannot omit an earlier optional parameter while setting a later one:
(Skip warehouse, set aisle: ambiguous URL structure)
3. Route Parameters & Validation
Dinou provides a clean, unified data contract. Route parameters are extracted from the URL and injected directly into components and server functions.
3.1. Dynamic Params Prop Injection
In Dinou v7, parameters are strictly typed using PageProps, LayoutProps, ErrorProps, and RouteParams:
page.tsxcomponents:Page({ params }: PageProps<"/blog/[slug]">)layout.tsxcomponents:Layout({ params, children }: LayoutProps<"/blog/[slug]">)error.tsxpages:Error({ error, params }: ErrorProps<"/blog/[slug]">)not_found.tsx404 pages:NotFound({ params }: PageProps<"/blog/[slug]">)- Parallel slot pages (
@slot/page.tsx):SlotPage({ params }: PageProps<"/dashboard/[teamId]">) - Server data fetching:
getProps(params: RouteParams<"/blog/[slug]">)inpage_functions.ts
// src/blog/[slug]/page.tsx
import type { PageProps } from "dinou";
interface BlogPostProps extends PageProps<"/blog/[slug]"> {
post?: { title: string; content: string };
}
export default function BlogPost({ params, post }: BlogPostProps) {
// ✅ params.slug is strongly typed as string automatically
return (
<article className="p-6">
<h1 className="text-2xl font-bold">{post?.title ?? params.slug}</h1>
<p className="mt-2 text-muted-foreground">{post?.content}</p>
</article>
);
}Strict Signature Rule
page_functions.ts and layout_functions.ts, Dinou passes the parameters dictionary directly as the first argument: use getProps(params) and validateParams(params) (do not write getProps({ params })). However, in React components (page.tsx, layout.tsx, error.tsx, not_found.tsx), params is passed as a component prop: export default function Page({ params }).3.2. Route Validation (validateParams)
Exporting a validateParams(params) function from page_functions.ts or layout_functions.ts lets you enforce semantic validation rules before data fetching or rendering begins. Returning false (or a Promise that resolves to false) immediately aborts the request and issues a clean 404 response:
// src/product/[id]/page_functions.ts
import type { RouteParams } from "dinou";
export function validateParams(params: RouteParams<"/product/[id]">) {
// Ensure the product ID is strictly numeric; otherwise render 404
return /^\d+$/.test(params.id);
}
export async function getProps(params: RouteParams<"/product/[id]">) {
// Only called if validateParams returned true
const product = await db.products.findById(params.id);
return { product };
}- Dynamic SSR: Returns an instant 404 without running
getProps, preventing wasteful database queries on invalid URLs. - Incremental Static Generation (ISG / ISR): Prevents invalid URLs from triggering on-demand page generation or polluting edge caches with broken pages.
- Static Site Exports (SSG): Only valid parameter combinations generate HTML files during
dinou export.
3.3. Query Parameters Access
Query parameters (e.g. ?search=react&sort=asc) are deliberately isolated from static route matching to preserve edge cache stability. In Dinou, useSearchParams() is a universal hook that works seamlessly in both Server Components and Client Components:
Use useSearchParams() anywhere. It returns a standard Web URLSearchParams instance:
import { useSearchParams } from "dinou";
// Works identically in Server Components & "use client"!
export default function SearchPage() {
const searchParams = useSearchParams();
const q = searchParams.get("search") ?? "";
return <div>Search: {q}</div>;
}Outside React components (where hooks cannot run), access the active query via getContext().req.query:
import { getContext } from "dinou";
// src/search/page_functions.ts
// getContext() is available in getProps, validateParams, etc.
export async function getProps() {
const { req } = getContext();
const search = req.query?.search;
return { search };
}"use server"; // Declared at the top of the file
import { getContext } from "dinou";
// src/actions/items.ts
export async function filterItems() {
const { req } = getContext();
const search = req.query?.search;
return db.items.find({ q: search });
}4. Layouts & Special Files
Dinou provides special files that cascade down the directory hierarchy. They allow you to compose nested layouts, manage independent ISR lifecycles, isolate errors, and break out of hierarchies using control flags.
4.1. Nested Layouts (layout.tsx)
A layout.tsx wraps all child pages and sub-layouts in its directory. On client navigation between sibling pages, parent layouts preserve their React state, input fields, scroll position, and DOM tree without re-mounting.
A layout component receives children, params, any parallel slots (e.g. sidebar), and props returned by layout_functions.ts:
// src/dashboard/[teamId]/layout.tsx
import type { ReactNode } from "react";
import type { LayoutProps } from "dinou";
interface DashboardLayoutProps extends LayoutProps<"/dashboard/[teamId]"> {
sidebar?: ReactNode; // Parallel slot (@sidebar)
}
export default function DashboardLayout({
children,
params,
sidebar,
}: DashboardLayoutProps) {
return (
<div className="flex min-h-screen">
<aside className="w-64 border-r p-4">{sidebar}</aside>
<main className="flex-1 p-6">
<h1 className="text-xl font-bold">Team: {params.teamId}</h1>
{children}
</main>
</div>
);
}4.2. Horizontal Segmentation & layout_functions.ts
In traditional SSR frameworks, layout shells and child pages are tightly coupled. Navigating to a dynamic child route often forces the entire layout to re-evaluate, resetting video playback, collapsing open menus, or wiping sidebar search state.
Dinou v7 introduces Horizontal Layered Segmentation. Layouts and pages travel on completely separate execution, streaming, and caching pathways:
Renders exclusively the outer layout shell and any parallel slots, establishing the boundary where child page content is mounted.
- Governed by
layout_functions.ts - Maintains its own independent cache TTL
- Never unmounts or re-executes during sibling navigations
Renders exclusively the active leaf page component (<Page />) and streams into the layout's page slot.
- Governed by
page_functions.ts - Can be highly dynamic or real-time SSR
- Navigating between siblings updates only this plane
Decoupled Lifecycles in Practice: layout_functions.ts
Because layouts and pages are horizontally decoupled, they can declare completely independent caching lifetimes (TTL / ISR). A layout shell can cache for hours while its child pages revalidate every few seconds:
// src/dashboard/[teamId]/layout_functions.ts - Independent Layout ISR
import type { RouteParams } from "dinou";
// 1. Data Fetching for the Layout Shell
export async function getProps(params: RouteParams<"/dashboard/[teamId]">) {
const team = await fetchTeamData(params.teamId);
return { team };
}
// 2. Independent Cache Lifetime (1 hour)
// Child pages inside this dashboard can update every 5 seconds without invalidating this shell!
export const revalidate = 3600;
// 3. Granular Invalidation Tags (constant or function getCacheTags)
// Calling revalidateTag("team-shell") purges only the layout without wiping child page caches.
export const getCacheTags = ["team-shell"];Props returned by getProps in layout_functions.ts are automatically injected into layout.tsx as top-level props alongside children, params, and any parallel slots.
4.3. Layout Control Flags (reset_layout, no_layout)
Dinou allows you to break out of the nested layout hierarchy using empty flag files (files without extensions) placed in the route directory:
| Flag File | Applies To | Architectural Effect |
|---|---|---|
| reset_layout | layout.tsx | Resets the layout tree. This layout becomes the new root layout, completely ignoring all parent layouts (including src/layout.tsx). Perfect for marketing landing pages or auth portals. |
| no_layout | page.tsx not_found.tsx error.tsx | Bypasses all layouts entirely across the route. The page, custom 404, and error boundary render raw without any parent layout wrappers (ideal for embeddable widgets, checkout modals, or pure HTML canvas views). |
| no_layout_error | error.tsx | Granular override: prevents parent layouts from wrapping the error boundary UI only, while preserving layouts for the regular page. |
| no_layout_not_found | not_found.tsx | Granular override: prevents parent layouts from wrapping the custom 404 page only, while preserving layouts for the regular page. |
src/landing/reset_layout (empty file)
src/landing/page.tsx
→ The landing page ignores the root
src/layout.tsx header and navigation bar.4.4. Error Pages (error.tsx)
Create an error.tsx file in any route directory to catch and isolate runtime errors locally. If an exception occurs in a child component, Dinou bubbles up to the closest error.tsx, rendering it while keeping all parent layouts interactive:
// src/dashboard/[teamId]/error.tsx
import type { ErrorProps } from "dinou";
// Native: ErrorProps infers params and types the captured error
export default function DashboardError({
error,
params,
}: ErrorProps<"/dashboard/[teamId]">) {
return (
<div className="p-6 rounded-xl border border-rose-500/20 bg-rose-50/20">
<h3 className="text-rose-600 font-bold">Failed to load team {params.teamId}</h3>
<p className="text-xs text-muted-foreground mt-1">{error.message}</p>
{error.stack && (
<pre className="mt-3 p-3 bg-slate-900 text-slate-100 rounded text-xs overflow-auto">
{error.stack}
</pre>
)}
</div>
);
}// src/dashboard/[teamId]/error.tsx
import type { PageProps } from "dinou";
// Alternative: extend PageProps if you declare an interface
interface DashboardErrorProps extends PageProps<"/dashboard/[teamId]"> {
error: Error;
}
export default function DashboardError({
error,
params,
}: DashboardErrorProps) {
return (
<div className="p-6 rounded-xl border border-rose-500/20 bg-rose-50/20">
<h3 className="text-rose-600 font-bold">Failed to load team {params.teamId}</h3>
<p className="text-xs text-muted-foreground mt-1">{error.message}</p>
{error.stack && (
<pre className="mt-3 p-3 bg-slate-900 text-slate-100 rounded text-xs overflow-auto">
{error.stack}
</pre>
)}
</div>
);
}// src/dashboard/[teamId]/error.tsx
import type { RouteParams } from "dinou";
// Alternative: inline typing using the RouteParams utility
export default function DashboardError({
error,
params,
}: {
error: Error;
params: RouteParams<"/dashboard/[teamId]">;
}) {
return (
<div className="p-6 rounded-xl border border-rose-500/20 bg-rose-50/20">
<h3 className="text-rose-600 font-bold">Failed to load team {params.teamId}</h3>
<p className="text-xs text-muted-foreground mt-1">{error.message}</p>
{error.stack && (
<pre className="mt-3 p-3 bg-slate-900 text-slate-100 rounded text-xs overflow-auto">
{error.stack}
</pre>
)}
</div>
);
}Security Note: For production deployments, error.stack is automatically stripped by Dinou to prevent internal file paths and system details from leaking. In development mode, the full cleaned stack trace is provided.
4.5. Custom 404 Pages (not_found.tsx)
Define custom 404 views by placing not_found.tsx in any route directory. When an unmatched URL segment is requested, Dinou bubbles up to render the closest not_found.tsx template inside its enclosing layout.
// src/blog/[slug]/not_found.tsx (Scoped to /blog/[slug])
import { Link, type PageProps } from "dinou";
// Strongly types params.slug as string
export default function NotFound({ params }: PageProps<"/blog/[slug]">) {
return (
<div className="flex flex-col items-center justify-center p-8 text-center">
<h2 className="text-3xl font-bold">404 - Article Not Found</h2>
<p className="mt-2 text-muted-foreground">
The article "{params.slug}" could not be located.
</p>
<Link
href="/"
className="mt-4 px-4 py-2 bg-primary text-primary-foreground rounded-lg font-medium text-sm"
>
Back to Home
</Link>
</div>
);
}// src/not_found.tsx (Global Root Fallback)
import { Link, type PageProps } from "dinou";
// PageProps<"/"> strictly ensures no dynamic params are accidentally accessed
export default function GlobalNotFound({ params }: PageProps<"/">) {
return (
<div className="flex flex-col items-center justify-center p-8 text-center">
<h2 className="text-3xl font-bold">404 - Page Not Found</h2>
<p className="mt-2 text-muted-foreground">
The requested page does not exist on this server.
</p>
<Link
href="/"
className="mt-4 px-4 py-2 bg-primary text-primary-foreground rounded-lg font-medium text-sm"
>
Back to Home
</Link>
</div>
);
}// 1. Sibling server contract: src/blog/[slug]/page_functions.ts
export async function getProps(params: { slug: string }) {
const suggestions = await db.posts.findMany({ take: 3 });
return { suggestions };
}
// 2. 404 Page: src/blog/[slug]/not_found.tsx
import { Link, type PageProps } from "dinou";
interface NotFoundProps extends PageProps<"/blog/[slug]"> {
suggestions: { id: string; title: string; slug: string }[];
}
export default function NotFound({ params, suggestions }: NotFoundProps) {
return (
<div className="flex flex-col items-center justify-center p-8 text-center">
<h2 className="text-3xl font-bold">404 - Article Not Found</h2>
<p className="mt-2 text-muted-foreground">
Could not find "{params.slug}". Here are some recommended articles:
</p>
{suggestions && (
<ul className="mt-4 space-y-1">
{suggestions.map((item) => (
<li key={item.id}>
<Link
href={`/blog/${item.slug}`}
className="text-primary hover:underline text-sm font-medium"
>
→ {item.title}
</Link>
</li>
))}
</ul>
)}
</div>
);
}When nested in a dynamic folder (such as src/blog/[slug]/not_found.tsx), pass the route pattern generic (PageProps<"/blog/[slug]">) to strongly type params. For a root global 404 (src/not_found.tsx), pass PageProps<"/"> to ensure strict type safety without accidental parameters.
Can 404 Pages Receive Server Props via getProps?
Yes. If you define a page_functions.{ts,js} file in the same route directory as not_found.tsx, Dinou executes its getProps(params) server lifecycle and merges the returned data directly into the component's props alongside params (as demonstrated in the With Server Props tab above).
This allows 404 views to dynamically query databases or external APIs (such as recommended articles, related categories, or search fallbacks) and combine them with PageProps via interface extension (interface NotFoundProps extends PageProps<"/blog/[slug]">) or intersection types.
Difference with error.tsx: While not_found.tsx can safely execute getProps, error.tsx pages never invoke server contracts (they receive strictly ErrorProps) to prevent compounding unhandled server exceptions.
5. Route Organization & Parallel Slots
Dinou provides dedicated directory conventions to organize enterprise codebases cleanly and compose parallel UI slots without impacting URL ergonomics:
5.1. Route Groups (folder)
Folders wrapped in parentheses (e.g. (auth), (marketing)) are omitted from the public URL path. They serve two fundamental architectural purposes in Dinou:
- Logical Organization: Group related routes together without introducing unwanted segment prefixes in the URL.
- Multiple Root Layouts: Apply completely different top-level layout wrappers to different areas of your application without URL changes.
In conventional file-system routers, a single global layout forces all pages to share the same top navigation bar and footer. With Dinou Route Groups, you can create independent root layouts for marketing pages versus authentication screens:
- •Independent Filesystem Paths:
src/(marketing)/layout.tsxandsrc/(auth)/layout.tsxlive in distinct disk directories, so there is zero file collision. - •Request to
/pricing: Dinou crawls into(marketing), matchespricing/page.tsx, and wraps it exclusively with the marketing layout. - •Request to
/login: Dinou crawls into(auth), matcheslogin/page.tsx, and wraps it exclusively with the auth layout.
src/layout.tsx, both groups nest inside it (e.g. for HTML <head> or global fonts). If omitted, each group acts as a completely separate root layout.Avoid URL Path Collisions
src/(marketing)/about/page.tsx and src/(auth)/about/page.tsx would cause a build/resolution collision because both map to /about.5.2. Escaping Route Scanning with Underscore (_folder)
In Dinou, you do not need to prefix folders like components/, hooks/, or utils/ with an underscore. Any directory that simply does not contain a page.tsx (or page.jsx) is already completely ignored by the router and will never register a route.
The Real Purpose of "_": Legacy Code & Archived Pages
_folderName) is to escape the in-process route scanner when dealing with legacy code, drafts, or archived folders that actually contain page.tsx / page.jsx files inside them.How it Works in Practice:
Whenever Dinou's scanner encounters a folder starting with _, it completely bypasses that directory tree, ensuring that stale, archived, or legacy page files are never compiled into the production router.
5.3. Parallel Routes & Slots (@slot)
Parallel routes allow you to render multiple independent pages simultaneously inside the same parent layout (e.g., @sidebar, @analytics, @modal):
- src/dashboard/@sidebar/page.tsx
- src/dashboard/@analytics/page.tsx
- src/dashboard/page.tsx (main children)
- src/dashboard/layout.tsx
Slots are passed as named props directly alongside children:
import type { ReactNode } from "react";
import type { LayoutProps } from "dinou";
interface DashboardLayoutProps extends LayoutProps<"/dashboard"> {
sidebar?: ReactNode;
analytics?: ReactNode;
}
export default function Layout({
children,
sidebar,
analytics,
}: DashboardLayoutProps) {
return (
<div className="flex">
<aside>{sidebar}</aside>
<main>{children}</main>
<section>{analytics}</section>
</div>
);
}5.4. Slot Error Containment Architecture
Dinou isolates errors within parallel slots to prevent a single widget failure from crashing the entire page:
error.tsx, Dinou safely falls back to rendering null for that slot without taking down the page.children page, and parent layouts remain fully mounted and interactive even if an individual slot fails to render.