roggc/dinou

Full-Stack React 19 Framework

JavaScript

11

459 commits

updated Oct 16, 2026

See the code

See what people are saying

SourceMessageScoreDate

Dinou v7: A React 19 RSC & Server Functions framework with PPR & vertical/horizontal segmentation that runs natively on Node.js, Cloudflare Workers, Netlify, Bun, and Deno (Zero lock-in) (r/reactjs)

Hi everyone! 👋 I’ve just released Dinou v7 (7.0.0), an ejectable full-stack React 19 framework built on web standards. My goal with Dinou v7 was to harness the full power of React 19 (Server Components, Server Functions, streaming SSR, and Partial Prerendering) while keeping the architecture…

0

Oct 14, 2026

README

Dinou

Documentation Version

Full-Stack React 19 Framework


The ejectable React 19 framework built on web standards. Build full-stack applications with native React Server Components (RSC), progressive streaming SSR, compile-time type-safe routing, dual-dimensional caching, and universal multi-platform deployment (Node.js, Bun, Deno, Cloudflare Workers, Netlify, and pure static hosts) with zero vendor lock-in.

Key Features

  • Native React 19 RSC & Progressive Web Streams: Render database queries, markdown parsers, and heavy backend dependencies exclusively on the server with 0kb client JavaScript overhead. Dinou streams progressive React Flight wire payloads and HTML over standard W3C Web Streams (renderToReadableStream) directly into the DOM with instant shell flushing.
  • Universal 3-Pass In-Memory Dual-Bundle Engine (Zero-Fork): Solves React 19's conflicting module conditions ("react-server" vs. "browser") within a single in-process pipeline via standard W3C Web Streams. Completely eliminates child_process.fork() and inter-process serialization latency, delivering an 82% memory reduction (~22 MB flat idle footprint) and enabling native execution on edge isolates.
  • Compile-Time Type-Safe Routing: Bidirectional type safety with zero runtime bundle overhead. Generates route definitions in real time: autocompletes and validates <Link href="..."> and useRouter().push(...), while strictly typing route parameters with PageProps<T>, LayoutProps<T>, RouteParams<T>, and ErrorProps<T>.
  • Persistent Nested Layouts & Zero-Remount: Folders in src/ map directly to URLs with nested layout.tsx files that wrap child pages without remounting during navigation, preserving client state, form inputs, audio playback, and scroll positions. Layouts feature independent data and cache lifecycles (layout_functions.ts).
  • Dual-Dimensional Caching (Horizontal & Vertical):
    • Horizontal (Route-Level): Static Site Generation (revalidate = false), strict seconds-based Incremental Static Regeneration (revalidate = N), and dynamic on-demand Server-Side Rendering (revalidate = 0).
    • Vertical (Component-Level): Micro-ISR via <DinouCacheSlot> to embed high-frequency reactive sections with micro-revalidation intervals (e.g., 5s) inside long-lived static shells, refreshable independently from the client (refreshSlot) or purged globally by tag (revalidateTag).
  • Partial Prerendering (PPR): Unifies instant static edge delivery with dynamic personalization in a single request. Serves an edge-cached static shell in under 20ms while personalized <Suspense> holes concurrently stream over a single HTTP connection.
  • Server Functions & Direct Suspense Streaming ("Dinou Pattern"): Asynchronous functions marked with "use server" can be called directly from Client Components ("use client") wrapped inside <Suspense> boundaries. They can return rendered React components over Flight RPC, eliminating synthetic REST/GraphQL API boilerplate.
  • Bundler Agnostic with 100% Feature Parity: Pre-configured, zero-config pipelines for esbuild (sub-millisecond dev server & HMR), Rollup (pristine ES module output for Cloudflare Workers & Edge), and Webpack (React Compiler and legacy npm compatibility) with identical dual-bundle output.
  • Universal Multi-Platform Deployment: Build once with React 19 and deploy the exact same application code to Node.js, Bun (standalone single-binary bun build --compile), Deno (native single-file binaries deno compile), Cloudflare Workers (native workerd + Cloudflare KV for ISR), Netlify, or pure static exports.
  • Universal W3C Web Middleware (onRequest): Intercept requests and high-frequency webhooks (Stripe, Clerk, GitHub) at the HTTP layer in dinou.config.mjs returning native Web Response objects in <2ms, completely bypassing React component rendering.
  • Hardened State-Preserving Fast Refresh: Under 5ms HMR powered by Rust (@swc/core) and WebSockets that patches updates into running applications while preserving active form inputs, counters, and component state across edits.
  • 100% Ejectable & AI-Friendly Architecture: Run npx dinou eject (or npm run eject) at any time to extract Dinou's complete compiler, server runtime, and React 19 flight pipeline directly into your project. Transparent, web standards-based, and free of proprietary black-box macros for seamless AI pair programming.

Table of contents

Getting Started

Start building modern, full-stack React 19 applications in seconds. Choose between the automated CLI generator or manual configuration for full architectural control.

Prerequisites

Before getting started, ensure your local environment meets the minimum requirements. Dinou is engineered to leverage modern JavaScript standards with zero transpile overhead:

  • Node.js 20.6.0+ (Required): Dinou utilizes Node's modern native ES module customization hooks (module.register) for zero-fork server execution and Web Standard APIs (Request, Response, ReadableStream, Headers) natively supported in modern Node.js versions.
  • Bun 1.1+ (Optional): If you plan to run on Bun or compile self-contained standalone native executables using npm run build:bun:compile.
  • Deno 2.0+ (Optional): If you target Deno or cross-compile single-file binaries for Linux, macOS, or Windows using npm run build:deno:compile.
  • Cloudflare Wrangler (Optional): Required if you deploy to Cloudflare Workers with npm run build:cloudflare and preview locally with wrangler dev.

Quick Start (CLI)

The fastest way to scaffold a ready-to-code Dinou application is using the official CLI starter. It sets up TypeScript, Tailwind CSS, sample Server Components, and the recommended directory layout:

# 1. Initialize a new Dinou application
npx create-dinou@latest my-app
# or
npm create dinou@latest my-app

# 2. Enter project directory
cd my-app

# 3. Launch development server (default: esbuild)
npm run dev

Open http://localhost:3000 in your browser to view your live application.

Manual Setup

Prefer to wire up everything manually? Follow these step-by-step instructions to configure Dinou from scratch with complete transparency:

1. Install Dependencies

Initialize your project folder and install React 19, React DOM, and the dinou framework package:

# Create and enter directory
mkdir my-dinou-app
cd my-dinou-app

# Initialize package.json
npm init -y

# Install React 19 and Dinou
npm install react react-dom dinou

# Install TypeScript definitions for IDE intellisense
npm install -D @types/react @types/react-dom

2. Project Structure & Files

Create the src directory with a root page (src/page.tsx) and the recommended root document layout (src/layout.tsx):

// src/page.tsx - React Server Component (RSC)
export default function Page() {
  return (
    <main className="p-8 max-w-4xl mx-auto space-y-4">
      <h1 className="text-3xl font-bold tracking-tight">
        Hello from Dinou!
      </h1>
      <p className="text-muted-foreground">
        Rendered natively on the server with React 19 and zero client JavaScript overhead.
      </p>
    </main>
  );
}
// src/layout.tsx - Root document layout
import type { ReactNode } from "react";

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>My Dinou App</title>
      </head>
      <body>{children}</body>
    </html>
  );
}

The root layout.tsx defines the shared <html> and <body> document shell, metadata, and global providers so you don't have to repeat them on each page.

my-dinou-app/
├── public/                # Static root assets (served directly at /)
│   ├── favicon.ico
│   ├── robots.txt
│   └── site.webmanifest
├── src/
│   ├── page.tsx           # Root page component (RSC)
│   ├── layout.tsx         # Root document layout (<html/head/body>)
│   └── globals.css        # Styles / Tailwind CSS
├── dinou.config.ts        # Framework configuration (optional: ppr, plugins)
├── dinou-env.d.ts         # Ambient framework typings
├── tsconfig.json          # TypeScript configuration
├── package.json
└── .gitignore

3. TypeScript Configuration

Dinou does not force you to use TypeScript—you are completely free to write your application in standard JavaScript (.js / .jsx). However, even for pure JavaScript projects, defining a tsconfig.json in your project root is strongly recommended:

  • Import Aliases: Dinou's build engines (esbuild, Rollup, Webpack) automatically read compilerOptions.paths from tsconfig.json to resolve clean path aliases like @/* into src/*.
  • IDE Autocomplete: With "allowJs": true, code editors (VS Code, Cursor) use tsconfig.json to provide full autocomplete, auto-imports, and path resolution across all .js and .jsx files.
{
  "compilerOptions": {
    "paths": {
      "@/*": ["src/*"]
    },
    "allowJs": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "module": "ESNext",
    "moduleResolution": "bundler"
  },
  "include": [
    "src",
    "dinou-env.d.ts"
  ]
}

Create dinou-env.d.ts in your root directory:

/// <reference types="dinou/env" />

The dinou/env reference automatically declares ambient typings for static assets (SVGs, images, web fonts) and CSS modules without manual type boilerplate.

4. Package Scripts

Add the standard Dinou scripts to your package.json to control development, multi-runtime production builds (Node, Cloudflare, Deno, Bun), binary compilation, static exports, and ejecting:

{
  "scripts": {
    "dev": "dinou dev",
    "build": "dinou build",
    "start": "dinou start",
    "dev:esbuild": "dinou dev:esbuild",
    "dev:rollup": "dinou dev:rollup",
    "dev:webpack": "dinou dev:webpack",
    "build:node": "dinou build:node",
    "start:node": "dinou start:node",
    "build:cloudflare": "dinou build:cloudflare",
    "build:deno": "dinou build:deno",
    "build:deno:compile": "dinou build:deno:compile",
    "start:deno": "dinou start:deno",
    "build:bun": "dinou build:bun",
    "build:bun:compile": "dinou build:bun:compile",
    "start:bun": "dinou start:bun",
    "export-static": "dinou export-static",
    "eject": "dinou eject"
  }
}

Note: For the complete catalog of targeted platform cross-compilation scripts (such as Windows, macOS, Linux ARM/x64), see the Complete CLI Scripts Catalog.

5. Configure .gitignore

Create a .gitignore file to ignore dependencies, environment variables, build outputs, and Dinou caches:

node_modules
.dinou
out
dist
.wrangler
.env
  • .dinou/ holds internal dev artifacts and runtime builds.
  • out/ contains pages generated by export-static.
  • dist/ stores compiled single-executable binaries (Deno / Bun).
  • .wrangler/ stores local state when testing Cloudflare Workers.

6. Start Development Server

Run the development server with the fast esbuild bundler (default), or choose Rollup or Webpack:

# Start with default ultra-fast esbuild
npm run dev

# Or start with Rollup or Webpack
npm run dev:rollup
npm run dev:webpack

Building for Production & Target Runtimes

Dinou provides both bundler freedom (esbuild, Rollup, Webpack) and universal runtime deployment (Node.js, Cloudflare Workers, Deno, Bun, or Static SSG) from a single codebase in src/ with zero code changes.

Bundler Flexibility (esbuild, Rollup, Webpack)

By default, Dinou uses esbuild for sub-second cold starts and in-memory compilation. When enterprise plugins or custom loaders are required, you can switch seamlessly to Rollup or Webpack with 100% feature parity:

# Start development with your preferred bundler
npm run dev          # Default: esbuild
npm run dev:rollup   # Dedicated Rollup in-memory engine
npm run dev:webpack  # Dedicated Webpack engine

For advanced compiler tuning, custom plugins, and SWC options, visit the Bundlers Guide.

First-Class Multi-Runtime Targets

Compile your application for any execution environment without altering component logic:

  • Node.js Production Server:
    npm run build:node && npm run start:node
    
  • Cloudflare Workers (Edge):
    npm run build:cloudflare && npx wrangler dev
    
  • Self-Contained Executable Binary (Deno / Bun):
    npm run build:deno:compile # or build:bun:compile
    
    Outputs standalone single-file executables directly to dist/.
  • Pure Static Site Export (SSG):
    npm run export-static
    
    Pre-rendered HTML and RSC Flight payloads output directly to out/.

For complete deployment guides, Docker setups, and cloud hosting instructions, see the Deployment Guide.

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.

[!TIP] 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 → /blog
  • src/blog/[slug]/page.tsx → /blog/hello-world
  • src/dashboard/settings/page.tsx → /dashboard/settings

1.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.tsx in 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

PatternFolder StructureURL Match ExampleParsed params
Staticsrc/about/page.tsx/about{}
Dynamicsrc/blog/[slug]/page.tsx/blog/hello{ slug: "hello" }
Optional Singlesrc/shop/[[category]]/page.tsx/shop
/shop/shoes
{ category: undefined }
{ category: "shoes" }

2.2. Catch-All & Optional Catch-All

PatternFolder StructureURL Match ExampleParsed params
Catch-all (Required)src/docs/[...slug]/page.tsx/docs/routing/params{ slug: ["routing", "params"] }
Optional Catch-allsrc/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]]:

✅ Allowed: Trailing Omission

You can omit optional segments only if they are omitted from the tail end of the URL:

  • /inventory/main/a1 ✅ (both provided: { warehouse: "main", aisle: "a1" })
  • /inventory/main ✅ (aisle omitted: { warehouse: "main", aisle: undefined })
  • /inventory ✅ (both omitted: { warehouse: undefined, aisle: undefined })
❌ Forbidden: Intercalated Gaps

You cannot omit an earlier optional parameter while setting a later one:

  • /inventory//a1 ❌ (empty gap / skipped warehouse while setting aisle: ambiguous URL structure rejected)

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.tsx components: Page({ params }: PageProps<"/blog/[slug]">)
  • layout.tsx components: Layout({ params, children }: LayoutProps<"/blog/[slug]">)
  • error.tsx pages: Error({ error, params }: ErrorProps<"/blog/[slug]">)
  • not_found.tsx 404 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]">) in page_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>
  );
}

[!IMPORTANT] Strict Signature Rule: In 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 };
}

Guaranteed behavior across all rendering modes:

  • 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:

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 (page_functions.ts & Server Functions)

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:

  1. The Layout Plane: 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
  2. The Page Plane: 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 FileApplies ToArchitectural Effect
reset_layoutlayout.tsxResets 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_layoutpage.tsx, not_found.tsx, error.tsxBypasses 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_errorerror.tsxGranular override: prevents parent layouts from wrapping the error boundary UI only, while preserving layouts for the regular page.
no_layout_not_foundnot_found.tsxGranular override: prevents parent layouts from wrapping the custom 404 page only, while preserving layouts for the regular page.
Example: Standalone Landing Page
  • src/landing/layout.tsx
  • 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>
  );
}

[!NOTE] 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.

Scoped to Route Segment
// 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>
  );
}
Global Root 404 Fallback
// 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>
  );
}
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:

// 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"
              >
                &rarr; {item.title}
              </Link>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

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.
Multiple Root Layouts in Practice

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:

src/
├── (marketing)/                # Public layout branch
│   ├── layout.tsx              # Navbar, Footer, Hero
│   ├── page.tsx                # → /
│   ├── about/page.tsx          # → /about
│   └── pricing/page.tsx        # → /pricing
└── (auth)/                     # Minimalist auth branch
    ├── layout.tsx              # Centered card, no Navbar
    ├── login/page.tsx          # → /login
    └── register/page.tsx       # → /register

How Dinou Resolves Layouts:

  • Independent Filesystem Paths: src/(marketing)/layout.tsx and src/(auth)/layout.tsx live in distinct disk directories, so there is zero file collision.
  • Request to /pricing: Dinou crawls into (marketing), matches pricing/page.tsx, and wraps it exclusively with the marketing layout.
  • Request to /login: Dinou crawls into (auth), matches login/page.tsx, and wraps it exclusively with the auth layout.
  • Root Hierarchy: If you also place a top-level 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

Because parenthesized folder names are stripped from the public URL, different route groups cannot define identical URL paths. For example, creating both 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

The sole and specific purpose of prefixing a directory with an underscore (_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:

src/blog/
├── page.tsx             → /blog (Active public route)
├── components/          (No page.tsx → already safe without "_")
│   └── post-card.tsx
└── _legacy_v1/          (Contains page.tsx, but "_" forces scanner to skip it!)
    ├── page.tsx         (SKIPPED — NOT a route)
    └── [slug]/page.tsx  (SKIPPED — NOT a route)

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):

Folder Structure:

  • src/dashboard/@sidebar/page.tsx
  • src/dashboard/@analytics/page.tsx
  • src/dashboard/page.tsx (main children)
  • src/dashboard/layout.tsx

Layout Prop Injection: 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:

  • Slot with error.tsx: If the slot fails, only that specific slot renders its local error UI. The rest of the page and sibling slots remain completely operational.
  • Slot without error.tsx: If an unhandled exception occurs in a slot with no error.tsx, Dinou safely falls back to rendering null for that slot without taking down the page.
  • Layout & Sibling Resilience: Sibling slots, the main children page, and parent layouts remain fully mounted and interactive even if an individual slot fails to render.

Master Dinou's client-side SPA navigation: smooth soft transitions, automatic hover prefetching, the Client Router Cache lifecycle, cache busting with the fresh prop, imperative useRouter controls, and Directory-First relative routing.

[!TIP] Looking for Type-Safe Navigation? Dinou generates automatic route types so <Link href="..."> and router.push("...") provide zero-runtime autocomplete and compile-time error checking.


1. Navigation Overview

In Dinou, navigating between routes does not trigger a full browser refresh (window.location.reload). Instead, Dinou operates as a high-performance single-page application (SPA):

  • Preserved Shell: Persistent layouts (layout.tsx) never unmount, maintaining video playback, active form inputs, scroll positions, and local component state across page changes.
  • Flight Streaming: Only the newly navigated route's Server Components are fetched over the wire via React Flight payloads.
  • Concurrent Transitions: Transitions execute inside React 19's startTransition, ensuring that the current UI stays fully responsive and interactive during data fetching.

The primary way to navigate between pages is the <Link> component imported from "dinou". It intercepts click events, prevents hard reloads, and handles background prefetching:

import { Link } from "dinou";

export function NavigationBar() {
  return (
    <nav className="flex gap-4">
      {/* ✅ Type-Safe href: autocompleted in your IDE via DinouRoute */}
      <Link href="/dashboard">Dashboard</Link>

      {/* ✅ Dynamic interpolation typed against registered routes */}
      <Link href={`/blog/${post.slug}`}>Post</Link>

      {/* Disable prefetch for heavy routes or rarely visited pages */}
      <Link href="/analytics" prefetch={false}>
        Analytics
      </Link>

      {/* ❌ TypeScript compile error on invalid/dead links when using type-safe routing */}
      {/* <Link href="/broken-link">Broken</Link> */}
    </nav>
  );
}

[!NOTE] Native HTML <a> Anchor Elements: In Dinou, standard HTML <a> tags also automatically trigger client-side soft navigation via global event delegation. However, <Link> is recommended because it provides compile-time type safety, background prefetching on hover, and the fresh prop.


3. Programmatic Navigation (useRouter)

To navigate imperatively from event handlers, timers, or form submissions, use the useRouter() hook inside Client Components ("use client"):

"use client";

import { useRouter } from "dinou";

export default function LoginButton() {
  const router = useRouter();

  const handleLogin = async () => {
    await loginUser();
    
    // Navigate programmatically
    router.push("/dashboard");
    
    // Or force fresh data after mutation:
    // router.push("/dashboard", { fresh: true });
  };

  return <button onClick={handleLogin}>Log In</button>;
}

Method Reference

MethodBehavior & Description
router.push(href, options)Navigates to the route and adds a new entry to the browser history stack. Supports { fresh: true }.
router.replace(href, options)Navigates to the route, replacing the current history entry without pushing a new history state. Supports { fresh: true }.
router.back()Navigates back to the previous page in history (equivalent to clicking the browser Back button).
router.forward()Navigates forward to the next page in the history stack.
router.refresh()Soft Reload: Re-fetches the current route data from the server and updates server components without losing client React state or scroll position.
router.refreshSlot(slotId)Re-fetches and updates an individual <DinouCacheSlot> component without re-rendering the surrounding page.

4. Client Router Cache & The fresh Prop

To deliver instant page transitions without unnecessary server requests, Dinou features an in-memory Client Router Cache. Understanding how this cache operates is key to knowing when and why to use the fresh prop.

4.1. How the Client Router Cache Works

During client execution, Dinou maintains three dedicated in-memory caches in the browser tab:

  1. pageCache: Stores prefetched and visited RSC promises keyed by pathname + search.
  2. scrollCache: Retains exact window and container scroll coordinates keyed by history state ID.
  3. popstate: Reads from pageCache and restores scroll without network requests on back/forward.
The Navigation Lifecycle
  1. On Hover: When the mouse enters a <Link href="/pricing">, Dinou automatically prefetches the route payload in the background and stores the pending RSC promise directly into pageCache.
  2. On Click: The router initiates navigation to "/pricing". Because the payload was already prefetched into pageCache, the transition renders instantly with 0ms network latency.
  3. On Back / Forward: Browser popstate events read from pageCache and restore the previous scroll position from scrollCache without making any network requests.

4.2. Cache Invalidation with the fresh Prop

While caching provides instantaneous navigation, pages with volatile data or pages visited immediately after a mutation (e.g., checkout, profile updates, or inventory counters) need fresh server state. The fresh prop provides explicit, predictable cache busting:

What happens under the hood when fresh is true:
  • No Stale Prefetch: Hovering over a <Link fresh> does not prefetch potentially stale data before the user actually clicks.
  • Cache Eviction: Upon navigation, the router explicitly runs pageCache.delete(targetUrl), removing any prior cached RSC promise.
  • Live Re-Execution: Issues a fresh HTTP request directly to the server, re-executing server components and page functions with latest data.
Usage Examples:
// Ideal for live feeds, stock tickers, or carts
<Link href="/live-feed" fresh>
  Live Feed
</Link>
// Navigate after a mutation to force fresh server data
const handleUpdate = async () => {
  await updateProfile(data);
  router.push("/profile", { fresh: true });
};

4.3. Comparison: When to Use Which Mechanism

Mechanism / ScopeCache BehaviorNetwork RequestTypical Use Case
Standard <Link>Uses pageCache if prefetched or visited0ms if in cache; single request on first visitGeneral navigation, blogs, docs, marketing pages
<Link fresh> / { fresh: true }Purges pageCache.delete(url)Always fetches live RSC payload from serverPost-mutation redirects, active carts, volatile feeds
router.refresh()Purges pageCache.delete(currentPath)Re-fetches current route in-placePolling, server function response handling, reload button
Hard Reload (F5)Destroys all memory (all caches lost)Full HTML document download + re-hydrationEmergency fallback; loses all client state

5. Route Transitions (useNavigationLoading)

In Dinou, all client-side navigations execute concurrently inside React 19's startTransition. This ensures that the current view remains fully interactive and visible while the target page's Server Components stream in from the server.

To inform users that a transition is occurring behind the scenes, Dinou provides the useNavigationLoading() hook:

  • Returns true while a route payload is actively streaming or preparing in the background, and resets to false the moment React 19 commits the new page tree.
  • Evaluates to false during server-side rendering (SSR), preventing hydration mismatches and flickering when the document first mounts.

Example: Global Top Navigation Progress Bar

Mount a client loading indicator in your root layout to provide visual feedback (similar to GitHub or YouTube) whenever a route transition is pending:

"use client";

import { useNavigationLoading } from "dinou";

export function NavigationProgressBar() {
  const isNavigating = useNavigationLoading();

  if (!isNavigating) return null;

  return (
    <div
      role="progressbar"
      aria-label="Page loading"
      className="fixed top-0 left-0 right-0 z-50 h-1 bg-blue-500/20 overflow-hidden pointer-events-none"
    >
      <div className="h-full bg-blue-600 dark:bg-blue-400 animate-pulse w-full transition-all duration-300" />
    </div>
  );
}

[!TIP] Because prefetched links resolve instantly in 0ms directly from pageCache, this progress bar only surfaces on fresh fetches (such as navigation with fresh: true or un-prefetched dynamic routes), keeping transitions ultra snappy.


6. Relative Routing Rules

Dinou resolves relative navigation paths (such as about, ./about, or ../dashboard) using a Directory-First Convention. This provides intuitive navigation that accurately mirrors your file system.

In a standard web browser, a URL like /blog/hello (without a trailing slash) treats hello as a file name, so a link to about resolves to /blog/about.

In Dinou, every route is treated as a directory because physically, each page is a folder containing a page.tsx component (e.g. src/blog/hello/page.tsx). Resolving relatively from a page navigates relative to that route's folder:

Current PathRelative TargetResolved Path
/blog/helloabout/blog/hello/about
/blog/hello./about/blog/hello/about
/blog/hello../about/blog/about

This rule applies uniformly across all navigation methods, including the <Link> component and programmatic calls like router.push().

Type-Safe Routing

Dinou v7 introduces a built-in, compile-time Type-Safe Routing engine with zero runtime overhead. During development and build time, Dinou automatically inspects your src/ directory and generates ambient TypeScript definitions in .dinou/types/routes.d.ts.


1. Overview & Zero-Runtime Guarantee

Unlike traditional routing setups that require external schema validators or proprietary bundler plugins, Dinou generates strict TypeScript definitions directly from your file system.

1.1. Outward Navigation vs. Inward Consumption

The system operates across two complementary architectural planes:

  1. Outward Navigation (Level 1): Protects outbound transitions. When using <Link href="...">, router.push("..."), or passing routes to reusable UI components with DinouRoute, TypeScript autocompletes valid routes, verifies dynamic template strings, and raises compile-time errors on dead links or typos.
  2. Inward Consumption (Level 2): Enforces inbound parameter safety. Pages, layouts, error boundaries, and server data loaders consume parameters strongly typed by route template strings via PageProps<T>, LayoutProps<T>, ErrorProps<T>, and RouteParams<T>.

1.2. Zero-Runtime Cost (0 Extra Bytes)

All route type validation occurs entirely within the TypeScript compiler. The emitted .dinou/types/routes.d.ts file contains ambient type declarations and is never bundled into your client JavaScript or server execution bundle:

  • 0 bytes added to production client bundles.
  • 0 runtime performance overhead on route matching or transitions.
  • 0 external dependencies required for type validation.

2. Quick Start for Developers

Import <Link> or useRouter from "dinou". Your IDE will immediately suggest all valid static routes, parameterized dynamic routes, relative paths, queries, and hashes:

import { Link, useRouter } from "dinou";

export default function Navigation({ post }: { post: { slug: string } }) {
  const router = useRouter();

  return (
    <nav className="flex gap-4">
      {/* ✅ Instant autocompletion in your IDE */}
      <Link href="/docs/routing">Routing Guide</Link>

      {/* ✅ Dynamic routes with typed template strings */}
      <Link href={`/blog/${post.slug}`}>View Post</Link>

      {/* ✅ Seamless support for queries, hashes, relative paths, and external URLs */}
      <Link href="/demo?tab=settings#section-1">Settings</Link>
      <Link href="../contact">Relative</Link>
      <Link href="https://github.com/roggc/dinou">GitHub</Link>

      {/* ❌ TypeScript compile-time error when pointing to a non-existent route */}
      {/* <Link href="/unknown-route" /> */}

      <button onClick={() => router.push("/docs/api-reference")}>
        View API Reference
      </button>
    </nav>
  );
}

2.2. Level 2: Typed Pages, Layouts & Server Functions

Type your components and data fetching functions by passing the route template string as a generic:

import type { PageProps, LayoutProps, RouteParams } from "dinou";

// 1. src/blog/[slug]/page.tsx
export default function BlogPost({ params }: PageProps<"/blog/[slug]">) {
  // ✅ params.slug is strongly typed as string
  return <h1>Post: {params.slug}</h1>;

  // ❌ TypeScript error: Property 'id' does not exist on type '{ slug: string }'
  // return <h1>{params.id}</h1>;
}

// 2. src/blog/[slug]/layout.tsx
export function BlogLayout({ children, params }: LayoutProps<"/blog/[slug]">) {
  // ✅ params.slug is strongly typed; children is typed as ReactNode
  return (
    <section>
      <header>Category: {params.slug}</header>
      <main>{children}</main>
    </section>
  );
}

// 3. src/blog/[slug]/page_functions.ts
export async function getProps(params: RouteParams<"/blog/[slug]">) {
  // ✅ Directly types the raw params object without params: any
  return {
    postTitle: `Post: ${params.slug}`,
  };
}

2.3. Quick Reference: Core Types

Exported TypePrimary PurposeCanonical Example
PageProps<T>React Page components (page.tsx)Page({ params }: PageProps<"/blog/[slug]">)
LayoutProps<T>React Layout components (layout.tsx)Layout({ children, params }: LayoutProps<"/blog/[slug]">)
ErrorProps<T>React Error Page components (error.tsx)Error({ error, params }: ErrorProps<"/blog/[slug]">)
RouteParams<T>Pure functions, data loaders & services (getProps, API helpers)getProps(params: RouteParams<"/blog/[slug]">)
DinouRouteURL props, <Link>, router.push() & custom navigation componentsinterface NavProps { href: DinouRoute }

3. Parameter Mapping Reference Table

Dinou deterministically transforms your file-system folder naming conventions into strict TypeScript definitions:

Filesystem ConventionInferred Parameter TypeRoute ExampleType of params
Static RouteNo parameterssrc/about/page.tsxRecord<string, never>
Single Dynamic [id]stringsrc/blog/[id]/page.tsx{ id: string }
Catch-All [...slug]string[]src/docs/[...slug]/page.tsx{ slug: string[] }
Optional Dynamic [[lang]]string | undefinedsrc/[[lang]]/page.tsx{ lang?: string }
Optional Catch-All [[...opt]]string[] | undefinedsrc/shop/[[...opt]]/page.tsx{ opt?: string[] }
Multiple Dynamic SegmentsCombined by segmentsrc/users/[uId]/posts/[pId]/page.tsx{ uId: string; pId: string }
Nested OptionalsPositional dependentsrc/inv/[[wh]]/[[aisle]]/page.tsx{ wh?: string; aisle?: string }

[!WARNING] Be cautious with trailing slashes in dynamic links!

In TypeScript, template literal types evaluate ${string} as matching any string, including the empty string "" (because "" extends string evaluates to true).

The scenario: Consider a nested route such as src/posts/[id]/[slug]/page.tsx, whose generated template literal type is `/posts/${string | number}/${string | number}`.

If a developer accidentally leaves off the second parameter but appends a trailing slash:

<Link href={`/posts/${id}/`}>View</Link>

TypeScript matches the first parameter with id, the slash with literal /, and the second parameter with empty string "". However, at runtime Dinou normalizes trailing slashes, requesting /posts/123 (which results in a 404 Not Found because no page.tsx exists at the parent segment).

Best Practice: Always avoid trailing slashes when building dynamic URLs. Written without the trailing slash (`/posts/${id}`), TypeScript accurately flags the missing parameter at compile-time.


4. Advanced Usage & Real-World Recipes

4.1. IDE Autocomplete Workflow (Ctrl + Space)

When typing a component, type PageProps<""> and press Ctrl + Space (or Cmd + Space on macOS). Your editor will list every registered route pattern in the project:

import type { PageProps } from "dinou";

// Trigger Ctrl + Space inside "" to pick from project routes
export default function BlogPost({ params }: PageProps<"/blog/[slug]">) {
  return <h1>{params.slug}</h1>;
}

4.2. Typed Error Pages with ErrorProps

Error boundaries in Dinou v7 strongly type both the captured error and the current route's parameters:

"use client";

import { useRouter, type ErrorProps } from "dinou";

export default function BlogPostError({ params, error }: ErrorProps<"/blog/[slug]">) {
  const router = useRouter();

  return (
    <div className="error-card p-6 border rounded-xl bg-destructive/10">
      <h2>Failed to load post: {params.slug}</h2>
      <p className="text-sm text-muted-foreground">{error.message}</p>
      {/* Soft reload via Dinou client router */}
      <button onClick={() => router.refresh()}>Try Again</button>
    </div>
  );
}

4.3. Custom Navigation Components with DinouRoute

When creating design-system components (e.g., custom buttons, sidebar items, dropdown menus), use the exported DinouRoute union type to pass through autocomplete and validation:

import type { DinouRoute } from "dinou";
import { Link } from "dinou";

interface NavButtonProps {
  href: DinouRoute; // 👈 Inherits full autocompletion and route validation
  label: string;
}

export function NavButton({ href, label }: NavButtonProps) {
  return (
    <Link href={href} className="px-3 py-1.5 rounded-lg bg-blue-600 text-white font-medium">
      {label}
    </Link>
  );
}

// In your application code:
const menu: { label: string; href: DinouRoute }[] = [
  { label: "Overview", href: "/docs" },              // ✅ Autocompleted
  { label: "Routing", href: "/docs/routing" },        // ✅ Autocompleted
  // { label: "Broken", href: "/non-existent-page" }, // ❌ TypeScript compile error!
];

4.4. Dynamic Navigation with Typed Interpolation

Dinou types template literal strings against dynamic route definitions, catching broken template strings while accepting valid parameters:

import { Link, useRouter } from "dinou";

interface Product {
  id: string;
  name: string;
}

export function ProductCard({ product }: { product: Product }) {
  const router = useRouter();

  return (
    <div className="card p-4 border rounded-xl">
      {/* String interpolation verified against `/products/${string | number}` */}
      <Link href={`/products/${product.id}`}>{product.name}</Link>

      <button onClick={() => router.push(`/products/${product.id}`)}>
        View Details
      </button>
    </div>
  );
}

5. Technical Architecture: Under the Hood

Dinou coordinates compilation, route scanning, and TypeScript declarations through a zero-dependency architecture:

  1. Filesystem src/ Directory: Synchronous read and structural crawl of all page and layout files.
  2. Scanner (generate-route-types): Low-overhead in-memory scanner completes in under 2ms.
  3. Type Emission (.dinou/types/routes.d.ts): Emits ambient module declarations extending the "dinou" package interface with exact union types (DinouRoute) and parameter maps (DinouRouteParams).
  4. IDE & TypeScript Consumption: VS Code, Cursor, and the TypeScript compiler immediately read the declarations via tsconfig.json without requiring editor restarts or language server reboots.

6. Lifecycle: Dev Server & Production Builds

How Dinou keeps route typings strictly synchronized across local development and deployment pipelines:

  • 6.1. Local Dev (Hot Watcher): During npm run dev, Dinou arms a low-overhead filesystem watcher on src/. Whenever a page.(tsx|jsx|ts|js) file is added, renamed, or deleted, .dinou/types/routes.d.ts regenerates in under 2ms.
  • 6.2. Production Builds: Running npm run build runs route type generation as step 0 before bundling. If code references a non-existent route, TypeScript type checking fails the build before deploying.
  • 6.3. CI / Type Checkers: Run npx tsc --noEmit in CI pipelines to validate link integrity across the entire project deterministically.

7. Backward Compatibility Guarantee

  • 100% Backward Compatible: Existing components without explicit generic parameters continue to work without modification. You can continue using export default function Page({ params }: any) or generic PageProps during progressive migrations.
  • Graceful Fallback Mode: If .dinou/types/routes.d.ts has not yet been generated (e.g., fresh clone prior to npm run dev or npm run build), types gracefully fallback to permissive strings (string & {}), ensuring clean git checkout and initial install steps without blocking developer workflows.

Component Architecture: Server and Client Components

Dinou is built natively on the React 19 Server Components (RSC) specification. Rather than treating your entire application as a single monolithic client-side bundle, Dinou partitions components into two distinct execution environments:

  • Server Components (RSC): The default component type in Dinou. Rendered to HTML on the server (SSR) with zero client JavaScript. Never hydrated in the browser, eliminating bundle overhead while providing direct access to databases, the filesystem, and private secrets.
  • Client Components: Opt-in with "use client". Also pre-rendered to HTML on the server (SSR) for instant first paint, and then hydrated with interactive JavaScript in the browser to enable local state (useState, useActionState), event listeners (onClick), and browser APIs.

1. Overview & Dual-Component Model

Dinou's dual-component model allows you to seamlessly mix server-rendered backbones with interactive client islands:

FeatureServer Components (RSC)Client Components
Default in Dinou?Yes (all components inside src/)No (requires explicit "use client")
Execution EnvironmentServer only (Node.js, Bun, Deno, Cloudflare Workers)Server (pre-rendering SSR) + Browser (hydration)
Client JS Bundle Impact0 KB (Zero client JavaScript overhead)Ships component JS code to the browser
Backend & Secret AccessDirect access to DB, ORM, filesystem, private env varsCannot access backend resources or private secrets
Interactivity & StatePure static output; no state or DOM event listenersFull React state, hooks, effects, and DOM events
Data Fetching PatternDirect top-level await inside component bodyProps passed from RSC, React 19 use(), or Server Functions

1.1. The RSC Mental Model

Think of Server Components as the backbone of your application that orchestrates data fetching, layouts, and non-interactive UI (rendering pure HTML with zero client JavaScript). Client Components act as interactive "islands" or leaves embedded inside the server tree.

The server streams HTML and React Flight JSON representations across an open HTTP connection. The browser consumes this stream without ever downloading the JavaScript source code of your Server Components.


2. Server Components (RSC)

In Dinou, every component inside src/ is a Server Component by default unless explicitly marked with "use client". They execute exclusively on the server and never ship their implementation code to the browser.

2.1. Capabilities & Backend Access

  • Direct Async Execution: Server Components can be async functions. You can await database queries, ORM calls, microservices, or files directly inside the component body.
  • Zero Client Bundle Impact: Large dependencies imported into Server Components (e.g., Markdown parsers, SQL drivers, encryption packages) are never downloaded or executed by the browser.
  • Secure Secret Access: Database credentials, private API keys, and environment variables stay strictly on the server without risk of client exposure.
  • Automatic Streaming with <Suspense>: When slow asynchronous operations are wrapped in React <Suspense>, the layout shell and fallback UI can be delivered immediately without waiting for slow queries, while data streams progressively into the HTTP response.
// src/users/page.tsx (Server Component by default)
import { getContext } from "dinou";
import { db } from "@/lib/db"; // Never bundled to the client!

export default async function UsersPage() {
  // Direct backend query without an intermediary REST API
  const users = await db.user.findMany({ take: 10 });

  return (
    <main className="p-8 max-w-4xl mx-auto">
      <h1 className="text-2xl font-bold">Registered Users</h1>
      <ul className="mt-4 divide-y">
        {users.map((user) => (
          <li key={user.id} className="py-2 flex justify-between">
            <span>{user.name}</span>
            <span className="text-xs font-mono text-muted-foreground">{user.email}</span>
          </li>
        ))}
      </ul>
    </main>
  );
}

[!NOTE] TTFB & Execution Timing Note:

  • Static by Default (SSG): In Dinou, this page is pre-rendered at build time by default. The database query runs during npm run build, serving static HTML to visitors with instant TTFB (0ms server wait).
  • Dynamic Routes: If the route is marked dynamic (e.g. revalidate = 0 in page_functions.ts or reading cookies), a top-level await blocks the entire HTTP response until the query finishes. To preserve instant TTFB on dynamic routes, delegate the await to a child component wrapped in React <Suspense>.
Non-Blocking Streaming with React <Suspense>

By delegating asynchronous queries to child components wrapped in <Suspense>, the outer layout and fallback UI are sent immediately:

// src/users/page.tsx - Non-blocking streaming layout
import { Suspense } from "react";
import { db } from "@/lib/db";
import { UsersSkeleton } from "./skeleton";

export default function UsersPage() {
  return (
    <main className="p-8 max-w-4xl mx-auto">
      {/* 1. The outer shell is sent immediately (instant TTFB) */}
      <h1 className="text-2xl font-bold">Registered Users</h1>

      {/* 2. Displays fallback while the child query resolves */}
      <Suspense fallback={<UsersSkeleton />}>
        <UsersList />
      </Suspense>
    </main>
  );
}

// Child component handles the async query independently
async function UsersList() {
  const users = await db.user.findMany({ take: 10 });
  return (
    <ul className="mt-4 divide-y">
      {users.map((u) => (
        <li key={u.id} className="py-2 flex justify-between">
          <span>{u.name}</span>
          <span className="text-xs font-mono text-muted-foreground">{u.email}</span>
        </li>
      ))}
    </ul>
  );
}
How <Suspense> Behaves Across Rendering Strategies
StrategyCompiler & Runtime BehaviorFallback Skeleton BehaviorTrade-off
Pure SSG (Static)The compiler automatically awaits all Suspense boundaries during npm run build and writes the resolved HTML directly into the static artifact.Visitors never see the loading skeleton because the fallback is eliminated once the build settles.0ms TTFB from CDN edge, but data reflects build-time state (stale until next build/revalidation).
Pure Dynamic SSRThe server flushes the layout shell and <Suspense fallback={<Skeleton />}> immediately over the open HTTP connection.The user sees the skeleton while waiting, and React streams the resolved HTML chunk when the query finishes.100% fresh data and non-blocking layout, but incurs continuous server compute costs, DB load, and request-time latency.
Partial Prerendering (PPR)The outer layout shell and the fallback skeleton are pre-rendered at build time and served immediately with 0ms TTFB from the CDN edge.The skeleton renders instantly from static cache, while the server concurrently executes only the dynamic query and streams live data into the hole.Combines the instant delivery of SSG with the fresh, live data of dynamic SSR at minimal server compute cost.

2.2. Constraints & Limitations

Because Server Components do not execute in the browser, they cannot use browser-specific or client-interactive features:

  • Cannot use state or lifecycle hooks: useState, useEffect, useReducer, useLayoutEffect.
  • Cannot attach interactive client DOM event listeners: onClick, onChange, onSubmit, onKeyDown.
  • Cannot access browser globals: window, document, localStorage, navigator, geolocation.
  • Cannot consume (read) React Context directly: Server Components cannot call useContext() or use(Context). (While Server Components can render <Context> providers for client children, they cannot read context values themselves).

3. Client Components

Client Components provide rich, real-time client-side interactivity. They enable event handlers, local component state, animations, and access to browser APIs.

3.1. Interactivity & Browser APIs

Client Components have complete access to the client-side React ecosystem:

  • Full state management with useState, useReducer, and React 19 useActionState.
  • Side effects, timers, and DOM subscriptions via useEffect and useLayoutEffect.
  • DOM event handling (clicks, hovers, drags, keystrokes, form submissions).
  • Browser Web APIs (localStorage, Geolocation, IntersectionObserver, Audio).
  • Consuming React Context providers created on the client.

3.2. Server Pre-Rendering Notice

A common point of confusion is assuming Client Components execute exclusively in the browser. In Dinou, Client Components are pre-rendered on the server to HTML during initial page load to ensure instant First Contentful Paint (FCP) and optimal SEO indexing before hydrating in the browser.

[!WARNING] SSR Execution Safety Rule: Because Client Components run on the server during pre-rendering, accessing browser globals directly in the root of the component function (e.g. const token = localStorage.getItem("token")) will throw a server runtime error.

Always defer browser-only API access inside useEffect or guard it with an environment check:

if (typeof window !== "undefined") {
  const token = localStorage.getItem("auth_token");
}

4. The "use client" Directive

To declare a component as a Client Component, add the "use client" directive at the very top of the file, before any import statements or comments.

4.1. Establishing Module Boundaries

The directive is not a runtime function call—it is an instruction to the bundler (esbuild, Rollup, Webpack) that defines the network split point:

"use client"; // Must be at the very top of the file!

import { useState } from "react";

export function Counter({ initial = 0 }: { initial?: number }) {
  const [count, setCount] = useState(initial);

  return (
    <button
      onClick={() => setCount((c) => c + 1)}
      className="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium"
    >
      Count: {count}
    </button>
  );
}

4.2. Import Cascading Behavior

Once a file declares "use client", all components, utility modules, and third-party libraries imported by that file are automatically treated as part of the client bundle. You do not need to add "use client" to child modules imported by a Client Component.


5. Data Serialization Boundary

When passing props from a Server Component to a Client Component, data crosses the network boundary (serialized as React Flight JSON). Props passed across this boundary must be serializable.

5.1. Supported Data Types

  • Serializable (Allowed across boundary):
    • Primitives: string, number, boolean, null, undefined, bigint.
    • Plain Objects & Arrays: Objects with serializable values (e.g. { id: 1, name: "Dinou" }).
    • Dates: Date objects are serialized and reconstructed by React on the client.
    • React Elements (JSX): Server Components passed as children or named slot props.
    • Promises: Passed directly from Server to Client (read with React 19's native use()).
    • Server Functions: Bound action functions declared with "use server".
  • Non-Serializable (Blocked across boundary):
    • Client Functions: Cannot pass client callbacks (e.g. onClick={() => ...}) from Server Components.
    • Class Instances: Complex prototypes, database connections, and ORM class instances (e.g. Prisma models with methods) will lose their prototype.
    • Symbols & WeakMaps: Non-enumerable or memory-bound JavaScript data structures.
    • Browser DOM Elements: Native HTML DOM nodes (window, document.body).

5.2. Passing Promises & React 19 use()

React 19 supports passing an unresolved Promise as a prop from a Server Component to a Client Component. The Client Component reads the promise synchronously using the use() hook, automatically triggering Suspense fallbacks until the promise settles:

// Server Component
import { Suspense } from "react";
import { UserProfile } from "./user-profile"; // "use client"

export default function Page() {
  // Start fetching without awaiting!
  const userPromise = fetchUserData();

  return (
    <Suspense fallback={<div>Loading profile skeleton...</div>}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}
// Client Component (user-profile.tsx)
"use client";
import { use } from "react";

export function UserProfile({ userPromise }: { userPromise: Promise<{ name: string; role: string }> }) {
  // Unwraps the promise value seamlessly inside Suspense without useEffect
  const user = use(userPromise);
  return <h2>Hello, {user.name} ({user.role})</h2>;
}

6. Architectural Patterns & Best Practices

Dinou empowers you to choose the exact architectural balance between server performance and client ergonomics. Follow these battle-tested patterns for optimal architecture:

Keep your layouts, route pages, and static wrappers as Server Components. Only push the "use client" boundary down to small, interactive "leaf" components (such as an upvote button, search box, or dropdown menu).

src/dashboard/layout.tsx              → Server Component (zero client JS)
src/dashboard/page.tsx                → Server Component (fetches data directly)
src/dashboard/_components/search-bar.tsx  → "use client" (interactive leaf)
src/dashboard/_components/action-button.tsx  → "use client" (interactive leaf)

6.2. Composition: Server Components as Children

A Client Component cannot directly import a Server Component (because bundling a Server Component into the client module would defeat the RSC boundary). However, a Client Component can receive Server Components as children or via named JSX props!

// Client Component: ModalContainer.tsx
"use client";
import { useState, type ReactNode } from "react";

export function ModalContainer({ children }: { children: ReactNode }) {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>Open Modal</button>
      {isOpen && (
        <div className="modal-backdrop">
          {/* children is rendered on the server and passed as pre-rendered JSX! */}
          {children}
        </div>
      )}
    </div>
  );
}

// Server Component: page.tsx
import { ModalContainer } from "./ModalContainer";
import { HeavyServerProfile } from "./HeavyServerProfile"; // Server Component

export default function Page() {
  return (
    <ModalContainer>
      {/* HeavyServerProfile remains 100% on the server! */}
      <HeavyServerProfile />
    </ModalContainer>
  );
}

6.3. Client-First with Server Data (getProps)

If you are building an interactive canvas, video editor, or complex SPA dashboard and prefer traditional React where you can use useState and onClick anywhere in the page without boundary splitting, use Dinou's Client-First Model:

// src/canvas/page_functions.ts - Runs securely on the server
import { db } from "@/lib/db";

export async function getProps(params: { id: string }) {
  const project = await db.projects.findById(params.id);
  return { project };
}

// src/canvas/page.tsx - Entire page is a Client Component
"use client";
import { useState } from "react";

export default function CanvasPage({ project }: { project: any }) {
  // Full access to state, effects, and events everywhere in the file!
  const [layers, setLayers] = useState(project.initialLayers);

  return (
    <div onClick={() => console.log("clicked")}>
      <Toolbar onAddLayer={(l) => setLayers([...layers, l])} />
      <Stage layers={layers} />
    </div>
  );
}

[!TIP] Advantage: Fast development speed and traditional React ergonomics, while preserving secure backend data fetching and SSR pre-rendering.

6.4. Granular Caching with <DinouCacheSlot> (Vertical Micro-ISR)

A common architectural trap in dynamic RSC applications is converting heavy server widgets into Client Components just to gain client-side memory caching (such as SWR or TanStack Query). In Dinou, <DinouCacheSlot> provides component-level server caching (Vertical Segmentation) directly in the RSC layer.

  • The Common Mistake (Demoting to Client): To avoid running an expensive 2-second database query on every dynamic page load, developers often convert the widget to "use client" with a client cache. This bloats the client JS bundle, loses direct database access, exposes internal API endpoints, and introduces client-side network waterfalls.
  • Dinou's Solution (<DinouCacheSlot>): Keep the component on the server! <DinouCacheSlot> caches the rendered RSC Flight payload of that specific subtree on the server for a designated TTL (e.g. 300s). The rest of the page remains 100% dynamic, while the expensive subtree serves in 0ms.
Can a <DinouCacheSlot> Contain Client Components?

Yes, absolutely. While <DinouCacheSlot> itself is an async Server Component that executes exclusively on the server, its children can freely render interactive Client Components. Dinou caches the Client Component reference along with its pre-computed props. The client hydrates it normally with full interactivity (events, state, animations):

// 1. Client Component: interactive-chart.tsx (Hydrates with 100% interactivity)
"use client";
import { useState } from "react";

export function InteractiveChart({ data }: { data: any }) {
  const [zoom, setZoom] = useState(1);
  return (
    <div className="p-4 border rounded-lg bg-card">
      <div className="flex justify-between items-center mb-2">
        <span className="text-sm font-bold">Metrics (Zoom: {zoom}x)</span>
        <button
          onClick={() => setZoom((z) => z + 1)}
          className="px-2 py-1 text-xs bg-primary text-primary-foreground rounded"
        >
          Zoom In
        </button>
      </div>
      <canvas data-zoom={zoom} />
    </div>
  );
}

// 2. Server Component: page.tsx (Dynamic page with an isolated cached slot)
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou";
import { db } from "@/lib/db";
import { UserGreeting } from "./UserGreeting"; // Reads user session/cookies
import { GreetingSkeleton, AnalyticsSkeleton } from "./skeletons";
import { InteractiveChart } from "./interactive-chart"; // Client Component

// Server Component performing the heavy query
async function HeavyAnalyticsWidget() {
  const analyticsData = await db.analytics.aggregateBigData(); // 2-second query
  return <InteractiveChart data={analyticsData} />;
}

export default function DashboardPage() {
  return (
    <main className="space-y-6 p-8">
      {/* Dynamic personal component: reads cookies/session without blocking initial layout delivery (preserves fast TTFB) */}
      <Suspense fallback={<GreetingSkeleton />}>
        <UserGreeting />
      </Suspense>

      {/* Micro-ISR hole: wrapped in Suspense so cache misses stream without blocking TTFB */}
      <Suspense fallback={<AnalyticsSkeleton />}>
        <DinouCacheSlot id="analytics-slot" duration={300} tags={["analytics"]}>
          <HeavyAnalyticsWidget />
        </DinouCacheSlot>
      </Suspense>
    </main>
  );
}
  • Why wrap in <Suspense>? While <DinouCacheSlot> works without it on cache hits, on a cache miss (cold start or expired TTL) an unwrapped slot would block the entire HTTP response during the heavy query. Wrapping it in <Suspense> guarantees instant TTFB by streaming the layout and skeleton immediately.
  • What about UserGreeting? In dynamic SSR, wrapping UserGreeting in <Suspense> ensures that reading user session or auth cookies does not delay the initial response, keeping TTFB fast. In PPR, it also prevents dynamic request data from de-optimizing the build-time static shell.
  • In-Place Client Refresh: Any Client Component can trigger an on-demand re-render of only this slot using refreshSlot("analytics-slot") from "dinou" without reloading the page or resetting state in other components.
  • On-Demand Invalidation: Server Functions and webhooks can invalidate the slot cache immediately via revalidateTag("analytics").

6.5. The Synergy: Partial Prerendering (PPR) + <DinouCacheSlot>

What happens when you nest <DinouCacheSlot> inside a React <Suspense> boundary on a page with Partial Prerendering (PPR)? You unlock the highest performance profile possible in full-stack web engineering: zero-delay initial delivery paired with near-zero streaming delay and 99.9% database query offloading.

MetricPPR Alone (Without Slot)PPR + <DinouCacheSlot>
ArchitectureStatic shell + on-demand dynamic executionInstant shell + cached streaming hole
TTFB (First Byte)0ms from CDN edge (instant outer layout)0ms from CDN edge (instant outer layout)
Skeleton DurationThe user waits the full query duration (e.g. 1,200ms) on every single request while the server executes the query.~5ms! Subsequent visitors receive the cached RSC stream chunk almost immediately; the skeleton barely blinks.
Database Load10,000 visitors per minute = 10,000 database queries executed on the server.10,000 visitors per minute with duration={60} = 1 database query per minute (99.99% reduction).
Implementation Example

Wrap the slow asynchronous Server Component with <DinouCacheSlot>, and then enclose both inside React <Suspense>:

// src/dashboard/page.tsx - High-traffic operational dashboard
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou";
import { db } from "@/lib/db";
import { MetricsSkeleton } from "./MetricsSkeleton";
import { InteractiveMetricsChart } from "./InteractiveMetricsChart"; // "use client"

// Heavy async Server Component
async function HeavyMetricsSubtree() {
  // Heavy aggregation across millions of database rows (e.g. 1.5 seconds)
  const aggregatedStats = await db.telemetry.aggregateOperations();
  return <InteractiveMetricsChart data={aggregatedStats} />;
}

export default function DashboardPage() {
  return (
    <main className="p-8 space-y-6 max-w-5xl mx-auto">
      {/* 1. PPR Static Shell: Delivered instantly with 0ms TTFB from edge CDN */}
      <header className="border-b pb-4">
        <h1 className="text-3xl font-extrabold tracking-tight">Operations Command Center</h1>
        <p className="text-sm text-muted-foreground">Real-time system telemetry and infrastructure health.</p>
      </header>

      {/* 2. PPR Dynamic Boundary + Cache Slot:
             - Fallback skeleton sent immediately in initial layout
             - Dynamic hole streams from server cache in ~5ms for subsequent users
             - Database query executes only ONCE every 60 seconds */}
      <Suspense fallback={<MetricsSkeleton />}>
        <DinouCacheSlot id="operations-telemetry" duration={60} tags={["telemetry"]}>
          <HeavyMetricsSubtree />
        </DinouCacheSlot>
      </Suspense>
    </main>
  );
}
  • Instant TTFB: Outer shell and fallback skeleton arrive at 0ms from CDN edge.
  • 99.9% Query Relief: Caches the dynamic stream chunk on the server during the TTL window.
  • Surgical Invalidation: Call revalidateTag("telemetry") or refreshSlot() to update without full-page reloads.

6.6. When Should You Use <DinouCacheSlot>?

A common question in full-stack architecture is: Does <DinouCacheSlot> only make sense for dynamic components or subtrees?

The short answer: While dynamic routes and PPR are where it delivers exponential returns, it also unlocks unique capabilities for static pages (such as asymmetric revalidation and in-place client slot refreshes). Understanding when to use it boils down to the distinction between Horizontal Segmentation (route/layout-level) and Vertical Segmentation (component-level).

When NOT to Use <DinouCacheSlot> (Redundant or Anti-Patterns)
  1. 100% Static Pages (SSG): If an entire route is pre-rendered at build time with no revalidation, it is already cached as immutable HTML at the CDN Edge. Wrapping parts of it in a slot adds zero caching benefit.
  2. Wrapping the Entire Page: Wrapping your entire page.tsx body inside a slot reinvents horizontal caching. Use export const revalidate = 300 in page_functions.ts instead.
  3. Unpartitioned Private Data: Wrapping personal user data (e.g. private user carts or session tokens) with a static slot ID will cache that private data for the first user and leak it to subsequent visitors.
  1. Heavy, Computationally Expensive Widgets in Dynamic / Authenticated Pages: Dynamic pages (reading cookies, auth tokens, or search params) cannot be cached at the CDN or route level because each user receives a customized response. However, many widgets on that page (stock tickers, trending feeds, heavy SQL analytics) are identical across users. <DinouCacheSlot> isolates the expensive widget with its own TTL:
// src/routes/dashboard/page.tsx (Dynamic authenticated SSR)
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou";
import { getSession } from "@/lib/auth";
import { GlobalMetricsWidget } from "./metrics";

export default async function DashboardPage() {
  const session = await getSession(); // Evaluated dynamically per user

  return (
    <div className="p-6">
      <h1>Welcome back, {session.user.name}</h1> {/* 100% Live & personal */}

      {/* Global widget: cached for 10 minutes on the server across all users */}
      <Suspense fallback={<div>Loading global telemetry...</div>}>
        <DinouCacheSlot id="global-metrics" duration={600} tags={["metrics"]}>
          <GlobalMetricsWidget />
        </DinouCacheSlot>
      </Suspense>
    </div>
  );
}
  1. Dynamic Holes in Partial Prerendering (PPR) — The Ultimate Synergy: In PPR, the static shell ships instantly from CDN (0ms TTFB), while dynamic holes stream in via React Suspense. Without a slot, 100,000 visitors trigger 100,000 database hits. Placing <DinouCacheSlot> inside the hole means only the first request queries the DB; the other 99,999 stream the cached RSC Flight payload in ~2-5ms.

  2. Static or ISR Pages with Asymmetric Lifecycles & Targeted Refreshes: This is where <DinouCacheSlot> proves it is not limited to dynamic pages. You can use it in static/ISR pages for two critical architectural advantages:

    • A. Asymmetric Revalidation Lifecycles: Suppose a product page has heavy media and specifications that rarely change (cached for 24 hours: revalidate = 86400). But live stock pricing fluctuates every 60 seconds. A slot lets the pricing widget expire every 60s without re-rendering the whole page!
    • B. Surgical In-Place Client Refresh (refreshSlot): A Client Component can trigger an on-demand re-render of only this Server Component using refreshSlot(slotId) without refreshing the browser page or losing client state (scroll position, inputs, audio):
// 1. In your Page (Server Component): Asymmetric slot inside static route
<DinouCacheSlot id={`price-${product.id}`} duration={60} tags={["pricing"]}>
  <LivePriceCalculator productId={product.id} />
</DinouCacheSlot>

// 2. In an interactive Client Component: refresh ONLY that slot
"use client";
import { refreshSlot } from "dinou";

export function RefreshPriceButton({ productId }: { productId: string }) {
  return (
    <button onClick={() => refreshSlot(`price-${productId}`, { fresh: true })}>
      Check Latest Stock & Price
    </button>
  );
}
Quick Decision Checklist
Question / RequirementRecommended ActionReason
Dynamic or PPR route with heavy queries?Wrap with <DinouCacheSlot> inside <Suspense>Protects database from repetitive execution and streams cached RSC Flight chunks in ~2-5ms.
Asymmetric expiration within a page?Use <DinouCacheSlot>Decouples widget TTL (e.g. 60s) from overall page TTL (e.g. 24h) without re-rendering the entire page.
Need surgical, targeted client refreshes?Use <DinouCacheSlot> + refreshSlot()Updates just that server subtree in-place without page reload or state loss.
Pure SSG with uniform expiration?Do NOT use <DinouCacheSlot>Let build-time SSG and CDN Edge caching handle the entire page with zero runtime overhead.

6.7. Common Traps & Anti-Patterns to Avoid

  • Trap 1: Placing "use client" at the root layout. Marking src/layout.tsx as a Client Component cascades the client boundary to every single page in your application. Root layouts should always be Server Components to protect secrets and enable streaming.
  • Trap 2: Direct Server Component imports in Client files. Never write import ServerComponent from "./ServerComponent" inside a file containing "use client". Use the Composition Pattern (Section 6.2) and pass it as children instead.
  • Trap 3: Accessing browser APIs in component body during SSR. Calling window.innerWidth or localStorage.getItem() outside of a useEffect in a Client Component causes server pre-rendering crashes. Always wrap browser calls in useEffect or an environment check (if (typeof window !== "undefined")).
  • Trap 4: Passing non-serializable objects across boundaries. Do not pass callback functions or ORM model class instances with methods as props to Client Components. Extract plain JavaScript objects or use Server Functions ("use server") for actions.
  • Trap 5: Fetching data in Client Components via useEffect on mount. In modern RSC architectures, fetching on mount via useEffect creates network waterfalls, causes layout shifts, and misses server streaming. Pass data from Server Components or use Strategy 9 (RSC Promise) / Strategy 12 (Server Function RPC).

Page & Layout Functions (page_functions.ts & layout_functions.ts)

In Dinou, every route segment can independently configure its data fetching, static pre-rendering, cache lifecycles, and parameter validation without cluttering component UI code.

  • page_functions.ts: Placed alongside page.tsx. Configures data fetching (getProps), static paths (getStaticPaths), cache strategy (revalidate), Partial Prerendering (ppr), cache tags (getCacheTags), and parameter validation (validateParams) specifically for that page.
  • layout_functions.ts: Placed alongside layout.tsx. Supplies data directly to the layout component (getProps) and manages segment-level cache strategy (revalidate, ppr) independently from child pages.

Overview & Horizontal Segmentation

Dinou strictly separates server-side data orchestration and cache configuration from presentation UI:

src/blog/
├── layout.tsx                # Presentation shell (RSC)
├── layout_functions.ts       # Layout cache strategy (revalidate = 300) & getProps
└── [slug]/
    ├── page.tsx              # Page presentation (RSC or "use client")
    └── page_functions.ts     # Page data fetching (getProps), paths, and validation

The Unified Architectural Contract: RouteSegmentConfig<TParams, TProps>

In Dinou, every server-side configuration file implements the exact same foundational contract: RouteSegmentConfig. This is the framework's underlying "abstract" interface defining all segment capabilities. For day-to-day application code, Dinou exports two concrete, role-based aliases: PageFunctions (for pages) and LayoutFunctions (for layouts):

// Exported directly from "dinou":

/**
 * Foundational "abstract" interface governing all route segments in Dinou.
 */
export interface RouteSegmentConfig<TParams = any, TProps = any> {
  // Partial Prerendering (PPR) configuration
  ppr?: boolean | (() => boolean | Promise<boolean>);

  // Cache & revalidation strategy (< 1 = Dynamic SSR, >= 1 = ISR, false = Static)
  revalidate?: number | false | (() => number | false | Promise<number | false>);

  // Cache tags for on-demand invalidation via revalidateTag(tag)
  getCacheTags?: string[] | ((params?: TParams) => string[] | Promise<string[]>);

  // Path generator for SSG (constant array or sync/async function)
  getStaticPaths?: StaticPathItem<TParams>[] | (() => StaticPathsResult<TParams>);

  // Server loader providing direct props to the component
  getProps?: (params?: TParams) => TProps | Promise<TProps>;

  // Parameter validator guard (returns false to halt and return 404)
  validateParams?: (params?: TParams) => boolean | Promise<boolean>;

  // Controls whether Incremental Static Generation (ISG) is allowed for ungenerated paths
  allowISG?: boolean | (() => boolean | Promise<boolean>);
}

/**
 * Concrete semantic alias to import and use in 'page_functions.ts'
 */
export type PageFunctions<TParams = any, TProps = any> = RouteSegmentConfig<TParams, TProps>;

/**
 * Concrete semantic alias to import and use in 'layout_functions.ts'
 */
export type LayoutFunctions<TParams = any, TProps = any> = RouteSegmentConfig<TParams, TProps>;
Where to useType to importPurpose
src/.../page_functions.tsPageFunctionsTypes data loading (getProps), static paths, and cache for the page.
src/.../layout_functions.tsLayoutFunctionsTypes data loading (getProps) and independent cache for the layout shell.
Plugins & Shared UtilsRouteSegmentConfigBase "abstract" interface for generic utilities that process arbitrary route segments.
3 Architectural Reasons for Separation
  1. Semantic Intent: Seeing import type { PageFunctions } in a page file immediately clarifies intent and self-documents that the functions belong to the page tier.
  2. Mental Separation: Reinforces Dinou's horizontal segment isolation: layouts have independent cache lifetimes and props from child pages.
  3. Future Extensibility: If future Dinou versions introduce layout-only properties (e.g. scroll boundaries) or page-only hooks, your application code remains completely decoupled and non-breaking.

1. getProps (Direct Component Props)

Use getProps to load data on the server based on route params and pass it directly to your component.

[!NOTE] v7 Direct Return Contract: In Dinou v7, getProps directly returns the props object ({ ... }). The legacy wrapper { page: { ... }, layout: { ... } } has been deprecated because pages and layouts now have their own dedicated configuration files.

  • Arguments: params?: TParams (The normalized dynamic route parameters)
  • Returns: TProps | Promise<TProps> (Injected directly as the component's props)

Example: Type-Safe Page Component Loader

// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db, type Post } from "@/lib/db";

export interface BlogPostData {
  post: Post;
}

// Best Practice: Type getProps using PageFunctions for strict parameters & return contract
export const getProps: PageFunctions<RouteParams<"/blog/[slug]">, BlogPostData>["getProps"] = async (params) => {
  const post = await db.getPost(params?.slug);
  // Directly returns BlogPostData for page.tsx
  return { post };
};
// src/blog/[slug]/page.tsx
import type { PageProps } from "dinou";
import type { BlogPostData } from "./page_functions";

// Best Practice: Extend PageProps with the loader's returned BlogPostData
interface BlogPostProps extends PageProps<"/blog/[slug]">, BlogPostData {}

export default function BlogPost({ params, post }: BlogPostProps) {
  return (
    <article>
      <span className="text-xs text-muted-foreground">Route: /blog/{params.slug}</span>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}

Example: Type-Safe Layout Component Loader

// src/blog/layout_functions.ts
import type { LayoutFunctions } from "dinou";
import { db } from "@/lib/db";

export interface BlogLayoutData {
  categories: string[];
}

// Best Practice: Type layout server functions with LayoutFunctions
export const getProps: LayoutFunctions<any, BlogLayoutData>["getProps"] = async () => {
  const categories = await db.getCategories();
  // Directly returns props for layout.tsx
  return { categories };
};
// src/blog/layout.tsx
import type { LayoutProps } from "dinou";
import type { BlogLayoutData } from "./layout_functions";

// Best Practice: Extend LayoutProps for automatic typing of children and params
interface BlogLayoutProps extends LayoutProps, BlogLayoutData {}

export default function BlogLayout({
  children,
  categories,
}: BlogLayoutProps) {
  return (
    <div className="blog-shell">
      <aside className="blog-sidebar">
        <h3>Categories</h3>
        <ul>
          {categories.map((c) => (
            <li key={c}>{c}</li>
          ))}
        </ul>
      </aside>
      <main>{children}</main>
    </div>
  );
}

Deep-Dive Architecture: Why getProps Only Receives params & The Dynamic Bailout Mechanism

Resource Identity & Cache Integrity

Route params (e.g. /blog/[slug]) identify the resource, not the visitor. Because the content for a given route is identical for everyone, Dinou can run getProps in advance—at build time (SSG) or on first request (ISR)—and safely pre-render and cache the HTML (in static storage, server memory, reverse proxies, or CDNs).

In contrast, cookies, headers, and query params belong to a specific HTTP connection. If getProps relied on them, the output could contain private user data, making shared caching impossible and forcing the server to render every request on demand.

The Dynamic Bailout Mechanism

If you call getContext() inside getProps to read request-time connection data:

Property AccessedBehaviorWhy?
context.request.headers❌ Build Error (SSG) / Dynamic Bailout (SSR)Headers vary per client request. Accessing them invalidates shared static caching.
context.request.cookies❌ Build Error (SSG) / Dynamic Bailout (SSR)Cookies are user-specific (sessions, auth). Cannot be baked into shared HTML.
context.searchParams❌ Build Error (SSG) / Dynamic Bailout (SSR)Query strings vary infinitely (?ref=..., ?utm_source=...). Cannot be pre-rendered at build time.
context.params✅ Allowed Everywhere (SSG, ISR, SSR, PPR)Identifies the route resource ([slug]). Pure and deterministically cacheable.

To keep pages pre-renderable (SSG/ISR) and instant to load, organize your data into its natural layer: route data in getProps, session cookies inside <Suspense>, and query parameters via the universal useSearchParams() hook:

// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db, type Post } from "@/lib/db";

export interface BlogPostData {
  post: Post;
}

// 1. Pure route loader: depends solely on params
// Cached statically for all visitors (SSG / ISR)
export const getProps: PageFunctions<RouteParams<"/blog/[slug]">, BlogPostData>["getProps"] = async (params) => {
  const post = await db.getPost(params?.slug);
  return { post };
};
// src/blog/[slug]/page.tsx
import { Suspense } from "react";
import type { PageProps } from "dinou";
import type { BlogPostData } from "./page_functions";
import { UserSession } from "./user-session";
import { RelatedPosts } from "./related-posts";
import { SearchFilter } from "./search-filter";

interface BlogPostProps extends PageProps<"/blog/[slug]">, BlogPostData {}

export default function BlogPostPage({ params, post }: BlogPostProps) {
  return (
    <article className="post-container">
      {/* 1. Instant static shell: delivered immediately from cache */}
      <header>
        <span className="text-xs text-muted-foreground">Route: /blog/{params.slug}</span>
        <h1>{post.title}</h1>
        {/* Client Component using useSearchParams() for instant input sync */}
        <SearchFilter />
      </header>

      <div className="post-body">{post.content}</div>

      {/* 2. Dynamic cookie component streamed via Suspense */}
      <Suspense fallback={<div className="text-xs text-muted-foreground">Loading session...</div>}>
        <UserSession />
      </Suspense>

      {/* 3. Server Component using useSearchParams() streamed via Suspense */}
      <Suspense fallback={<div className="text-xs text-muted-foreground">Filtering articles...</div>}>
        <RelatedPosts />
      </Suspense>
    </article>
  );
}
// src/blog/[slug]/user-session.tsx (Server Component)
import { getContext } from "dinou";
import { auth } from "@/lib/auth";

// Cookie & auth access isolated inside Suspense:
// Streaming this component prevents getProps from bailing out to Dynamic SSR!
export async function UserSession() {
  const { req } = getContext();
  const sessionToken = req?.cookies?.session;
  const user = sessionToken ? await auth.verify(sessionToken) : null;

  if (!user) {
    return <a href="/login" className="text-xs font-semibold">Sign In</a>;
  }

  return (
    <aside className="user-badge">
      <span>Welcome back, <strong>{user.name}</strong>!</span>
    </aside>
  );
}
// src/blog/[slug]/related-posts.tsx (Server Component)
import { useSearchParams } from "dinou";
import { db } from "@/lib/db";

// Universal hook in a Server Component:
// Reads ctx.req.query to fetch filtered data from the DB on the server.
// Because it reads query, wrap it in <Suspense> in page.tsx so the post shell stays static!
export async function RelatedPosts() {
  const searchParams = useSearchParams();
  const tag = searchParams.get("tag") ?? "featured";
  const articles = await db.getPostsByTag(tag);

  return (
    <section className="related-posts">
      <h3>Articles matching "?tag={tag}"</h3>
      <ul>
        {articles.map((item) => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </section>
  );
}
// src/blog/[slug]/search-filter.tsx (Client Component)
"use client";

import { useSearchParams } from "dinou";

// Universal hook in a Client Component:
// Reads URL params on the browser for reactive input state without contacting the server
export function SearchFilter() {
  const searchParams = useSearchParams();
  const currentTag = searchParams.get("tag") ?? "";

  return (
    <input
      type="search"
      defaultValue={currentTag}
      placeholder="Filter by tag (?tag=...)..."
      className="text-xs border rounded px-2 py-1"
    />
  );
}
Data LayerHandled InLifecycle & Strategy
1. Route Data (Params)getProps in page_functions.tsPure and cacheable for all visitors (SSG / ISR).
2. Cookies & HeadersServer Components wrapped in <Suspense>Streams dynamically without bailing out the static page shell.
3. Universal useSearchParams()Server or Client Components with <Suspense>Works in Server Components (streamed) and Client Components (instant UI).

2. getStaticPaths (Static Generation)

Defines which dynamic parameter combinations should be pre-rendered at build time (SSG). By default, any dynamic path not returned is generated on-demand when first requested and cached statically for all subsequent visitors (Incremental Static Generation / ISG), unless disabled via allowISG: false (which strictly restricts routes to the pre-rendered list and returns 404 for any other path).

2.1. Constant Array vs Function Export

In Dinou v7, getStaticPaths supports both a constant array export (for known, fixed paths) and a synchronous or asynchronous function export (for dynamic data sources like databases or headless CMSs):

// 1. Constant Array Export (Zero runtime overhead, ideal for fixed routes or locales)
// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";

export const getStaticPaths: PageFunctions<RouteParams<"/blog/[slug]">]["getStaticPaths"] = [
  "welcome",
  "getting-started",
  "release-notes",
];
// 2. Async Function Export (Queries databases or headless CMSs at build time)
// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db } from "@/lib/db";

export const getStaticPaths: PageFunctions<RouteParams<"/blog/[slug]">]["getStaticPaths"] = async () => {
  const posts = await db.post.findMany({ select: { slug: true } });
  return posts.map((p) => p.slug);
};

2.2. Return Formats

Dinou accommodates simple, catch-all, and multi-segment architectures with flexible item formats:

Route TypeAccepted FormatExample ReturnPre-rendered URLs
Simple ([id] or [slug])string[]number[]["foo", "bar"] or [1, 2, 3]
Optional Simple ([[id]])`(stringnumberundefined)[]`
Catch-all ([...slug])string[][][["a", "b"], ["c"]]/docs/a/b, /docs/c
Optional Catch-all ([[...slug]])`(string[]undefined)[]`[undefined, ["a"], ["a", "b"]]
Multi-Segment / Nested ([cat]/[id])`Record<string, stringstring[]>[]`[{ cat: "shoes", id: "100" }]

2.3. Simple Dynamic Routes

Simple dynamic routes match a single URL segment. Dinou supports both required segments ([id]) and optional segments ([[category]]):

// src/blog/[id]/page_functions.ts (Required segment [id])
import type { PageFunctions, RouteParams } from "dinou";

export const getStaticPaths: PageFunctions<RouteParams<"/blog/[id]">]["getStaticPaths"] = [
  "1",
  "2",
  "3",
]; // Generates: /blog/1, /blog/2, /blog/3
// src/shop/[[category]]/page_functions.ts (Optional segment [[category]])
import type { PageFunctions, RouteParams } from "dinou";

// Pass undefined to pre-render the root route (/shop) without a parameter
export const getStaticPaths: PageFunctions<RouteParams<"/shop/[[category]]">]["getStaticPaths"] = [
  undefined, // Generates: /shop
  "shoes",   // Generates: /shop/shoes
  "shirts",  // Generates: /shop/shirts
];

2.4. Catch-all Routes

Catch-all routes match multiple dynamic path segments. Dinou supports both standard catch-alls ([...slug]) and optional catch-alls ([[...slug]]):

// src/docs/[...slug]/page_functions.ts (Required catch-all [...slug])
import type { PageFunctions, RouteParams } from "dinou";

export const getStaticPaths: PageFunctions<RouteParams<"/docs/[...slug]">]["getStaticPaths"] = [
  ["intro"],              // Generates: /docs/intro
  ["api", "v1", "auth"],  // Generates: /docs/api/v1/auth
];
// src/docs/[[...slug]]/page_functions.ts (Optional catch-all [[...slug]])
import type { PageFunctions, RouteParams } from "dinou";

export const getStaticPaths: PageFunctions<RouteParams<"/docs/[[...slug]]">]["getStaticPaths"] = [
  undefined,              // Generates: /docs (root)
  ["intro"],              // Generates: /docs/intro
  ["api", "v1", "auth"],  // Generates: /docs/api/v1/auth
];

2.5. Automatic Route Propagation (Recursion)

Static parameters automatically propagate downwards through the directory tree. If you define values for a segment, Dinou will recursively generate all nested static sub-pages under those values.

src/blog/[slug]/
├── page.tsx          # Page: /blog/[slug]
├── page_functions.ts # Returns ["post-a", "post-b"]
└── details/
    └── page.tsx      # Nested static page

If getStaticPaths in blog/[slug] returns ["post-a", "post-b"], Dinou generates 4 static pages:

  1. /blog/post-a
  2. /blog/post-a/details
  3. /blog/post-b
  4. /blog/post-b/details

2.6. Nested Pages & The "Chain of Responsibility"

When nesting routes where both the parent and the child segment have their own page.tsx, static generation follows a strict top-down "chain of responsibility":

src/shop/[category]/
├── page.tsx              # Parent page: /shop/[category]
├── page_functions.ts     # Parent gatekeeper
└── [id]/
    ├── page.tsx          # Child page: /shop/[category]/[id]
    └── page_functions.ts # Child generator

Because the parent folder contains a page.tsx, Dinou must generate valid parent pages first before it can traverse down to the child level. The parent acts as a mandatory gatekeeper:

  1. If parent returns []: Dinou halts at the parent level and never invokes the child [id]/page_functions.ts. No product pages are generated.
  2. When parent resolves its categories: It unlocks the child level:
// 1. Parent: src/shop/[category]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";

export const getStaticPaths: PageFunctions<RouteParams<"/shop/[category]">]["getStaticPaths"] = [
  "shoes",
  "shirts",
];

// 2. Child: src/shop/[category]/[id]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";

export const getStaticPaths: PageFunctions<RouteParams<"/shop/[category]/[id]">]["getStaticPaths"] = [
  "100",
  "200",
];

// Automatically generates 4 static pages:
// /shop/shoes/100, /shop/shoes/200
// /shop/shirts/100, /shop/shirts/200

[!TIP] Rule of Thumb:

  • If an intermediate folder has its own page.tsx, define a page_functions.ts at that folder level with scalar arrays (string[]).
  • If an intermediate folder does not have a page.tsx, you can define all nested parameters together in the leaf page_functions.ts using an Object Array.

2.7. Nested & Pass-through Segments

When you have multiple dynamic segments in a path without intermediate pages (the intermediate folders do not have a page.tsx), the leaf segment returns an Object mapping all parameter names at once:

src/shop/[category]/[brand]/[...specs]/
└── page.tsx # Only leaf has a page.tsx
// src/shop/[category]/[brand]/[...specs]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";

export const getStaticPaths: PageFunctions<RouteParams<"/shop/[category]/[brand]/[...specs]">]["getStaticPaths"] = [
  {
    category: "electronics",
    brand: "apple",
    specs: ["m3", "16gb"], // → Pre-renders: /shop/electronics/apple/m3/16gb
  },
  {
    category: "clothing",
    brand: "nike",
    specs: ["running", "shoes", "size-10"], // → Pre-renders: /shop/clothing/nike/running/shoes/size-10
  },
];

2.8. Async Support (CMS & DB)

getStaticPaths can be an async function, allowing you to query external headless CMSs, databases, or REST APIs during the build:

// src/products/[id]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db } from "@/lib/db";

export const getStaticPaths: PageFunctions<RouteParams<"/products/[id]">]["getStaticPaths"] = async () => {
  // Queries your database at build time (npm run build)
  const products = await db.products.findMany({ select: { id: true } });
  return products.map((product) => String(product.id));
};

3. validateParams (Route Parameter Validation)

Validates dynamic parameters (such as [id] or [...slug]) on the server prior to rendering. If validateParams returns false (or a falsy value), Dinou immediately terminates request processing and issues a 404 Not Found response.

  • Arguments: params?: TParams (The normalized route parameters object)
  • Returns: boolean | Promise<boolean> (Return false to abort and render 404)
// src/products/[id]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";

type ProductParams = RouteParams<"/products/[id]">;

// 1. Synchronous validation: guarantees id is purely numeric
export const validateParams: PageFunctions<ProductParams>["validateParams"] = (params) => {
  return /^[0-9]+$/.test(params?.id ?? "");
};
// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db } from "@/lib/db";

type BlogParams = RouteParams<"/blog/[slug]">;

// 2. Asynchronous validation: verifies record existence before rendering
export const validateParams: PageFunctions<BlogParams>["validateParams"] = async (params) => {
  if (!params?.slug) return false;
  const exists = await db.posts.exists({ slug: params.slug });
  return exists;
};

[!NOTE] Execution Timing: validateParams executes before getProps. If validation fails, getProps will never run, preventing unnecessary database lookups.


4. allowISG (Incremental Static Opt-Out)

Controls whether dynamic paths that were not pre-rendered at build time (via getStaticPaths during dinou build) are allowed to be generated on-demand on the first incoming user request.

In Dinou v7, pre-rendering happens strictly during the build phase, ensuring production server startup is instantaneous.

  • Default (true): Any valid path not pre-generated at build time is generated on-demand when the first visitor requests it, and subsequently cached statically (Incremental Static Generation / ISG).
  • Opt-Out (false): Strictly restricts accessible routes to those explicitly returned by getStaticPaths. Any request for a path not pre-rendered immediately returns a 404 Not Found without invoking getProps or running SSR.
// src/docs/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";

// Strict SSG: only these two articles exist; all other /docs/[slug] return 404
export const getStaticPaths: PageFunctions<RouteParams<"/docs/[slug]">]["getStaticPaths"] = [
  "installation",
  "architecture",
];

// Disallows on-demand generation for unlisted slugs
export const allowISG: PageFunctions["allowISG"] = false;

5. revalidate (ISR & Dynamic SSR Escape)

The revalidate export is Dinou's single source of truth for route caching and rendering behavior. It is measured in seconds (s).

Revalidate ValueStrategyLifecycle & Behavior
revalidate < 1 (or 0)Dynamic SSREscapes static generation. The page is rendered dynamically on every incoming request with live database reads.
revalidate >= 1Incremental Static Regeneration (ISR)The page is cached statically for N seconds. Requests within the window receive instant cached HTML (0ms TTFB). After N seconds, a background regeneration is triggered.
revalidate = falsePure SSG (Default)The page is rendered once at build time and cached indefinitely until the next build or manual purge.
// src/metrics/page_functions.ts - Real-time dynamic SSR
import type { PageFunctions } from "dinou";

// Escapes SSG: executes getProps on every request
export const revalidate: PageFunctions["revalidate"] = 0;
// src/store/[id]/page_functions.ts - 60-second ISR
import type { PageFunctions, RouteParams } from "dinou";

export const revalidate: PageFunctions["revalidate"] = 60; // 1 minute ISR
// Conditional revalidation based on environment
export const revalidate: PageFunctions["revalidate"] = () => {
  return process.env.NODE_ENV === "development" ? 0 : 3600;
};

6. getCacheTags (Cache Tagging)

Assigns custom tags to the static page cache files, allowing the page to be purged and regenerated on-demand using revalidateTag(tag) (On-Demand Revalidation).

// src/products/[id]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";

type ProductParams = RouteParams<"/products/[id]">;

// 1. Constant Array
export const getCacheTags: PageFunctions["getCacheTags"] = ["catalog", "products"];

// 2. Synchronous Function (derives tags from route parameters)
export const getCacheTags: PageFunctions<ProductParams>["getCacheTags"] = (params) => {
  return ["catalog", `product-${params?.id}`];
};

// 3. Asynchronous Function (queries external tags or taxonomy)
export const getCacheTags: PageFunctions<ProductParams>["getCacheTags"] = async (params) => {
  const categories = await db.categories.forProduct(params?.id);
  return ["catalog", ...categories.map((c) => `category-${c.slug}`)];
};

On-Demand Invalidation via Server Function

When data changes, call revalidateTag() in a Server Function or webhook to instantly invalidate all cached files tagged with that identifier:

// src/actions/products.ts
"use server";

import { revalidateTag } from "dinou";
import { db } from "@/lib/db";

export async function updateProduct(id: string, data: any) {
  await db.products.update({ where: { id }, data });
  // Instantly invalidates /products/[id] and any catalog pages sharing the tag
  await revalidateTag(`product-${id}`);
}

7. ppr (Partial Prerendering)

Partial Prerendering (PPR) combines the instant TTFB (Time to First Byte) of a static page with the real-time capabilities of dynamic server-side rendering over a single HTTP response.

How PPR Works: Instant Shell + Progressive Streaming

PPR eliminates the traditional trade-off between static and dynamic rendering by dividing the page into two phases over a single HTTP connection:

  1. Instant Static Shell: Delivered immediately (0ms TTFB) from CDN edge or server cache. Includes layouts, navigation, and fallback skeletons.
  2. Dynamic Progressive Streaming: Live components wrapped in React <Suspense> execute concurrently on the server and stream their resolved HTML chunks directly into the page.
// src/dashboard/page_functions.ts
import type { PageFunctions } from "dinou";

// Enables Partial Prerendering for this route segment
export const ppr: PageFunctions["ppr"] = true;
// src/dashboard/page.tsx
import { Suspense } from "react";
import { UserGreeting, UserGreetingSkeleton } from "./user-greeting";
import { LiveMetrics, MetricsSkeleton } from "./live-metrics";

export default function DashboardPage() {
  return (
    <div className="dashboard-layout">
      {/* Static Shell: Pre-rendered at build time (0ms TTFB) */}
      <header className="dashboard-header">
        <h1>Management Console</h1>
      </header>

      {/* Dynamic Holes: Streamed progressively over the open HTTP connection */}
      <Suspense fallback={<UserGreetingSkeleton />}>
        <UserGreeting />
      </Suspense>

      <Suspense fallback={<MetricsSkeleton />}>
        <LiveMetrics />
      </Suspense>
    </div>
  );
}

8. End-to-End Type-Safe Example

To achieve complete type safety, autocompletion, and compile-time validation in your routes, import PageFunctions and LayoutFunctions from "dinou" and pass your parameter and props types:

1. Defining src/blog/[slug]/page_functions.ts

// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db, type Post } from "@/lib/db";

// 1. Strict route parameters derived from URL pattern
export type BlogParams = RouteParams<"/blog/[slug]">;

// 2. Define data contract passed to the component
export interface BlogPageProps {
  post: Post;
  views: number;
}

// 3. Page configuration with full type inference
export const ppr: PageFunctions["ppr"] = true;
export const revalidate: PageFunctions["revalidate"] = 60; // 1 min ISR

export const getStaticPaths: PageFunctions<BlogParams>["getStaticPaths"] = [
  "welcome",
  "announcement",
];

export const getProps: PageFunctions<BlogParams, BlogPageProps>["getProps"] = async (params) => {
  const post = await db.posts.findUnique({ where: { slug: params?.slug } });
  if (!post) throw new Error("Post not found");
  return { post, views: 100 };
};

export const validateParams: PageFunctions<BlogParams>["validateParams"] = (params) => {
  return Boolean(params?.slug && params.slug.length > 2);
};

export const getCacheTags: PageFunctions<BlogParams>["getCacheTags"] = (params) => {
  return ["blog", `post-${params?.slug}`];
};

2. Consuming Props in src/blog/[slug]/page.tsx

// src/blog/[slug]/page.tsx
import { Suspense } from "react";
import type { PageProps } from "dinou";
import type { BlogPageProps } from "./page_functions";
import { RealtimeViews, ViewsSkeleton } from "./components";

// Extend PageProps to receive both route params and loader data with full type safety
interface BlogPostPageProps extends PageProps<"/blog/[slug]">, BlogPageProps {}

export default function BlogPostPage({ params, post }: BlogPostPageProps) {
  return (
    <article className="max-w-2xl mx-auto py-8">
      {/* 1. Static shell (pre-rendered at build time, delivered with 0ms TTFB) */}
      <span className="text-xs text-muted-foreground font-mono">/blog/{params.slug}</span>
      <h1 className="text-3xl font-bold">{post.title}</h1>
      <div className="text-sm text-muted-foreground my-2">
        By {post.author} • published {post.publishedAt}
      </div>
      <div className="prose mt-6">{post.content}</div>

      {/* 2. PPR Dynamic Stream: Real-time analytics streamed in via Suspense */}
      <Suspense fallback={<ViewsSkeleton />}>
        <RealtimeViews slug={params.slug} />
      </Suspense>
    </article>
  );
}

3. Defining src/blog/layout_functions.ts

// src/blog/layout_functions.ts
import type { LayoutFunctions, RouteParams } from "dinou";
import { db } from "@/lib/db";

// 1. Layout parameters for /blog (static route segment without dynamic params = {})
type BlogLayoutParams = RouteParams<"/blog">;

// 2. Define layout data contract
export interface BlogCategory {
  id: string;
  name: string;
  count: number;
}

export interface BlogLayoutData {
  categories: BlogCategory[];
  featuredSlug: string;
}

// 3. Dedicated layout loader typed via LayoutFunctions
export const getProps: LayoutFunctions<BlogLayoutParams, BlogLayoutData>["getProps"] = async () => {
  const categories = await db.categories.findMany();
  const featured = await db.posts.findFirst({ where: { featured: true } });

  // Injected directly as props into layout.tsx
  return {
    categories,
    featuredSlug: featured?.slug ?? "welcome",
  };
};

// 4. Segment-level caching strategy for the layout shell
export const revalidate: LayoutFunctions["revalidate"] = 300; // Layout revalidates every 5 mins
export const ppr: LayoutFunctions["ppr"] = true;

4. Consuming Props in src/blog/layout.tsx

// src/blog/layout.tsx
import type { LayoutProps } from "dinou";
import type { BlogLayoutData } from "./layout_functions";

// Extend LayoutProps to receive children, segment params, and layout loader data
interface BlogLayoutComponentProps extends LayoutProps<"/blog">, BlogLayoutData {}

export default function BlogLayout({
  children,
  categories,
  featuredSlug,
}: BlogLayoutComponentProps) {
  return (
    <div className="blog-container">
      <aside className="blog-sidebar">
        <h3>Categories</h3>
        <ul>
          {categories.map((c) => (
            <li key={c.id}>
              {c.name} ({c.count})
            </li>
          ))}
        </ul>
      </aside>
      <main className="blog-content">
        {children}
      </main>
    </div>
  );
}

Alternative Object Destructuring Pattern

If you prefer defining all configurations in a single typed object before exporting, use object destructuring:

// 1. Page Config Object in src/blog/[slug]/page_functions.ts
const pageConfig: PageFunctions<BlogParams, BlogPageProps> = {
  ppr: true,
  revalidate: 60,
  getStaticPaths: ["welcome", "announcement"],
  getProps: async (params) => {
    const post = await fetchPost(params?.slug);
    return { post, views: 100 };
  },
  validateParams: (params) => Boolean(params?.slug),
  getCacheTags: (params) => ["blog", `post-${params?.slug}`],
};

export const { ppr, revalidate, getStaticPaths, getProps, validateParams, getCacheTags } = pageConfig;

// 2. Layout Config Object in src/blog/layout_functions.ts
const layoutConfig: LayoutFunctions<any, BlogLayoutData> = {
  revalidate: 300,
  ppr: true,
  getProps: async () => ({
    categories: await fetchCategories(),
    featuredSlug: "welcome",
  }),
};

export const { revalidate, ppr, getProps } = layoutConfig;

Static & Dynamic Rendering Strategies

Dinou is Static by Default. It provides a continuum of rendering strategies—from build-time SSG and stale-while-revalidate ISR to real-time Dynamic SSR, revolutionary Partial Prerendering (PPR), and component-level Micro-ISR with <DinouCacheSlot>.

Overview: Static by Default Architecture

In Dinou, every route is assumed to be static until proven otherwise. During dinou build, the compiler analyzes route segments, components, and loader functions to evaluate whether a route can be pre-rendered or requires real-time request data:

  1. Static by Default: Routes without dynamic dependencies are pre-rendered into immutable HTML and RSC payloads at build time, achieving 0ms TTFB at the CDN edge.
  2. Dynamic Bailout: When a page reads cookies, request headers, or sets revalidate: 0, Dinou automatically bails out to Live SSR on every incoming request.
  3. Hybrid Continuum: Escape the binary choice: stream dynamic components into a static shell with PPR, or isolate component caching with <DinouCacheSlot>.

1. Static Site Generation (SSG) & Bailout

Pure static rendering pre-generates HTML and RSC files during the build phase (npm run build). Production server startup is instantaneous, and static pages achieve the fastest possible Time to First Byte (TTFB) cached by Edge CDNs globally.

1.1. Pure Static Pre-rendering

Any page that does not consume request-specific headers, cookies, or query parameters and does not set revalidate < 1 is automatically generated as static HTML:

// src/about/page.tsx
// Automatically rendered as static HTML during build time ('npm run build')
export default function AboutPage() {
  return (
    <article className="prose">
      <h1>About Us</h1>
      <p>This page is pre-rendered once and served statically forever.</p>
    </article>
  );
}

1.2. Automatic Static Bailout

If a Server Component reads request-specific data at render time without wrapping it inside a dynamic streaming boundary (React <Suspense> with Partial Prerendering / PPR enabled), Dinou automatically "bails out" of static generation and transitions the entire page into Dynamic SSR for that request:

// src/profile/page.tsx
import { getContext } from "dinou";
import db from "@/lib/db";

export default async function ProfilePage() {
  const context = getContext();

  // ⚠️ Reading cookies outside <Suspense> triggers an Automatic Static Bailout
  // This page will now be rendered dynamically on every incoming request
  const sessionToken = context?.req.cookies["session_token"];
  const user = await db.users.findByToken(sessionToken);

  return <h1>Welcome back, {user?.name}</h1>;
}
Bailout Triggers in Server Components

Automatic bailout occurs when reading context.req.cookies, context.req.headers, or context.req.query (URL search parameters) during server component execution without wrapping the access in a React <Suspense> boundary.

What about Client Components and useSearchParams()?

Client Components ("use client") that call useSearchParams() do not trigger dynamic bailout. The page remains 100% static (SSG). During build time, the component is pre-rendered with an empty parameter set, and live URL parameters are synchronized in the browser upon hydration.

[!TIP] Preventing Hydration Mismatches with Search Parameters:
If a Client Component immediately renders searchParams.get("q") in its initial JSX markup, the server-generated static HTML (where the query was empty at build time) will differ from the live browser DOM (e.g. ?q=react), triggering a React hydration mismatch error.

Recommended Fix: Keep initial render deterministic, then read and apply live search parameters inside useEffect() after mounting:

// src/components/search-filter.tsx
"use client";

import { useSearchParams } from "dinou";
import { useState, useEffect } from "react";

export function SearchFilter() {
  const searchParams = useSearchParams();
  const [query, setQuery] = useState<string | null>(null);

  useEffect(() => {
    // Synchronize client-side only after mounting -> eliminates hydration mismatch
    setQuery(searchParams.get("q") ?? "");
  }, [searchParams]);

  if (query === null) {
    return <span className="text-muted-foreground">Loading filter...</span>;
  }

  return <span>Filter: <strong>{query}</strong></span>;
}

Alternative (Live SSR): If the initial HTML sent over the wire must contain search parameters for SEO, switch the page to Live SSR by exporting export const revalidate = 0; in page_functions.ts.


2. The Unified revalidate Strategy (Strict Seconds)

In Dinou v7, the revalidate export in page_functions.ts (or layout_functions.ts) serves as the single source of truth for route rendering behavior. It is measured strictly in seconds (s):

Value of revalidateStrategyDescription
false (or omitted)Pure Static (SSG)Pre-rendered once at build time. Cached indefinitely until manual invalidation.
0 (or number < 1)Dynamic SSRRendered on every incoming HTTP request on the fly.
N (number >= 1)Time-Based ISRPre-rendered, served instantly from cache, and revalidated in the background every N seconds.

2.1. Dynamic SSR (revalidate < 1 or 0)

To force a page to render dynamically on every single request (escaping the default static generation), set revalidate to 0:

// src/analytics/live/page_functions.ts

// Escapes static generation: executes dynamic SSR on every request
export const revalidate = 0;

[!NOTE] Migration from Legacy dynamic Export: In older versions of Dinou, dynamic rendering was toggled via export function dynamic(). In Dinou v7, the dynamic export has been completely removed. Setting export const revalidate = 0; is the official unified mechanism to escape static generation.

2.2. Incremental Static Regeneration (revalidate >= 1)

When revalidate >= 1, Dinou serves pre-rendered static HTML/RSC payloads while regenerating the page in the background at most once every N seconds (stale-while-revalidate):

// src/blog/page_functions.ts

// Rebuilds the cached page in the background at most once every 60 seconds
export const revalidate = 60; // Strict seconds (1 minute)
  • Background Revalidation Flow: The first request after the 60s window receives the cached page immediately. Dinou re-executes the server render in the background. Subsequent visitors see the freshly generated version.
  • Edge ISR Storage Adapters: On edge platforms like Deno Deploy and Cloudflare Workers, ISR seamlessly uses distributed key-value storage (DenoKVStorage, CloudflareKVStorage, MemoryStorage) without needing a persistent disk.

3. Partial Prerendering (PPR)

Partial Prerendering (PPR) resolves the decades-old dilemma between static and dynamic rendering. Instead of forcing an entire page to be 100% static or 100% dynamic, PPR delivers a pre-rendered static shell instantly (0ms TTFB), while streaming dynamic holes in parallel over the same HTTP response.

3.1. Hybrid Architecture: Static Shell + Dynamic Holes

At build time, Dinou compiles the layout, navigation, and static components into an immutable HTML shell. Any dynamic component wrapped in React <Suspense> is saved as a streaming placeholder hole (<div data-ppr-hole="slot-id">...fallback...</div>).

The Single HTTP Response Timeline
  1. 0ms (Instant TTFB): The browser receives the static shell with skeleton fallbacks directly from Edge CDN cache. Visual layout is painted immediately.
  2. Request Processing: Concurrently, Dinou executes the async Server Components inside <Suspense> on the server.
  3. Streaming In-Place: As each Suspense promise resolves, the HTML chunk and RSC payload are streamed down the same HTTP connection, replacing the skeleton with real content without full page reloads.
Data Freshness: Pure SSG vs. Partial Prerendering
  • Without PPR (Frozen Build Data): Data is baked into the build artifact. Live inventory or user carts are stale upon deployment. Applications historically resorted to legacy client-side useEffect() + fetch() anti-patterns, suffering waterfalls, layout shifts, and spinners.
  • With PPR (Request-Time Fresh Data): The visual shell is delivered at 0ms TTFB from the edge, while the component inside <Suspense> executes on the server on every incoming request. You get 100% fresh, live database data streamed directly to the browser without client fetch waterfalls.
PPR as a Bailout Boundary: Containing Dynamic De-optimization

Without PPR, dynamic data access is infectious: if a single nested component reads request cookies or headers, Dinou triggers an Automatic Static Bailout for the entire page, destroying static pre-rendering and edge caching for the whole route.

With PPR enabled (ppr = true), React <Suspense> acts as an isolated sandbox. Dynamic data consumption inside the boundary does not escape to parent or sibling components:

// src/store/page.tsx
import { Suspense } from "react";
import { getContext } from "dinou";
import db from "@/lib/db";
import { ProductGallery } from "@/components/gallery";
import { CartBadgeSkeleton } from "@/components/skeletons";

// ⚠️ Dynamic component: reads request-specific session cookie
async function LiveCartBadge() {
  const context = getContext();
  const sessionToken = context?.req.cookies["session_id"];
  const cart = await db.carts.findActive(sessionToken);

  return <div className="cart-badge">Items: {cart?.itemCount ?? 0}</div>;
}

export default function StorePage() {
  return (
    <main className="container">
      {/* 1. Static Shell: 0ms TTFB, pre-rendered during build */}
      <header className="navbar">
        <h1>Flagship Store</h1>
        
        {/* 2. PPR Boundary: Isolates cookie bailout exclusively to this hole */}
        <Suspense fallback={<CartBadgeSkeleton />}>
          <LiveCartBadge />
        </Suspense>
      </header>

      {/* 3. Static Sibling: Remains 100% static and edge-cached */}
      <ProductGallery />
    </main>
  );
}
  • ❌ Without PPR (Full-Page Bailout): Reading cookies["session_id"] turns the entire page into dynamic SSR. Edge caching is lost, and the visitor waits with a blank screen.
  • ✅ With PPR (Isolated Hole Bailout): The cookie bailout is quarantined inside <Suspense>. The navbar and product gallery are delivered instantly at 0ms TTFB. Only the cart badge executes on the server and streams into place.

3.2. Enabling PPR & Specificity Hierarchy

Dinou provides granular control over Partial Prerendering across three hierarchical levels:

  1. Page Level (page_functions.ts) — Highest Priority: A page can override any parent layout or global configuration:
    // src/shop/products/[slug]/page_functions.ts
    export const ppr = true; // Guarantees PPR on product details
    
  2. Layout Level (layout_functions.ts) — Subtree Cascading: Applies to that layout and cascades to all its descendant subroutes:
    // src/admin/layout_functions.ts
    export const ppr = false; // Disables PPR for /admin and ALL subroutes
    
  3. Global Configuration (dinou.config.mjs): Enables PPR globally across all eligible routes:
    // dinou.config.mjs
    import { defineConfig } from "dinou";
    export default defineConfig({ ppr: true });
    

4. Vertical Segmentation: Micro-ISR (<DinouCacheSlot>)

Traditional full-stack frameworks only offer Horizontal Segmentation (caching an entire URL route or layout shell). When a dynamic page contains an expensive database widget, developers are often forced into an architectural trap: converting the widget to a Client Component with client-side cache libraries (SWR, TanStack Query), introducing bundle bloat, waterfall requests, and layout shifts.

Dinou v7 introduces Vertical Segmentation with <DinouCacheSlot> from "dinou/server". It keeps components securely on the server as React Server Components (RSC) while caching their rendered serialized Flight payload in server storage with dedicated TTLs and invalidation tags.

4.1. Component Caching: Dynamic vs. Static Patterns

<DinouCacheSlot> functions as a universal caching primitive across two core architectural patterns:

Implementation Pattern 1: Dynamic SSR Dashboard with Cached Slot

The route runs in Live SSR mode (evaluating user cookies and personal greetings live). The heavy analytics calculation is cached on the server for 300 seconds and wrapped in React <Suspense>:

// src/dashboard/page.tsx
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou/server";
import { FastUserProfile, SlowAnalyticsWidget } from "./widgets";
import { AnalyticsSkeleton } from "./skeletons";

export default function DashboardPage() {
  return (
    <main className="grid grid-cols-1 md:grid-cols-2 gap-6 p-6">
      {/* 1. Dynamic Component: Evaluated live on every request (reads session/cookies) */}
      <FastUserProfile />

      {/* 2. Micro-ISR Slot: Cached on the server for 300 seconds */}
      {/* ⚠️ Wrapped in <Suspense> so cache misses stream without blocking TTFB */}
      <Suspense fallback={<AnalyticsSkeleton />}>
        <DinouCacheSlot id="analytics-widget" tag="analytics" revalidate={300}>
          <SlowAnalyticsWidget />
        </DinouCacheSlot>
      </Suspense>
    </main>
  );
}
Implementation Pattern 2: Static / ISR Product Page with Asymmetric Slot

The parent route has a slow horizontal revalidation of 24 hours (revalidate: 86400). Inside it, a volatile inventory counter revalidates vertically every 60 seconds (revalidate={60}) or on-demand:

// src/products/[slug]/page_functions.ts
export const revalidate = 86400; // Slow horizontal revalidation (24h)

// src/products/[slug]/page.tsx
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou/server";
import { ProductHero, ProductDescription } from "./static-widgets";
import { LiveStockCounter, StockSkeleton } from "./stock-widgets";

export default function ProductDetailPage({ params }: { params: { slug: string } }) {
  return (
    <main className="container py-8 space-y-6">
      {/* 1. Heavy Static Content: Cached at the CDN edge for 24 hours */}
      <ProductHero slug={params.slug} />
      <ProductDescription slug={params.slug} />

      {/* 2. Asymmetric Micro-ISR Slot: Revalidates vertically every 60s */}
      <Suspense fallback={<StockSkeleton />}>
        <DinouCacheSlot 
          id={`stock-${params.slug}`} 
          tag={`stock-${params.slug}`} 
          revalidate={60}
        >
          <LiveStockCounter slug={params.slug} />
        </DinouCacheSlot>
      </Suspense>
    </main>
  );
}

[!IMPORTANT] Boundary Placement: Why <Suspense> Must Wrap <DinouCacheSlot>
<DinouCacheSlot> is itself an asynchronous Server Component. When it awaits storage or renders on a cache miss, React encounters a pending promise in the parent page tree. Without an outer <Suspense>, suspension bubbles to the root, blocking the entire HTTP response and delaying fast sibling components.

The Rule of Hierarchy:

  1. <Suspense fallback={<Skeleton />}> → Controls browser streaming & instant visual layout.
  2. <DinouCacheSlot revalidate={300}> → Controls server cache storage, TTL, and cache tags.
  3. <SlowAnalyticsWidget /> → Executes database queries and business logic.

4.2. Interactive Revalidation & On-Demand Purging

Dinou provides specialized mechanisms to refresh or purge <DinouCacheSlot> instances without reloading the browser page:

Client-Side In-Place Refresh (refreshSlot)

Any interactive Client Component can trigger a surgical re-render of only that slot without full page reload or state loss:

"use client";
import { refreshSlot, useRouter } from "dinou";

export function RefreshButton() {
  const router = useRouter();

  return (
    <div className="flex gap-2">
      {/* Option 1: Standalone (respects server slot TTL) */}
      <button onClick={() => refreshSlot("analytics-widget")}>
        Refresh Metrics
      </button>

      {/* Option 2: Force fresh (bypasses server slot TTL immediately) */}
      <button onClick={() => router.refreshSlot("analytics-widget", { fresh: true })}>
        Force Fresh
      </button>
    </div>
  );
}
  • refreshSlot(id) (Default): Refreshes the client DOM with the server's current state. If the slot's server TTL has not expired, it returns cached server JSX without touching your database (protecting against button spam and traffic spikes).
  • refreshSlot(id, { fresh: true }) (Bypass): Hard revalidation. Forces the server to ignore unexpired TTL, immediately re-run queries against the database, and update both cache and client.
Server-Side Tag Invalidation (revalidateTag)

Mutations purge slots globally via revalidateTag() on the server, while the client immediately pulls the fresh result via refreshSlot():

// src/app/feed/actions.ts
"use server";
import { revalidateTag } from "dinou/server";
import db from "@/lib/db";

export async function publishPost(formData: FormData) {
  const content = formData.get("content") as string;
  await db.posts.insert({ content, createdAt: new Date() });

  // Purges cache globally for ALL future visitors across CDNs and Micro-ISR slots
  await revalidateTag("feed");
}
Zero Skeleton Flash (startTransition + Stale-Preserving Fallback)

While the server re-renders an expired slot during an interactive refresh, the user does NOT see a blank skeleton flash or layout shift. Dinou's internal client boundary wraps slot updates in React.startTransition and sets fallback: children (the previous DOM content). The existing widget stays interactive and visible until the new server stream arrives, then swaps seamlessly.

To display an inline loading indicator on the button itself, wrap refreshSlot inside React's standard useTransition():

"use client";
import { useTransition } from "react";
import { refreshSlot } from "dinou";

export function RefreshAnalyticsButton() {
  const [isPending, startTransition] = useTransition();

  const handleRefresh = () => {
    startTransition(() => {
      refreshSlot("analytics-widget");
    });
  };

  return (
    <button onClick={handleRefresh} disabled={isPending}>
      {isPending ? "Refreshing Metrics..." : "Refresh Metrics"}
    </button>
  );
}

4.3. Horizontal vs. Vertical Segmentation & Anti-Patterns

DimensionScopeConfigurationBest For
Horizontal SegmentationRoute / Segment Levelpage_functions.ts, layout_functions.tsWhole-page SSG, ISR, and layout persistence.
Vertical SegmentationComponent Subtree Level<DinouCacheSlot> from dinou/serverIndependent widget TTLs, asymmetric revalidation, and in-place client refreshes.
When NOT to Use <DinouCacheSlot>
  1. 100% Static Pages (SSG): If an entire route is pre-rendered at build time with no revalidation, it is already cached as immutable HTML at the CDN. Slots add zero benefit.
  2. Wrapping the Entire Page: Wrapping the entire page.tsx body inside a slot reinvents horizontal caching. Declare export const revalidate = 300 in page_functions.ts instead.
  3. Unpartitioned Private User Data: Never wrap private user data (such as a specific user's cart or session token) with a static slot ID. This would cache user A's private data and leak it to user B.

5. High-Traffic Synergy: PPR + <DinouCacheSlot>

Nesting <DinouCacheSlot> inside a React <Suspense> boundary on a route with Partial Prerendering (PPR) unlocks the highest performance profile possible: zero-delay initial delivery (0ms TTFB) paired with near-zero streaming delay (~2ms) and 99.9% database query offloading.

  • PPR Alone (Uncached Dynamic Hole): 0ms static shell from Edge CDN, but the hole waits for live DB execution (e.g. 1.5s) on every single request. 10,000 visitors/min = 10,000 live DB queries.
  • PPR + <DinouCacheSlot> (Cached Streamed Hole): 0ms static shell from Edge CDN + hole resolves from server cache in ~2ms. 10,000 visitors/min = 1 database query per TTL window (99.9% database offload).

Implementation Pattern: High-Traffic Telemetry Dashboard

// src/telemetry/page_functions.ts
import type { PageFunctions } from "dinou";

// Enable Partial Prerendering on this high-traffic route
export const ppr: PageFunctions["ppr"] = true;
// src/telemetry/page.tsx
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou/server";
import { TelemetryHeader, TelemetryFooter } from "./static-shell";
import { HeavyMetricsGraph, GraphSkeleton } from "./graph-widgets";

export default function TelemetryPage() {
  return (
    <main className="container py-8 space-y-6">
      {/* 1. Static Shell: Delivered instantly in 0ms TTFB from Edge CDN */}
      <TelemetryHeader />

      {/* 2. PPR Dynamic Hole + Micro-ISR Slot:
             - Fallback skeleton sent immediately in initial layout
             - Hole streams from server cache in ~2ms for subsequent visitors
             - Database query runs only ONCE every 60 seconds */}
      <Suspense fallback={<GraphSkeleton />}>
        <DinouCacheSlot id="operations-telemetry" tag="telemetry" revalidate={60}>
          <HeavyMetricsGraph />
        </DinouCacheSlot>
      </Suspense>

      <TelemetryFooter />
    </main>
  );
}

6. Incremental Static Generation (ISG on-demand)

While ISR updates existing pre-rendered pages, ISG (Incremental Static Generation) generates new dynamic paths on demand that were omitted from the build-time getStaticPaths list.

6.1. On-Demand Path Generation

Any route path omitted from getStaticPaths will be generated dynamically when the first visitor requests it, cached, and then served as static HTML to subsequent visitors:

// src/products/[id]/page_functions.ts

// Pre-render only the top 10 products at build time:
export const getStaticPaths = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"];

// Any other ID (e.g. /products/452) will be generated on-demand
// on its first visitor and cached permanently.

6.2. Governance APIs (validateParams & allowISG)

Dinou provides strict server-side governance exports to secure on-demand generation against cache pollution and DDoS:

  • validateParams(params): Validates route parameters before generation begins. Returning false halts rendering immediately and serves an instant 404:
    export function validateParams(params: { id: string }) {
      return /^\d+$/.test(params.id); // Only allow numeric IDs
    }
    
  • allowISG: Disables on-demand generation entirely. When set to false, any path not pre-rendered in getStaticPaths returns 404 without querying the server:
    export const allowISG = false; // Strict SSG only
    

7. Complete Rendering Strategies Matrix

Summary of all rendering strategies supported natively by Dinou v7:

StrategyHow to ConfigureTTFBFreshnessDatabase LoadBest For
Pure SSGDefault / revalidate = false0ms (Instant)Build-time0 queries (Build-time only)Landing pages, marketing, documentation
ISRrevalidate = N (N >= 1)0ms (Cached)Stale-while-revalidate1 query per TTL (0 on cache hits)Blogs, catalogs, news feeds
Dynamic SSRrevalidate = 0Request-time (Blocked)100% Real-timeHeavy: 100% queries on every requestUser settings, banking, checkout
PPR (Partial Prerendering)ppr = true + <Suspense>0ms (Shell from Edge)100% Real-time (Holes)Heavy: 100% Hole queries on every requestE-commerce, dashboards with static shells
Micro-ISR (Static / Asymmetric ISR)<DinouCacheSlot> (in Static/ISR page)0ms (CDN Edge)Decoupled Slot TTL / TagsLow: Isolated to expired slotsStatic product/catalog pages with volatile widgets (stock, prices)
Micro-ISR (Dynamic SSR)<DinouCacheSlot> (in Dynamic SSR page)Fast (Early Layout / Slot-cached)Component TTL / TagsLow: Isolated to expired slotsDynamic user dashboards with heavy SQL calculations
PPR + Micro-ISR (Synergy)ppr = true + <DinouCacheSlot>0ms (Edge) + ~2ms (Hole)Cached Streamed HoleNear-Zero: 1 query per slot TTL (99.9% reduction)High-concurrency e-commerce, telemetry dashboards
ISG (Incremental Static)allowISG = true (default)0ms (after 1st hit)Cached permanently1 query on 1st generation (0 thereafter)Long-tail e-commerce products, dynamic paths

[!TIP] For deep-dive code recipes, runtime execution timing, TTFB charts, and production trade-offs across all 13 rendering and data fetching patterns, see Data Fetching: The 13 Strategies below.


Server Functions ("use server")

Server Functions are asynchronous RPC endpoints that run exclusively in the server runtime. In Dinou, you declare them by adding the "use server" directive at the top of a dedicated action file (module-level directive).

Overview & "use server" Directive

CapabilityArchitectural Benefit
Zero API BoilerplateNo manual route handlers, fetch() boilerplate, or JSON serialization required. Dinou generates typed Flight RPC endpoints (POST /____server_function____) automatically.
JSX & Data ReturnsUnlike standard RPCs that only return JSON, Dinou Server Functions can return fully rendered React components streamed over Flight RPC (The Dinou Pattern).
Security & SecretsSecret keys, database connections, and ORM calls are never bundled into the client-side JavaScript chunk.
Form & Action SynergyNative integration with HTML <form action={...}> and React 19 primitives (useActionState, useFormStatus, useOptimistic).

1. Returning Live React Components (UI Streaming)

Dinou uniquely allows Server Functions to return fully rendered React component trees directly over Flight RPC. In Server Components, the component is awaited directly inside native <Suspense>; in Client Components, the returned promise is unwrapped with React 19's use() hook:

Pattern A: Server Function (Returning Rendered JSX)

// src/actions/get-post-card.tsx
"use server"; // 👈 Module-level directive in Dinou

import db from "@/lib/db";
import { PostCard } from "@/components/post-card";

export async function getPostCard(postId: string) {
  const post = await db.posts.findUnique({ where: { id: postId } });
  if (!post) throw new Error("Post not found");

  // Returns a rendered JSX component directly over Flight RPC!
  return <PostCard post={post} />;
}

Pattern B: Consuming in Server Components (Async / Await)

Directly await the Server Function inside an async component. React streams HTML progressively as promises resolve:

// src/blog/page.tsx
// Server Component: directly await the Server Function with React 19 Suspense
import { Suspense } from "react";
import { getPostCard } from "@/actions/get-post-card";

async function PostSlot({ id }: { id: string }) {
  // Directly awaited on the server during SSR / streaming
  return await getPostCard(id);
}

export default function BlogPage() {
  return (
    <section className="space-y-4">
      <h1 className="text-2xl font-bold">Featured Story</h1>
      
      {/* Native React 19 Suspense fallback */}
      <Suspense fallback={<div className="skeleton h-48 w-full rounded" />}>
        <PostSlot id="post-101" />
      </Suspense>
    </section>
  );
}

Pattern C: Consuming in Client Components (with React 19 use())

Dispatches Flight RPC returning a Promise. React 19's native use(promise) unrolls it synchronously inside <Suspense> without external state libraries, and re-suspends cleanly on promise update:

// src/blog/page.tsx
// Client Component: unwrap the Flight RPC promise with React 19 use()
"use client";

import { useState, use, Suspense } from "react";
import { getPostCard } from "@/actions/get-post-card";

function PostStream({ promise }: { promise: Promise<React.ReactNode> }) {
  // Pure React 19 'use()' unrolls the promise within Suspense
  const content = use(promise);
  return <>{content}</>;
}

export default function BlogPage() {
  const [postId, setPostId] = useState("post-1");
  const [postPromise, setPostPromise] = useState(() => getPostCard("post-1"));

  const loadPost = (id: string) => {
    setPostId(id);
    setPostPromise(getPostCard(id));
  };

  return (
    <section className="space-y-4">
      <h1 className="text-2xl font-bold">Featured Story</h1>

      <div className="flex gap-2">
        <button onClick={() => loadPost("post-1")} className="btn">Post 1</button>
        <button onClick={() => loadPost("post-2")} className="btn">Post 2</button>
      </div>

      {/* Re-suspends cleanly on promise update */}
      <Suspense key={postId} fallback={<p>Streaming post from server...</p>}>
        <PostStream promise={postPromise} />
      </Suspense>
    </section>
  );
}

[!NOTE] Execution Comparison: Server vs. Client Components

  • In Server Components (Async / Await): Directly await the Server Function inside an async component. React streams HTML progressively as promises resolve.
  • In Client Components (with use()): Dispatches Flight RPC returning a Promise. React 19's native use(promise) unrolls it synchronously inside <Suspense> without external state libraries.

2. Server Actions & Form Mutations

Dinou integrates seamlessly with standard HTML forms and React 19 form actions for data mutations.

2.1. Server Actions in Forms

You can pass a Server Action directly to the native HTML <form action={...}> attribute:

// src/components/newsletter-page.tsx
"use client";

import { subscribeNewsletter } from "@/actions/subscribe";

export default function NewsletterPage() {
  return (
    <form action={subscribeNewsletter} className="flex gap-2">
      <input name="email" type="email" placeholder="[email protected]" required className="input" />
      <button type="submit" className="btn-primary">Subscribe</button>
    </form>
  );
}
// src/actions/subscribe.ts
"use server"; // 👈 Required at the module level in Dinou

import db from "@/lib/db";

export async function subscribeNewsletter(formData: FormData) {
  const email = formData.get("email") as string;
  await db.subscribers.create({ data: { email } });
}

[!NOTE] Module-Level Directives & Client Execution Dinou requires the "use server" directive to be placed at the top of the action file (not inline within function bodies). When a form submits, Dinou's client runtime transparently dispatches a Flight RPC request (POST /____server_function____) to execute the server action and receive updated UI state.

[!TIP] What does subscribeNewsletter return? (Understanding void / undefined) This action returns Promise<void>, which resolves to undefined. In pure React 19 form actions (<form action={subscribeNewsletter}>), this is completely standard for fire-and-forget mutations: the browser submits the form data, the server executes the database write, and no state payload is needed by the UI.

When should a Server Action return a value? If your UI needs to display interactive feedback (such as "Subscribed successfully!" or validation errors), the action should return a serializable payload (e.g. { success: true, message: "..." }) and be wired to React 19's useActionState hook (as shown below in Section 2.2). Other common patterns include performing a server-side redirect() or calling revalidatePath() before completing.


2.2. React 19 Hooks: useActionState & useFormStatus

For rich interactive forms with validation feedback and loading spinners, use React 19's standard hooks:

  • useActionState (React 19): Captures the returned state (errors, success payload) and pending status of the Server Action across submissions.
  • useFormStatus (react-dom): Reads the pending submission status of the parent <form> without prop drilling.
// src/components/create-post-form.tsx
"use client";

import { useActionState } from "react";
import { useFormStatus } from "react-dom";
import { createPostAction, type FormState } from "@/actions/posts";

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending} className="btn-primary">
      {pending ? "Saving..." : "Publish Post"}
    </button>
  );
}

const initialState: FormState = { success: false, message: "" };

export function CreatePostForm() {
  // useActionState binds the server action to form state and tracks submission
  const [state, formAction, isPending] = useActionState(createPostAction, initialState);

  return (
    <form action={formAction} className="flex flex-col gap-4">
      {state.message && (
        <div className={state.success ? "text-green-500" : "text-red-500"}>
          {state.message}
        </div>
      )}

      <input name="title" placeholder="Post title" required className="input" />
      <textarea name="content" placeholder="Write content..." required className="textarea" />
      
      <SubmitButton />
    </form>
  );
}
// src/actions/posts.ts
"use server"; // 👈 Module-level directive required in Dinou

import db from "@/lib/db";

export type FormState = {
  success: boolean;
  message: string;
};

// Server Action receives (prevState, formData) when bound to useActionState
export async function createPostAction(
  prevState: FormState,
  formData: FormData
): Promise<FormState> {
  const title = (formData.get("title") as string)?.trim();
  const content = (formData.get("content") as string)?.trim();

  if (!title || title.length < 3) {
    return { success: false, message: "Title must be at least 3 characters." };
  }

  if (!content) {
    return { success: false, message: "Content cannot be empty." };
  }

  await db.posts.create({ data: { title, content } });
  return { success: true, message: "Post published successfully!" };
}

2.3. Optimistic Updates with useOptimistic

With React 19's useOptimistic, you can update the user interface immediately before the server responds:

// src/components/like-button.tsx
"use client";

import { useOptimistic, useTransition } from "react";
import { toggleLikeAction } from "@/actions/likes";

export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
  const [isPending, startTransition] = useTransition();
  const [optimisticLikes, setOptimisticLikes] = useOptimistic(
    initialLikes,
    (current, update: number) => current + update
  );

  const handleClick = () => {
    startTransition(async () => {
      setOptimisticLikes(1); // Optimistically increment in 0ms on client
      await toggleLikeAction(postId); // Dispatches Server Action via Flight RPC
    });
  };

  return (
    <button onClick={handleClick} disabled={isPending} className="btn-like">
      ❤️ {optimisticLikes}
    </button>
  );
}
// src/actions/likes.ts
"use server"; // 👈 Module-level directive required in Dinou

import db from "@/lib/db";

export async function toggleLikeAction(postId: string) {
  // Updates like count on the database
  await db.posts.update({
    where: { id: postId },
    data: { likes: { increment: 1 } },
  });

  return { success: true };
}

[!NOTE] Why is there no <Suspense> boundary here? <Suspense> only activates when a component suspends (by throwing a pending Promise, such as with React.use(promise) or an async Server Component).

In contrast, useOptimistic does the exact opposite of suspending: it synchronously paints the temporary new value (e.g. switching immediately from 10 to 11) in 0ms on the client, avoiding loading spinners or skeleton fallbacks entirely while the Server Action runs in the background. It relies strictly on React 19 transitions (useTransition or Form Actions). If the server call succeeds, React seamlessly commits the real server state; if it fails, React automatically rolls back to the initial value with zero boilerplate.


3. Request Context, Cookies & Headers (getContext)

Dinou provides getContext() (exported from "dinou") to access the underlying request and response lifecycle synchronously within any Server Function or SSR component.

RequestContextStore API Reference:

  • context.req.cookies: Dictionary of incoming client cookies (e.g. cookies["session_token"]).
  • context.req.headers: Incoming HTTP headers dictionary (e.g. headers["user-agent"]).
  • context.req.query: URL search parameters.
  • context.res.cookie(name, value, options): Sets an HTTP cookie with options (httpOnly, secure, maxAge, sameSite, path, etc.).
  • context.res.clearCookie(name, options): Clears a cookie from the client.

3.1. Reading Query, Headers & Cookies

// src/actions/auth-check.ts
"use server";

import { getContext } from "dinou";

export async function getCurrentUser() {
  const context = getContext();
  if (!context) return null;

  const sessionToken = context.req.cookies["session_token"];
  const userAgent = context.req.headers["user-agent"];

  if (!sessionToken) return null;
  return await verifySession(sessionToken);
}

3.2. Setting & Clearing Cookies

// src/actions/login.ts
"use server";

import { getContext } from "dinou";

export async function loginAction(formData: FormData) {
  const context = getContext();
  const email = formData.get("email") as string;
  const password = formData.get("password") as string;

  const session = await authenticateUser(email, password);

  // Set secure HTTP-only session cookie
  context?.res.cookie("session_token", session.token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax",
    maxAge: 60 * 60 * 24 * 7, // 7 days in seconds
    path: "/",
  });

  return { success: true };
}

export async function logoutAction() {
  const context = getContext();
  // Clear the cookie upon logout
  context?.res.clearCookie("session_token", { path: "/" });
  return { success: true };
}

4. Redirections (redirect)

Dinou provides a universal redirect(destination) utility exported from "dinou":

  • In Server Functions (Soft SPA Navigation): When called inside a Server Function (e.g. form mutations or RPC calls), Dinou automatically performs a smooth client-side transition to the destination route via the SPA router without a full page reload.
  • In Server Components (Initial SSR): If headers have not yet been sent, Dinou issues a clean HTTP 302 redirect for SEO and search engine crawlers. If response streaming has already started, it completes the transition on the client upon hydration.
  • In Client Components (Declarative Redirects): When called directly in client components during render, it triggers an immediate client router replacement (router.replace()) without reloading the page.
// src/actions/checkout.ts
"use server";

import { redirect } from "dinou";
import db from "@/lib/db";

export async function completeOrder(orderId: string) {
  await db.orders.updateStatus(orderId, "PAID");

  // Automatically navigates the client router to the success page
  return redirect(`/checkout/${orderId}/success`);
}

4.1. The 4 Execution Contexts (HTTP 302 vs. Client Transition)

A foundational question when handling redirects across hybrid architectures is: How does Dinou determine whether redirect() issues an HTTP 302 vs. a client-side transition, and is calling redirect() in a Server Component equivalent to invoking it via a Server Function?

[!NOTE] The Equivalence Principle in Server Components Calling redirect() directly inside a Server Component vs. calling an imported Server Function (e.g., await verifyUser()) from that Server Component is functionally identical. Because both run inside the same server Node.js process during the same SSR request lifecycle, they share the exact same request context and streaming state.

Context 1: SSR Root (page_functions.ts, page.tsx, Plugin) -> HTTP 302

Occurs inside getProps() in page_functions.ts, at the top level of the root page.tsx Server Component (outside any <Suspense> boundary), or in a global plugin hook (onRequest) in dinou.config.mjs. Because zero HTML bytes have reached the wire (headersSent === false), Dinou issues a clean HTTP 302 with a Location header. Crucial for SEO crawlers and search indexers.

// src/dashboard/page_functions.ts
import { redirect, getContext } from "dinou";

export async function getProps() {
  const { req } = getContext();
  if (!req.cookies.session_token) {
    // Evaluated before page rendering -> HTTP 302 redirect
    return redirect("/login");
  }
  return { user: { name: "Alex" } };
}
// src/dashboard/page.tsx (Server Component root)
import { redirect, getContext } from "dinou";

export default async function DashboardPage() {
  const { req } = getContext();
  if (!req.cookies.session_token) {
    // At root before stream begins -> HTTP 302 redirect
    return redirect("/login");
  }

  return <h1>Welcome to Dashboard</h1>;
}
// dinou.config.mjs (Global Plugin Hook)
import { defineConfig } from "dinou/config";

export default defineConfig({
  plugins: [
    {
      name: "auth-guard",
      async onRequest(request, context) {
        const url = new URL(request.url);
        const cookieHeader = request.headers.get("cookie") || "";
        // Runs pre-flight before SSR or route resolution
        if (url.pathname.startsWith("/dashboard") && !cookieHeader.includes("session_token=")) {
          return Response.redirect(new URL("/login", request.url), 302);
        }
      },
    },
  ],
});
Context 2: SSR Deferred () -> Client Hydration

Occurs inside an async child wrapped in <Suspense fallback={<Skeleton />}>. React opens the HTTP connection with status 200 OK immediately to stream the initial shell. At HTTP protocol level, an open 200 OK stream cannot turn into a 302. Dinou safely emits <ClientRedirect />, completing the transition upon browser hydration without breaking the stream:

// src/reports/page.tsx (Server Component)
import { Suspense } from "react";
import { redirect } from "dinou";

async function RestrictedReport() {
  const isAllowed = await checkUserAccess();
  if (!isAllowed) {
    // 200 OK stream already open -> <ClientRedirect />
    return redirect("/unauthorized");
  }
  return <div>Financial Metrics</div>;
}

export default function Page() {
  return (
    <Suspense fallback={<p>Loading financial report...</p>}>
      <RestrictedReport />
    </Suspense>
  );
}
Context 3: Server Function from Client -> Soft SPA Navigation

Triggered from browser interactions (form actions or button clicks) via a dedicated POST request. Dinou executes the server mutation and intercepts redirect() before streaming (headers unsent), returning redirect metadata. The client proxy navigates via the SPA router without a full page reload:

// src/actions/cart.ts
"use server";

import { redirect } from "dinou";
import db from "@/lib/db";

export async function checkoutAction(formData: FormData) {
  const order = await db.orders.create({ /* ... */ });

  // Dedicated POST: soft SPA navigation in browser without page reload
  return redirect(`/orders/${order.id}`);
}
Context 4: Client Component (Render) -> Client Router Replacement

Executes 100% in the browser with zero server roundtrip. Immediately dispatches router.replace() in the client SPA router, updating route and UI in 0ms:

// src/components/client-guard.tsx
"use client";

import { redirect } from "dinou";

export function ClientGuard({ isAllowed }: { isAllowed: boolean }) {
  if (!isAllowed) {
    // In-browser render: executes router.replace() in SPA router
    return redirect("/forbidden");
  }

  return <button className="btn">Control Panel</button>;
}

4.2. Universal Redirection Matrix

Invocation ContextHeaders Sent?Resulting MechanismRecommended For
1. SSR Root
(page_functions.ts, root page.tsx, Plugin)
NoHard HTTP Redirect (Status 302)
Server issues an HTTP/1.1 302 Found with a Location header. The browser or search crawler intercepts it at the protocol level and fetches the new page as a fresh document.
Auth guards (getProps), root guards, global middleware plugins, SEO canonicals
2. SSR: Deferred
(<Suspense>)
Yes
(200 OK stream open)
Streaming Fallback (<ClientRedirect />)
Because the 200 OK chunked stream cannot be rewritten to a 302, Dinou injects <ClientRedirect /> into the stream. The browser router transitions upon hydration without aborting the connection.
Deferred conditional access, late data-dependent checks inside Suspense
3. Server Function
(from Client Action)
No
(standard POST)
Soft SPA Navigation (Status 200)
Server returns HTTP 200 OK with redirection metadata. Dinou's client runtime intercepts it and navigates via the SPA router without a full page reload or loss of client state.
Form submissions, shopping cart checkout, stateful mutations
4. Client Component
(Render)
N/A
(in-browser)
Client Router Replacement (router.replace())
Executes 100% in the browser with zero server roundtrip. Immediately dispatches router.replace() in the client SPA router, updating route and UI in 0ms.
Pure UI state redirection, client-side permission gates

[!TIP] Looking for Cache Revalidation? Learn how to purge stale SSG pages and ISR cache slots on-demand using revalidatePath and revalidateTag in On-Demand Revalidation below.


On-Demand Revalidation

High-performance web applications cannot rely exclusively on time-based expiration (TTL). On-Demand Revalidation in Dinou enables instantaneous cache purging and static regeneration across four specialized tiers:

Overview & Multi-Tier Architecture

Invalidation TierAPI & MechanismArchitectural Scope
Surgical PagerevalidatePage(path)Regenerates exclusively page.rsc and index.html, leaving all parent layout shells completely untouched.
Route SegmentrevalidatePath(path)Purges page RSC, HTML, and any layout.rsc in that segment. Supports recursive propagation with { cascade: true }.
Cross-Cutting TagrevalidateTag(tag)Selectively updates layouts, pages, or micro-slots across disparate routes sharing a common metadata tag key.
Micro-Slot<DinouCacheSlot>Isolates volatile component subtrees refreshed independently via refreshSlot() or targeted revalidateTag().

Server revalidation APIs are exported from the server-only endpoint "dinou/server", keeping browser client bundles clean, lightweight, and secure.

[!CAUTION] Strict Execution Rules: Where to Call Revalidation APIs

  • Only in Mutative Event Contexts: Revalidation APIs (revalidatePage, revalidatePath, revalidateTag) are mutative side-effects. They must only be invoked inside Server Functions ("use server") or HTTP Webhook endpoints (such as Headless CMS publishing events).
  • Never Inside Components or getProps: Calling revalidation during the component render phase or within getProps creates an infinite regeneration loop, causing severe CPU exhaustion and server crashes.

1. revalidatePage (Surgical Page Invalidation)

Exported from "dinou/server", revalidatePage(path) is specifically designed for scenarios where only the leaf page content has changed. It provides two critical advantages:

  1. Maximum Performance: Avoids the CPU a

Truncated — view the full README on GitHub.

framework
reactjs

roggc/dinou

Full-Stack React 19 Framework

JavaScript

11

459 commits

updated Oct 16, 2026

See the code

See what people are saying

SourceMessageScoreDate

Dinou v7: A React 19 RSC & Server Functions framework with PPR & vertical/horizontal segmentation that runs natively on Node.js, Cloudflare Workers, Netlify, Bun, and Deno (Zero lock-in) (r/reactjs)

Hi everyone! 👋 I’ve just released Dinou v7 (7.0.0), an ejectable full-stack React 19 framework built on web standards. My goal with Dinou v7 was to harness the full power of React 19 (Server Components, Server Functions, streaming SSR, and Partial Prerendering) while keeping the architecture…

0

Oct 14, 2026

README

Dinou

Documentation Version

Full-Stack React 19 Framework


The ejectable React 19 framework built on web standards. Build full-stack applications with native React Server Components (RSC), progressive streaming SSR, compile-time type-safe routing, dual-dimensional caching, and universal multi-platform deployment (Node.js, Bun, Deno, Cloudflare Workers, Netlify, and pure static hosts) with zero vendor lock-in.

Key Features

  • Native React 19 RSC & Progressive Web Streams: Render database queries, markdown parsers, and heavy backend dependencies exclusively on the server with 0kb client JavaScript overhead. Dinou streams progressive React Flight wire payloads and HTML over standard W3C Web Streams (renderToReadableStream) directly into the DOM with instant shell flushing.
  • Universal 3-Pass In-Memory Dual-Bundle Engine (Zero-Fork): Solves React 19's conflicting module conditions ("react-server" vs. "browser") within a single in-process pipeline via standard W3C Web Streams. Completely eliminates child_process.fork() and inter-process serialization latency, delivering an 82% memory reduction (~22 MB flat idle footprint) and enabling native execution on edge isolates.
  • Compile-Time Type-Safe Routing: Bidirectional type safety with zero runtime bundle overhead. Generates route definitions in real time: autocompletes and validates <Link href="..."> and useRouter().push(...), while strictly typing route parameters with PageProps<T>, LayoutProps<T>, RouteParams<T>, and ErrorProps<T>.
  • Persistent Nested Layouts & Zero-Remount: Folders in src/ map directly to URLs with nested layout.tsx files that wrap child pages without remounting during navigation, preserving client state, form inputs, audio playback, and scroll positions. Layouts feature independent data and cache lifecycles (layout_functions.ts).
  • Dual-Dimensional Caching (Horizontal & Vertical):
    • Horizontal (Route-Level): Static Site Generation (revalidate = false), strict seconds-based Incremental Static Regeneration (revalidate = N), and dynamic on-demand Server-Side Rendering (revalidate = 0).
    • Vertical (Component-Level): Micro-ISR via <DinouCacheSlot> to embed high-frequency reactive sections with micro-revalidation intervals (e.g., 5s) inside long-lived static shells, refreshable independently from the client (refreshSlot) or purged globally by tag (revalidateTag).
  • Partial Prerendering (PPR): Unifies instant static edge delivery with dynamic personalization in a single request. Serves an edge-cached static shell in under 20ms while personalized <Suspense> holes concurrently stream over a single HTTP connection.
  • Server Functions & Direct Suspense Streaming ("Dinou Pattern"): Asynchronous functions marked with "use server" can be called directly from Client Components ("use client") wrapped inside <Suspense> boundaries. They can return rendered React components over Flight RPC, eliminating synthetic REST/GraphQL API boilerplate.
  • Bundler Agnostic with 100% Feature Parity: Pre-configured, zero-config pipelines for esbuild (sub-millisecond dev server & HMR), Rollup (pristine ES module output for Cloudflare Workers & Edge), and Webpack (React Compiler and legacy npm compatibility) with identical dual-bundle output.
  • Universal Multi-Platform Deployment: Build once with React 19 and deploy the exact same application code to Node.js, Bun (standalone single-binary bun build --compile), Deno (native single-file binaries deno compile), Cloudflare Workers (native workerd + Cloudflare KV for ISR), Netlify, or pure static exports.
  • Universal W3C Web Middleware (onRequest): Intercept requests and high-frequency webhooks (Stripe, Clerk, GitHub) at the HTTP layer in dinou.config.mjs returning native Web Response objects in <2ms, completely bypassing React component rendering.
  • Hardened State-Preserving Fast Refresh: Under 5ms HMR powered by Rust (@swc/core) and WebSockets that patches updates into running applications while preserving active form inputs, counters, and component state across edits.
  • 100% Ejectable & AI-Friendly Architecture: Run npx dinou eject (or npm run eject) at any time to extract Dinou's complete compiler, server runtime, and React 19 flight pipeline directly into your project. Transparent, web standards-based, and free of proprietary black-box macros for seamless AI pair programming.

Table of contents

Getting Started

Start building modern, full-stack React 19 applications in seconds. Choose between the automated CLI generator or manual configuration for full architectural control.

Prerequisites

Before getting started, ensure your local environment meets the minimum requirements. Dinou is engineered to leverage modern JavaScript standards with zero transpile overhead:

  • Node.js 20.6.0+ (Required): Dinou utilizes Node's modern native ES module customization hooks (module.register) for zero-fork server execution and Web Standard APIs (Request, Response, ReadableStream, Headers) natively supported in modern Node.js versions.
  • Bun 1.1+ (Optional): If you plan to run on Bun or compile self-contained standalone native executables using npm run build:bun:compile.
  • Deno 2.0+ (Optional): If you target Deno or cross-compile single-file binaries for Linux, macOS, or Windows using npm run build:deno:compile.
  • Cloudflare Wrangler (Optional): Required if you deploy to Cloudflare Workers with npm run build:cloudflare and preview locally with wrangler dev.

Quick Start (CLI)

The fastest way to scaffold a ready-to-code Dinou application is using the official CLI starter. It sets up TypeScript, Tailwind CSS, sample Server Components, and the recommended directory layout:

# 1. Initialize a new Dinou application
npx create-dinou@latest my-app
# or
npm create dinou@latest my-app

# 2. Enter project directory
cd my-app

# 3. Launch development server (default: esbuild)
npm run dev

Open http://localhost:3000 in your browser to view your live application.

Manual Setup

Prefer to wire up everything manually? Follow these step-by-step instructions to configure Dinou from scratch with complete transparency:

1. Install Dependencies

Initialize your project folder and install React 19, React DOM, and the dinou framework package:

# Create and enter directory
mkdir my-dinou-app
cd my-dinou-app

# Initialize package.json
npm init -y

# Install React 19 and Dinou
npm install react react-dom dinou

# Install TypeScript definitions for IDE intellisense
npm install -D @types/react @types/react-dom

2. Project Structure & Files

Create the src directory with a root page (src/page.tsx) and the recommended root document layout (src/layout.tsx):

// src/page.tsx - React Server Component (RSC)
export default function Page() {
  return (
    <main className="p-8 max-w-4xl mx-auto space-y-4">
      <h1 className="text-3xl font-bold tracking-tight">
        Hello from Dinou!
      </h1>
      <p className="text-muted-foreground">
        Rendered natively on the server with React 19 and zero client JavaScript overhead.
      </p>
    </main>
  );
}
// src/layout.tsx - Root document layout
import type { ReactNode } from "react";

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>My Dinou App</title>
      </head>
      <body>{children}</body>
    </html>
  );
}

The root layout.tsx defines the shared <html> and <body> document shell, metadata, and global providers so you don't have to repeat them on each page.

my-dinou-app/
├── public/                # Static root assets (served directly at /)
│   ├── favicon.ico
│   ├── robots.txt
│   └── site.webmanifest
├── src/
│   ├── page.tsx           # Root page component (RSC)
│   ├── layout.tsx         # Root document layout (<html/head/body>)
│   └── globals.css        # Styles / Tailwind CSS
├── dinou.config.ts        # Framework configuration (optional: ppr, plugins)
├── dinou-env.d.ts         # Ambient framework typings
├── tsconfig.json          # TypeScript configuration
├── package.json
└── .gitignore

3. TypeScript Configuration

Dinou does not force you to use TypeScript—you are completely free to write your application in standard JavaScript (.js / .jsx). However, even for pure JavaScript projects, defining a tsconfig.json in your project root is strongly recommended:

  • Import Aliases: Dinou's build engines (esbuild, Rollup, Webpack) automatically read compilerOptions.paths from tsconfig.json to resolve clean path aliases like @/* into src/*.
  • IDE Autocomplete: With "allowJs": true, code editors (VS Code, Cursor) use tsconfig.json to provide full autocomplete, auto-imports, and path resolution across all .js and .jsx files.
{
  "compilerOptions": {
    "paths": {
      "@/*": ["src/*"]
    },
    "allowJs": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "module": "ESNext",
    "moduleResolution": "bundler"
  },
  "include": [
    "src",
    "dinou-env.d.ts"
  ]
}

Create dinou-env.d.ts in your root directory:

/// <reference types="dinou/env" />

The dinou/env reference automatically declares ambient typings for static assets (SVGs, images, web fonts) and CSS modules without manual type boilerplate.

4. Package Scripts

Add the standard Dinou scripts to your package.json to control development, multi-runtime production builds (Node, Cloudflare, Deno, Bun), binary compilation, static exports, and ejecting:

{
  "scripts": {
    "dev": "dinou dev",
    "build": "dinou build",
    "start": "dinou start",
    "dev:esbuild": "dinou dev:esbuild",
    "dev:rollup": "dinou dev:rollup",
    "dev:webpack": "dinou dev:webpack",
    "build:node": "dinou build:node",
    "start:node": "dinou start:node",
    "build:cloudflare": "dinou build:cloudflare",
    "build:deno": "dinou build:deno",
    "build:deno:compile": "dinou build:deno:compile",
    "start:deno": "dinou start:deno",
    "build:bun": "dinou build:bun",
    "build:bun:compile": "dinou build:bun:compile",
    "start:bun": "dinou start:bun",
    "export-static": "dinou export-static",
    "eject": "dinou eject"
  }
}

Note: For the complete catalog of targeted platform cross-compilation scripts (such as Windows, macOS, Linux ARM/x64), see the Complete CLI Scripts Catalog.

5. Configure .gitignore

Create a .gitignore file to ignore dependencies, environment variables, build outputs, and Dinou caches:

node_modules
.dinou
out
dist
.wrangler
.env
  • .dinou/ holds internal dev artifacts and runtime builds.
  • out/ contains pages generated by export-static.
  • dist/ stores compiled single-executable binaries (Deno / Bun).
  • .wrangler/ stores local state when testing Cloudflare Workers.

6. Start Development Server

Run the development server with the fast esbuild bundler (default), or choose Rollup or Webpack:

# Start with default ultra-fast esbuild
npm run dev

# Or start with Rollup or Webpack
npm run dev:rollup
npm run dev:webpack

Building for Production & Target Runtimes

Dinou provides both bundler freedom (esbuild, Rollup, Webpack) and universal runtime deployment (Node.js, Cloudflare Workers, Deno, Bun, or Static SSG) from a single codebase in src/ with zero code changes.

Bundler Flexibility (esbuild, Rollup, Webpack)

By default, Dinou uses esbuild for sub-second cold starts and in-memory compilation. When enterprise plugins or custom loaders are required, you can switch seamlessly to Rollup or Webpack with 100% feature parity:

# Start development with your preferred bundler
npm run dev          # Default: esbuild
npm run dev:rollup   # Dedicated Rollup in-memory engine
npm run dev:webpack  # Dedicated Webpack engine

For advanced compiler tuning, custom plugins, and SWC options, visit the Bundlers Guide.

First-Class Multi-Runtime Targets

Compile your application for any execution environment without altering component logic:

  • Node.js Production Server:
    npm run build:node && npm run start:node
    
  • Cloudflare Workers (Edge):
    npm run build:cloudflare && npx wrangler dev
    
  • Self-Contained Executable Binary (Deno / Bun):
    npm run build:deno:compile # or build:bun:compile
    
    Outputs standalone single-file executables directly to dist/.
  • Pure Static Site Export (SSG):
    npm run export-static
    
    Pre-rendered HTML and RSC Flight payloads output directly to out/.

For complete deployment guides, Docker setups, and cloud hosting instructions, see the Deployment Guide.

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.

[!TIP] 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 → /blog
  • src/blog/[slug]/page.tsx → /blog/hello-world
  • src/dashboard/settings/page.tsx → /dashboard/settings

1.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.tsx in 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

PatternFolder StructureURL Match ExampleParsed params
Staticsrc/about/page.tsx/about{}
Dynamicsrc/blog/[slug]/page.tsx/blog/hello{ slug: "hello" }
Optional Singlesrc/shop/[[category]]/page.tsx/shop
/shop/shoes
{ category: undefined }
{ category: "shoes" }

2.2. Catch-All & Optional Catch-All

PatternFolder StructureURL Match ExampleParsed params
Catch-all (Required)src/docs/[...slug]/page.tsx/docs/routing/params{ slug: ["routing", "params"] }
Optional Catch-allsrc/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]]:

✅ Allowed: Trailing Omission

You can omit optional segments only if they are omitted from the tail end of the URL:

  • /inventory/main/a1 ✅ (both provided: { warehouse: "main", aisle: "a1" })
  • /inventory/main ✅ (aisle omitted: { warehouse: "main", aisle: undefined })
  • /inventory ✅ (both omitted: { warehouse: undefined, aisle: undefined })
❌ Forbidden: Intercalated Gaps

You cannot omit an earlier optional parameter while setting a later one:

  • /inventory//a1 ❌ (empty gap / skipped warehouse while setting aisle: ambiguous URL structure rejected)

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.tsx components: Page({ params }: PageProps<"/blog/[slug]">)
  • layout.tsx components: Layout({ params, children }: LayoutProps<"/blog/[slug]">)
  • error.tsx pages: Error({ error, params }: ErrorProps<"/blog/[slug]">)
  • not_found.tsx 404 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]">) in page_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>
  );
}

[!IMPORTANT] Strict Signature Rule: In 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 };
}

Guaranteed behavior across all rendering modes:

  • 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:

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 (page_functions.ts & Server Functions)

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:

  1. The Layout Plane: 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
  2. The Page Plane: 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 FileApplies ToArchitectural Effect
reset_layoutlayout.tsxResets 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_layoutpage.tsx, not_found.tsx, error.tsxBypasses 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_errorerror.tsxGranular override: prevents parent layouts from wrapping the error boundary UI only, while preserving layouts for the regular page.
no_layout_not_foundnot_found.tsxGranular override: prevents parent layouts from wrapping the custom 404 page only, while preserving layouts for the regular page.
Example: Standalone Landing Page
  • src/landing/layout.tsx
  • 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>
  );
}

[!NOTE] 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.

Scoped to Route Segment
// 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>
  );
}
Global Root 404 Fallback
// 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>
  );
}
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:

// 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"
              >
                &rarr; {item.title}
              </Link>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

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.
Multiple Root Layouts in Practice

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:

src/
├── (marketing)/                # Public layout branch
│   ├── layout.tsx              # Navbar, Footer, Hero
│   ├── page.tsx                # → /
│   ├── about/page.tsx          # → /about
│   └── pricing/page.tsx        # → /pricing
└── (auth)/                     # Minimalist auth branch
    ├── layout.tsx              # Centered card, no Navbar
    ├── login/page.tsx          # → /login
    └── register/page.tsx       # → /register

How Dinou Resolves Layouts:

  • Independent Filesystem Paths: src/(marketing)/layout.tsx and src/(auth)/layout.tsx live in distinct disk directories, so there is zero file collision.
  • Request to /pricing: Dinou crawls into (marketing), matches pricing/page.tsx, and wraps it exclusively with the marketing layout.
  • Request to /login: Dinou crawls into (auth), matches login/page.tsx, and wraps it exclusively with the auth layout.
  • Root Hierarchy: If you also place a top-level 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

Because parenthesized folder names are stripped from the public URL, different route groups cannot define identical URL paths. For example, creating both 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

The sole and specific purpose of prefixing a directory with an underscore (_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:

src/blog/
├── page.tsx             → /blog (Active public route)
├── components/          (No page.tsx → already safe without "_")
│   └── post-card.tsx
└── _legacy_v1/          (Contains page.tsx, but "_" forces scanner to skip it!)
    ├── page.tsx         (SKIPPED — NOT a route)
    └── [slug]/page.tsx  (SKIPPED — NOT a route)

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):

Folder Structure:

  • src/dashboard/@sidebar/page.tsx
  • src/dashboard/@analytics/page.tsx
  • src/dashboard/page.tsx (main children)
  • src/dashboard/layout.tsx

Layout Prop Injection: 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:

  • Slot with error.tsx: If the slot fails, only that specific slot renders its local error UI. The rest of the page and sibling slots remain completely operational.
  • Slot without error.tsx: If an unhandled exception occurs in a slot with no error.tsx, Dinou safely falls back to rendering null for that slot without taking down the page.
  • Layout & Sibling Resilience: Sibling slots, the main children page, and parent layouts remain fully mounted and interactive even if an individual slot fails to render.

Master Dinou's client-side SPA navigation: smooth soft transitions, automatic hover prefetching, the Client Router Cache lifecycle, cache busting with the fresh prop, imperative useRouter controls, and Directory-First relative routing.

[!TIP] Looking for Type-Safe Navigation? Dinou generates automatic route types so <Link href="..."> and router.push("...") provide zero-runtime autocomplete and compile-time error checking.


1. Navigation Overview

In Dinou, navigating between routes does not trigger a full browser refresh (window.location.reload). Instead, Dinou operates as a high-performance single-page application (SPA):

  • Preserved Shell: Persistent layouts (layout.tsx) never unmount, maintaining video playback, active form inputs, scroll positions, and local component state across page changes.
  • Flight Streaming: Only the newly navigated route's Server Components are fetched over the wire via React Flight payloads.
  • Concurrent Transitions: Transitions execute inside React 19's startTransition, ensuring that the current UI stays fully responsive and interactive during data fetching.

The primary way to navigate between pages is the <Link> component imported from "dinou". It intercepts click events, prevents hard reloads, and handles background prefetching:

import { Link } from "dinou";

export function NavigationBar() {
  return (
    <nav className="flex gap-4">
      {/* ✅ Type-Safe href: autocompleted in your IDE via DinouRoute */}
      <Link href="/dashboard">Dashboard</Link>

      {/* ✅ Dynamic interpolation typed against registered routes */}
      <Link href={`/blog/${post.slug}`}>Post</Link>

      {/* Disable prefetch for heavy routes or rarely visited pages */}
      <Link href="/analytics" prefetch={false}>
        Analytics
      </Link>

      {/* ❌ TypeScript compile error on invalid/dead links when using type-safe routing */}
      {/* <Link href="/broken-link">Broken</Link> */}
    </nav>
  );
}

[!NOTE] Native HTML <a> Anchor Elements: In Dinou, standard HTML <a> tags also automatically trigger client-side soft navigation via global event delegation. However, <Link> is recommended because it provides compile-time type safety, background prefetching on hover, and the fresh prop.


3. Programmatic Navigation (useRouter)

To navigate imperatively from event handlers, timers, or form submissions, use the useRouter() hook inside Client Components ("use client"):

"use client";

import { useRouter } from "dinou";

export default function LoginButton() {
  const router = useRouter();

  const handleLogin = async () => {
    await loginUser();
    
    // Navigate programmatically
    router.push("/dashboard");
    
    // Or force fresh data after mutation:
    // router.push("/dashboard", { fresh: true });
  };

  return <button onClick={handleLogin}>Log In</button>;
}

Method Reference

MethodBehavior & Description
router.push(href, options)Navigates to the route and adds a new entry to the browser history stack. Supports { fresh: true }.
router.replace(href, options)Navigates to the route, replacing the current history entry without pushing a new history state. Supports { fresh: true }.
router.back()Navigates back to the previous page in history (equivalent to clicking the browser Back button).
router.forward()Navigates forward to the next page in the history stack.
router.refresh()Soft Reload: Re-fetches the current route data from the server and updates server components without losing client React state or scroll position.
router.refreshSlot(slotId)Re-fetches and updates an individual <DinouCacheSlot> component without re-rendering the surrounding page.

4. Client Router Cache & The fresh Prop

To deliver instant page transitions without unnecessary server requests, Dinou features an in-memory Client Router Cache. Understanding how this cache operates is key to knowing when and why to use the fresh prop.

4.1. How the Client Router Cache Works

During client execution, Dinou maintains three dedicated in-memory caches in the browser tab:

  1. pageCache: Stores prefetched and visited RSC promises keyed by pathname + search.
  2. scrollCache: Retains exact window and container scroll coordinates keyed by history state ID.
  3. popstate: Reads from pageCache and restores scroll without network requests on back/forward.
The Navigation Lifecycle
  1. On Hover: When the mouse enters a <Link href="/pricing">, Dinou automatically prefetches the route payload in the background and stores the pending RSC promise directly into pageCache.
  2. On Click: The router initiates navigation to "/pricing". Because the payload was already prefetched into pageCache, the transition renders instantly with 0ms network latency.
  3. On Back / Forward: Browser popstate events read from pageCache and restore the previous scroll position from scrollCache without making any network requests.

4.2. Cache Invalidation with the fresh Prop

While caching provides instantaneous navigation, pages with volatile data or pages visited immediately after a mutation (e.g., checkout, profile updates, or inventory counters) need fresh server state. The fresh prop provides explicit, predictable cache busting:

What happens under the hood when fresh is true:
  • No Stale Prefetch: Hovering over a <Link fresh> does not prefetch potentially stale data before the user actually clicks.
  • Cache Eviction: Upon navigation, the router explicitly runs pageCache.delete(targetUrl), removing any prior cached RSC promise.
  • Live Re-Execution: Issues a fresh HTTP request directly to the server, re-executing server components and page functions with latest data.
Usage Examples:
// Ideal for live feeds, stock tickers, or carts
<Link href="/live-feed" fresh>
  Live Feed
</Link>
// Navigate after a mutation to force fresh server data
const handleUpdate = async () => {
  await updateProfile(data);
  router.push("/profile", { fresh: true });
};

4.3. Comparison: When to Use Which Mechanism

Mechanism / ScopeCache BehaviorNetwork RequestTypical Use Case
Standard <Link>Uses pageCache if prefetched or visited0ms if in cache; single request on first visitGeneral navigation, blogs, docs, marketing pages
<Link fresh> / { fresh: true }Purges pageCache.delete(url)Always fetches live RSC payload from serverPost-mutation redirects, active carts, volatile feeds
router.refresh()Purges pageCache.delete(currentPath)Re-fetches current route in-placePolling, server function response handling, reload button
Hard Reload (F5)Destroys all memory (all caches lost)Full HTML document download + re-hydrationEmergency fallback; loses all client state

5. Route Transitions (useNavigationLoading)

In Dinou, all client-side navigations execute concurrently inside React 19's startTransition. This ensures that the current view remains fully interactive and visible while the target page's Server Components stream in from the server.

To inform users that a transition is occurring behind the scenes, Dinou provides the useNavigationLoading() hook:

  • Returns true while a route payload is actively streaming or preparing in the background, and resets to false the moment React 19 commits the new page tree.
  • Evaluates to false during server-side rendering (SSR), preventing hydration mismatches and flickering when the document first mounts.

Example: Global Top Navigation Progress Bar

Mount a client loading indicator in your root layout to provide visual feedback (similar to GitHub or YouTube) whenever a route transition is pending:

"use client";

import { useNavigationLoading } from "dinou";

export function NavigationProgressBar() {
  const isNavigating = useNavigationLoading();

  if (!isNavigating) return null;

  return (
    <div
      role="progressbar"
      aria-label="Page loading"
      className="fixed top-0 left-0 right-0 z-50 h-1 bg-blue-500/20 overflow-hidden pointer-events-none"
    >
      <div className="h-full bg-blue-600 dark:bg-blue-400 animate-pulse w-full transition-all duration-300" />
    </div>
  );
}

[!TIP] Because prefetched links resolve instantly in 0ms directly from pageCache, this progress bar only surfaces on fresh fetches (such as navigation with fresh: true or un-prefetched dynamic routes), keeping transitions ultra snappy.


6. Relative Routing Rules

Dinou resolves relative navigation paths (such as about, ./about, or ../dashboard) using a Directory-First Convention. This provides intuitive navigation that accurately mirrors your file system.

In a standard web browser, a URL like /blog/hello (without a trailing slash) treats hello as a file name, so a link to about resolves to /blog/about.

In Dinou, every route is treated as a directory because physically, each page is a folder containing a page.tsx component (e.g. src/blog/hello/page.tsx). Resolving relatively from a page navigates relative to that route's folder:

Current PathRelative TargetResolved Path
/blog/helloabout/blog/hello/about
/blog/hello./about/blog/hello/about
/blog/hello../about/blog/about

This rule applies uniformly across all navigation methods, including the <Link> component and programmatic calls like router.push().

Type-Safe Routing

Dinou v7 introduces a built-in, compile-time Type-Safe Routing engine with zero runtime overhead. During development and build time, Dinou automatically inspects your src/ directory and generates ambient TypeScript definitions in .dinou/types/routes.d.ts.


1. Overview & Zero-Runtime Guarantee

Unlike traditional routing setups that require external schema validators or proprietary bundler plugins, Dinou generates strict TypeScript definitions directly from your file system.

1.1. Outward Navigation vs. Inward Consumption

The system operates across two complementary architectural planes:

  1. Outward Navigation (Level 1): Protects outbound transitions. When using <Link href="...">, router.push("..."), or passing routes to reusable UI components with DinouRoute, TypeScript autocompletes valid routes, verifies dynamic template strings, and raises compile-time errors on dead links or typos.
  2. Inward Consumption (Level 2): Enforces inbound parameter safety. Pages, layouts, error boundaries, and server data loaders consume parameters strongly typed by route template strings via PageProps<T>, LayoutProps<T>, ErrorProps<T>, and RouteParams<T>.

1.2. Zero-Runtime Cost (0 Extra Bytes)

All route type validation occurs entirely within the TypeScript compiler. The emitted .dinou/types/routes.d.ts file contains ambient type declarations and is never bundled into your client JavaScript or server execution bundle:

  • 0 bytes added to production client bundles.
  • 0 runtime performance overhead on route matching or transitions.
  • 0 external dependencies required for type validation.

2. Quick Start for Developers

Import <Link> or useRouter from "dinou". Your IDE will immediately suggest all valid static routes, parameterized dynamic routes, relative paths, queries, and hashes:

import { Link, useRouter } from "dinou";

export default function Navigation({ post }: { post: { slug: string } }) {
  const router = useRouter();

  return (
    <nav className="flex gap-4">
      {/* ✅ Instant autocompletion in your IDE */}
      <Link href="/docs/routing">Routing Guide</Link>

      {/* ✅ Dynamic routes with typed template strings */}
      <Link href={`/blog/${post.slug}`}>View Post</Link>

      {/* ✅ Seamless support for queries, hashes, relative paths, and external URLs */}
      <Link href="/demo?tab=settings#section-1">Settings</Link>
      <Link href="../contact">Relative</Link>
      <Link href="https://github.com/roggc/dinou">GitHub</Link>

      {/* ❌ TypeScript compile-time error when pointing to a non-existent route */}
      {/* <Link href="/unknown-route" /> */}

      <button onClick={() => router.push("/docs/api-reference")}>
        View API Reference
      </button>
    </nav>
  );
}

2.2. Level 2: Typed Pages, Layouts & Server Functions

Type your components and data fetching functions by passing the route template string as a generic:

import type { PageProps, LayoutProps, RouteParams } from "dinou";

// 1. src/blog/[slug]/page.tsx
export default function BlogPost({ params }: PageProps<"/blog/[slug]">) {
  // ✅ params.slug is strongly typed as string
  return <h1>Post: {params.slug}</h1>;

  // ❌ TypeScript error: Property 'id' does not exist on type '{ slug: string }'
  // return <h1>{params.id}</h1>;
}

// 2. src/blog/[slug]/layout.tsx
export function BlogLayout({ children, params }: LayoutProps<"/blog/[slug]">) {
  // ✅ params.slug is strongly typed; children is typed as ReactNode
  return (
    <section>
      <header>Category: {params.slug}</header>
      <main>{children}</main>
    </section>
  );
}

// 3. src/blog/[slug]/page_functions.ts
export async function getProps(params: RouteParams<"/blog/[slug]">) {
  // ✅ Directly types the raw params object without params: any
  return {
    postTitle: `Post: ${params.slug}`,
  };
}

2.3. Quick Reference: Core Types

Exported TypePrimary PurposeCanonical Example
PageProps<T>React Page components (page.tsx)Page({ params }: PageProps<"/blog/[slug]">)
LayoutProps<T>React Layout components (layout.tsx)Layout({ children, params }: LayoutProps<"/blog/[slug]">)
ErrorProps<T>React Error Page components (error.tsx)Error({ error, params }: ErrorProps<"/blog/[slug]">)
RouteParams<T>Pure functions, data loaders & services (getProps, API helpers)getProps(params: RouteParams<"/blog/[slug]">)
DinouRouteURL props, <Link>, router.push() & custom navigation componentsinterface NavProps { href: DinouRoute }

3. Parameter Mapping Reference Table

Dinou deterministically transforms your file-system folder naming conventions into strict TypeScript definitions:

Filesystem ConventionInferred Parameter TypeRoute ExampleType of params
Static RouteNo parameterssrc/about/page.tsxRecord<string, never>
Single Dynamic [id]stringsrc/blog/[id]/page.tsx{ id: string }
Catch-All [...slug]string[]src/docs/[...slug]/page.tsx{ slug: string[] }
Optional Dynamic [[lang]]string | undefinedsrc/[[lang]]/page.tsx{ lang?: string }
Optional Catch-All [[...opt]]string[] | undefinedsrc/shop/[[...opt]]/page.tsx{ opt?: string[] }
Multiple Dynamic SegmentsCombined by segmentsrc/users/[uId]/posts/[pId]/page.tsx{ uId: string; pId: string }
Nested OptionalsPositional dependentsrc/inv/[[wh]]/[[aisle]]/page.tsx{ wh?: string; aisle?: string }

[!WARNING] Be cautious with trailing slashes in dynamic links!

In TypeScript, template literal types evaluate ${string} as matching any string, including the empty string "" (because "" extends string evaluates to true).

The scenario: Consider a nested route such as src/posts/[id]/[slug]/page.tsx, whose generated template literal type is `/posts/${string | number}/${string | number}`.

If a developer accidentally leaves off the second parameter but appends a trailing slash:

<Link href={`/posts/${id}/`}>View</Link>

TypeScript matches the first parameter with id, the slash with literal /, and the second parameter with empty string "". However, at runtime Dinou normalizes trailing slashes, requesting /posts/123 (which results in a 404 Not Found because no page.tsx exists at the parent segment).

Best Practice: Always avoid trailing slashes when building dynamic URLs. Written without the trailing slash (`/posts/${id}`), TypeScript accurately flags the missing parameter at compile-time.


4. Advanced Usage & Real-World Recipes

4.1. IDE Autocomplete Workflow (Ctrl + Space)

When typing a component, type PageProps<""> and press Ctrl + Space (or Cmd + Space on macOS). Your editor will list every registered route pattern in the project:

import type { PageProps } from "dinou";

// Trigger Ctrl + Space inside "" to pick from project routes
export default function BlogPost({ params }: PageProps<"/blog/[slug]">) {
  return <h1>{params.slug}</h1>;
}

4.2. Typed Error Pages with ErrorProps

Error boundaries in Dinou v7 strongly type both the captured error and the current route's parameters:

"use client";

import { useRouter, type ErrorProps } from "dinou";

export default function BlogPostError({ params, error }: ErrorProps<"/blog/[slug]">) {
  const router = useRouter();

  return (
    <div className="error-card p-6 border rounded-xl bg-destructive/10">
      <h2>Failed to load post: {params.slug}</h2>
      <p className="text-sm text-muted-foreground">{error.message}</p>
      {/* Soft reload via Dinou client router */}
      <button onClick={() => router.refresh()}>Try Again</button>
    </div>
  );
}

4.3. Custom Navigation Components with DinouRoute

When creating design-system components (e.g., custom buttons, sidebar items, dropdown menus), use the exported DinouRoute union type to pass through autocomplete and validation:

import type { DinouRoute } from "dinou";
import { Link } from "dinou";

interface NavButtonProps {
  href: DinouRoute; // 👈 Inherits full autocompletion and route validation
  label: string;
}

export function NavButton({ href, label }: NavButtonProps) {
  return (
    <Link href={href} className="px-3 py-1.5 rounded-lg bg-blue-600 text-white font-medium">
      {label}
    </Link>
  );
}

// In your application code:
const menu: { label: string; href: DinouRoute }[] = [
  { label: "Overview", href: "/docs" },              // ✅ Autocompleted
  { label: "Routing", href: "/docs/routing" },        // ✅ Autocompleted
  // { label: "Broken", href: "/non-existent-page" }, // ❌ TypeScript compile error!
];

4.4. Dynamic Navigation with Typed Interpolation

Dinou types template literal strings against dynamic route definitions, catching broken template strings while accepting valid parameters:

import { Link, useRouter } from "dinou";

interface Product {
  id: string;
  name: string;
}

export function ProductCard({ product }: { product: Product }) {
  const router = useRouter();

  return (
    <div className="card p-4 border rounded-xl">
      {/* String interpolation verified against `/products/${string | number}` */}
      <Link href={`/products/${product.id}`}>{product.name}</Link>

      <button onClick={() => router.push(`/products/${product.id}`)}>
        View Details
      </button>
    </div>
  );
}

5. Technical Architecture: Under the Hood

Dinou coordinates compilation, route scanning, and TypeScript declarations through a zero-dependency architecture:

  1. Filesystem src/ Directory: Synchronous read and structural crawl of all page and layout files.
  2. Scanner (generate-route-types): Low-overhead in-memory scanner completes in under 2ms.
  3. Type Emission (.dinou/types/routes.d.ts): Emits ambient module declarations extending the "dinou" package interface with exact union types (DinouRoute) and parameter maps (DinouRouteParams).
  4. IDE & TypeScript Consumption: VS Code, Cursor, and the TypeScript compiler immediately read the declarations via tsconfig.json without requiring editor restarts or language server reboots.

6. Lifecycle: Dev Server & Production Builds

How Dinou keeps route typings strictly synchronized across local development and deployment pipelines:

  • 6.1. Local Dev (Hot Watcher): During npm run dev, Dinou arms a low-overhead filesystem watcher on src/. Whenever a page.(tsx|jsx|ts|js) file is added, renamed, or deleted, .dinou/types/routes.d.ts regenerates in under 2ms.
  • 6.2. Production Builds: Running npm run build runs route type generation as step 0 before bundling. If code references a non-existent route, TypeScript type checking fails the build before deploying.
  • 6.3. CI / Type Checkers: Run npx tsc --noEmit in CI pipelines to validate link integrity across the entire project deterministically.

7. Backward Compatibility Guarantee

  • 100% Backward Compatible: Existing components without explicit generic parameters continue to work without modification. You can continue using export default function Page({ params }: any) or generic PageProps during progressive migrations.
  • Graceful Fallback Mode: If .dinou/types/routes.d.ts has not yet been generated (e.g., fresh clone prior to npm run dev or npm run build), types gracefully fallback to permissive strings (string & {}), ensuring clean git checkout and initial install steps without blocking developer workflows.

Component Architecture: Server and Client Components

Dinou is built natively on the React 19 Server Components (RSC) specification. Rather than treating your entire application as a single monolithic client-side bundle, Dinou partitions components into two distinct execution environments:

  • Server Components (RSC): The default component type in Dinou. Rendered to HTML on the server (SSR) with zero client JavaScript. Never hydrated in the browser, eliminating bundle overhead while providing direct access to databases, the filesystem, and private secrets.
  • Client Components: Opt-in with "use client". Also pre-rendered to HTML on the server (SSR) for instant first paint, and then hydrated with interactive JavaScript in the browser to enable local state (useState, useActionState), event listeners (onClick), and browser APIs.

1. Overview & Dual-Component Model

Dinou's dual-component model allows you to seamlessly mix server-rendered backbones with interactive client islands:

FeatureServer Components (RSC)Client Components
Default in Dinou?Yes (all components inside src/)No (requires explicit "use client")
Execution EnvironmentServer only (Node.js, Bun, Deno, Cloudflare Workers)Server (pre-rendering SSR) + Browser (hydration)
Client JS Bundle Impact0 KB (Zero client JavaScript overhead)Ships component JS code to the browser
Backend & Secret AccessDirect access to DB, ORM, filesystem, private env varsCannot access backend resources or private secrets
Interactivity & StatePure static output; no state or DOM event listenersFull React state, hooks, effects, and DOM events
Data Fetching PatternDirect top-level await inside component bodyProps passed from RSC, React 19 use(), or Server Functions

1.1. The RSC Mental Model

Think of Server Components as the backbone of your application that orchestrates data fetching, layouts, and non-interactive UI (rendering pure HTML with zero client JavaScript). Client Components act as interactive "islands" or leaves embedded inside the server tree.

The server streams HTML and React Flight JSON representations across an open HTTP connection. The browser consumes this stream without ever downloading the JavaScript source code of your Server Components.


2. Server Components (RSC)

In Dinou, every component inside src/ is a Server Component by default unless explicitly marked with "use client". They execute exclusively on the server and never ship their implementation code to the browser.

2.1. Capabilities & Backend Access

  • Direct Async Execution: Server Components can be async functions. You can await database queries, ORM calls, microservices, or files directly inside the component body.
  • Zero Client Bundle Impact: Large dependencies imported into Server Components (e.g., Markdown parsers, SQL drivers, encryption packages) are never downloaded or executed by the browser.
  • Secure Secret Access: Database credentials, private API keys, and environment variables stay strictly on the server without risk of client exposure.
  • Automatic Streaming with <Suspense>: When slow asynchronous operations are wrapped in React <Suspense>, the layout shell and fallback UI can be delivered immediately without waiting for slow queries, while data streams progressively into the HTTP response.
// src/users/page.tsx (Server Component by default)
import { getContext } from "dinou";
import { db } from "@/lib/db"; // Never bundled to the client!

export default async function UsersPage() {
  // Direct backend query without an intermediary REST API
  const users = await db.user.findMany({ take: 10 });

  return (
    <main className="p-8 max-w-4xl mx-auto">
      <h1 className="text-2xl font-bold">Registered Users</h1>
      <ul className="mt-4 divide-y">
        {users.map((user) => (
          <li key={user.id} className="py-2 flex justify-between">
            <span>{user.name}</span>
            <span className="text-xs font-mono text-muted-foreground">{user.email}</span>
          </li>
        ))}
      </ul>
    </main>
  );
}

[!NOTE] TTFB & Execution Timing Note:

  • Static by Default (SSG): In Dinou, this page is pre-rendered at build time by default. The database query runs during npm run build, serving static HTML to visitors with instant TTFB (0ms server wait).
  • Dynamic Routes: If the route is marked dynamic (e.g. revalidate = 0 in page_functions.ts or reading cookies), a top-level await blocks the entire HTTP response until the query finishes. To preserve instant TTFB on dynamic routes, delegate the await to a child component wrapped in React <Suspense>.
Non-Blocking Streaming with React <Suspense>

By delegating asynchronous queries to child components wrapped in <Suspense>, the outer layout and fallback UI are sent immediately:

// src/users/page.tsx - Non-blocking streaming layout
import { Suspense } from "react";
import { db } from "@/lib/db";
import { UsersSkeleton } from "./skeleton";

export default function UsersPage() {
  return (
    <main className="p-8 max-w-4xl mx-auto">
      {/* 1. The outer shell is sent immediately (instant TTFB) */}
      <h1 className="text-2xl font-bold">Registered Users</h1>

      {/* 2. Displays fallback while the child query resolves */}
      <Suspense fallback={<UsersSkeleton />}>
        <UsersList />
      </Suspense>
    </main>
  );
}

// Child component handles the async query independently
async function UsersList() {
  const users = await db.user.findMany({ take: 10 });
  return (
    <ul className="mt-4 divide-y">
      {users.map((u) => (
        <li key={u.id} className="py-2 flex justify-between">
          <span>{u.name}</span>
          <span className="text-xs font-mono text-muted-foreground">{u.email}</span>
        </li>
      ))}
    </ul>
  );
}
How <Suspense> Behaves Across Rendering Strategies
StrategyCompiler & Runtime BehaviorFallback Skeleton BehaviorTrade-off
Pure SSG (Static)The compiler automatically awaits all Suspense boundaries during npm run build and writes the resolved HTML directly into the static artifact.Visitors never see the loading skeleton because the fallback is eliminated once the build settles.0ms TTFB from CDN edge, but data reflects build-time state (stale until next build/revalidation).
Pure Dynamic SSRThe server flushes the layout shell and <Suspense fallback={<Skeleton />}> immediately over the open HTTP connection.The user sees the skeleton while waiting, and React streams the resolved HTML chunk when the query finishes.100% fresh data and non-blocking layout, but incurs continuous server compute costs, DB load, and request-time latency.
Partial Prerendering (PPR)The outer layout shell and the fallback skeleton are pre-rendered at build time and served immediately with 0ms TTFB from the CDN edge.The skeleton renders instantly from static cache, while the server concurrently executes only the dynamic query and streams live data into the hole.Combines the instant delivery of SSG with the fresh, live data of dynamic SSR at minimal server compute cost.

2.2. Constraints & Limitations

Because Server Components do not execute in the browser, they cannot use browser-specific or client-interactive features:

  • Cannot use state or lifecycle hooks: useState, useEffect, useReducer, useLayoutEffect.
  • Cannot attach interactive client DOM event listeners: onClick, onChange, onSubmit, onKeyDown.
  • Cannot access browser globals: window, document, localStorage, navigator, geolocation.
  • Cannot consume (read) React Context directly: Server Components cannot call useContext() or use(Context). (While Server Components can render <Context> providers for client children, they cannot read context values themselves).

3. Client Components

Client Components provide rich, real-time client-side interactivity. They enable event handlers, local component state, animations, and access to browser APIs.

3.1. Interactivity & Browser APIs

Client Components have complete access to the client-side React ecosystem:

  • Full state management with useState, useReducer, and React 19 useActionState.
  • Side effects, timers, and DOM subscriptions via useEffect and useLayoutEffect.
  • DOM event handling (clicks, hovers, drags, keystrokes, form submissions).
  • Browser Web APIs (localStorage, Geolocation, IntersectionObserver, Audio).
  • Consuming React Context providers created on the client.

3.2. Server Pre-Rendering Notice

A common point of confusion is assuming Client Components execute exclusively in the browser. In Dinou, Client Components are pre-rendered on the server to HTML during initial page load to ensure instant First Contentful Paint (FCP) and optimal SEO indexing before hydrating in the browser.

[!WARNING] SSR Execution Safety Rule: Because Client Components run on the server during pre-rendering, accessing browser globals directly in the root of the component function (e.g. const token = localStorage.getItem("token")) will throw a server runtime error.

Always defer browser-only API access inside useEffect or guard it with an environment check:

if (typeof window !== "undefined") {
  const token = localStorage.getItem("auth_token");
}

4. The "use client" Directive

To declare a component as a Client Component, add the "use client" directive at the very top of the file, before any import statements or comments.

4.1. Establishing Module Boundaries

The directive is not a runtime function call—it is an instruction to the bundler (esbuild, Rollup, Webpack) that defines the network split point:

"use client"; // Must be at the very top of the file!

import { useState } from "react";

export function Counter({ initial = 0 }: { initial?: number }) {
  const [count, setCount] = useState(initial);

  return (
    <button
      onClick={() => setCount((c) => c + 1)}
      className="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium"
    >
      Count: {count}
    </button>
  );
}

4.2. Import Cascading Behavior

Once a file declares "use client", all components, utility modules, and third-party libraries imported by that file are automatically treated as part of the client bundle. You do not need to add "use client" to child modules imported by a Client Component.


5. Data Serialization Boundary

When passing props from a Server Component to a Client Component, data crosses the network boundary (serialized as React Flight JSON). Props passed across this boundary must be serializable.

5.1. Supported Data Types

  • Serializable (Allowed across boundary):
    • Primitives: string, number, boolean, null, undefined, bigint.
    • Plain Objects & Arrays: Objects with serializable values (e.g. { id: 1, name: "Dinou" }).
    • Dates: Date objects are serialized and reconstructed by React on the client.
    • React Elements (JSX): Server Components passed as children or named slot props.
    • Promises: Passed directly from Server to Client (read with React 19's native use()).
    • Server Functions: Bound action functions declared with "use server".
  • Non-Serializable (Blocked across boundary):
    • Client Functions: Cannot pass client callbacks (e.g. onClick={() => ...}) from Server Components.
    • Class Instances: Complex prototypes, database connections, and ORM class instances (e.g. Prisma models with methods) will lose their prototype.
    • Symbols & WeakMaps: Non-enumerable or memory-bound JavaScript data structures.
    • Browser DOM Elements: Native HTML DOM nodes (window, document.body).

5.2. Passing Promises & React 19 use()

React 19 supports passing an unresolved Promise as a prop from a Server Component to a Client Component. The Client Component reads the promise synchronously using the use() hook, automatically triggering Suspense fallbacks until the promise settles:

// Server Component
import { Suspense } from "react";
import { UserProfile } from "./user-profile"; // "use client"

export default function Page() {
  // Start fetching without awaiting!
  const userPromise = fetchUserData();

  return (
    <Suspense fallback={<div>Loading profile skeleton...</div>}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}
// Client Component (user-profile.tsx)
"use client";
import { use } from "react";

export function UserProfile({ userPromise }: { userPromise: Promise<{ name: string; role: string }> }) {
  // Unwraps the promise value seamlessly inside Suspense without useEffect
  const user = use(userPromise);
  return <h2>Hello, {user.name} ({user.role})</h2>;
}

6. Architectural Patterns & Best Practices

Dinou empowers you to choose the exact architectural balance between server performance and client ergonomics. Follow these battle-tested patterns for optimal architecture:

Keep your layouts, route pages, and static wrappers as Server Components. Only push the "use client" boundary down to small, interactive "leaf" components (such as an upvote button, search box, or dropdown menu).

src/dashboard/layout.tsx              → Server Component (zero client JS)
src/dashboard/page.tsx                → Server Component (fetches data directly)
src/dashboard/_components/search-bar.tsx  → "use client" (interactive leaf)
src/dashboard/_components/action-button.tsx  → "use client" (interactive leaf)

6.2. Composition: Server Components as Children

A Client Component cannot directly import a Server Component (because bundling a Server Component into the client module would defeat the RSC boundary). However, a Client Component can receive Server Components as children or via named JSX props!

// Client Component: ModalContainer.tsx
"use client";
import { useState, type ReactNode } from "react";

export function ModalContainer({ children }: { children: ReactNode }) {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>Open Modal</button>
      {isOpen && (
        <div className="modal-backdrop">
          {/* children is rendered on the server and passed as pre-rendered JSX! */}
          {children}
        </div>
      )}
    </div>
  );
}

// Server Component: page.tsx
import { ModalContainer } from "./ModalContainer";
import { HeavyServerProfile } from "./HeavyServerProfile"; // Server Component

export default function Page() {
  return (
    <ModalContainer>
      {/* HeavyServerProfile remains 100% on the server! */}
      <HeavyServerProfile />
    </ModalContainer>
  );
}

6.3. Client-First with Server Data (getProps)

If you are building an interactive canvas, video editor, or complex SPA dashboard and prefer traditional React where you can use useState and onClick anywhere in the page without boundary splitting, use Dinou's Client-First Model:

// src/canvas/page_functions.ts - Runs securely on the server
import { db } from "@/lib/db";

export async function getProps(params: { id: string }) {
  const project = await db.projects.findById(params.id);
  return { project };
}

// src/canvas/page.tsx - Entire page is a Client Component
"use client";
import { useState } from "react";

export default function CanvasPage({ project }: { project: any }) {
  // Full access to state, effects, and events everywhere in the file!
  const [layers, setLayers] = useState(project.initialLayers);

  return (
    <div onClick={() => console.log("clicked")}>
      <Toolbar onAddLayer={(l) => setLayers([...layers, l])} />
      <Stage layers={layers} />
    </div>
  );
}

[!TIP] Advantage: Fast development speed and traditional React ergonomics, while preserving secure backend data fetching and SSR pre-rendering.

6.4. Granular Caching with <DinouCacheSlot> (Vertical Micro-ISR)

A common architectural trap in dynamic RSC applications is converting heavy server widgets into Client Components just to gain client-side memory caching (such as SWR or TanStack Query). In Dinou, <DinouCacheSlot> provides component-level server caching (Vertical Segmentation) directly in the RSC layer.

  • The Common Mistake (Demoting to Client): To avoid running an expensive 2-second database query on every dynamic page load, developers often convert the widget to "use client" with a client cache. This bloats the client JS bundle, loses direct database access, exposes internal API endpoints, and introduces client-side network waterfalls.
  • Dinou's Solution (<DinouCacheSlot>): Keep the component on the server! <DinouCacheSlot> caches the rendered RSC Flight payload of that specific subtree on the server for a designated TTL (e.g. 300s). The rest of the page remains 100% dynamic, while the expensive subtree serves in 0ms.
Can a <DinouCacheSlot> Contain Client Components?

Yes, absolutely. While <DinouCacheSlot> itself is an async Server Component that executes exclusively on the server, its children can freely render interactive Client Components. Dinou caches the Client Component reference along with its pre-computed props. The client hydrates it normally with full interactivity (events, state, animations):

// 1. Client Component: interactive-chart.tsx (Hydrates with 100% interactivity)
"use client";
import { useState } from "react";

export function InteractiveChart({ data }: { data: any }) {
  const [zoom, setZoom] = useState(1);
  return (
    <div className="p-4 border rounded-lg bg-card">
      <div className="flex justify-between items-center mb-2">
        <span className="text-sm font-bold">Metrics (Zoom: {zoom}x)</span>
        <button
          onClick={() => setZoom((z) => z + 1)}
          className="px-2 py-1 text-xs bg-primary text-primary-foreground rounded"
        >
          Zoom In
        </button>
      </div>
      <canvas data-zoom={zoom} />
    </div>
  );
}

// 2. Server Component: page.tsx (Dynamic page with an isolated cached slot)
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou";
import { db } from "@/lib/db";
import { UserGreeting } from "./UserGreeting"; // Reads user session/cookies
import { GreetingSkeleton, AnalyticsSkeleton } from "./skeletons";
import { InteractiveChart } from "./interactive-chart"; // Client Component

// Server Component performing the heavy query
async function HeavyAnalyticsWidget() {
  const analyticsData = await db.analytics.aggregateBigData(); // 2-second query
  return <InteractiveChart data={analyticsData} />;
}

export default function DashboardPage() {
  return (
    <main className="space-y-6 p-8">
      {/* Dynamic personal component: reads cookies/session without blocking initial layout delivery (preserves fast TTFB) */}
      <Suspense fallback={<GreetingSkeleton />}>
        <UserGreeting />
      </Suspense>

      {/* Micro-ISR hole: wrapped in Suspense so cache misses stream without blocking TTFB */}
      <Suspense fallback={<AnalyticsSkeleton />}>
        <DinouCacheSlot id="analytics-slot" duration={300} tags={["analytics"]}>
          <HeavyAnalyticsWidget />
        </DinouCacheSlot>
      </Suspense>
    </main>
  );
}
  • Why wrap in <Suspense>? While <DinouCacheSlot> works without it on cache hits, on a cache miss (cold start or expired TTL) an unwrapped slot would block the entire HTTP response during the heavy query. Wrapping it in <Suspense> guarantees instant TTFB by streaming the layout and skeleton immediately.
  • What about UserGreeting? In dynamic SSR, wrapping UserGreeting in <Suspense> ensures that reading user session or auth cookies does not delay the initial response, keeping TTFB fast. In PPR, it also prevents dynamic request data from de-optimizing the build-time static shell.
  • In-Place Client Refresh: Any Client Component can trigger an on-demand re-render of only this slot using refreshSlot("analytics-slot") from "dinou" without reloading the page or resetting state in other components.
  • On-Demand Invalidation: Server Functions and webhooks can invalidate the slot cache immediately via revalidateTag("analytics").

6.5. The Synergy: Partial Prerendering (PPR) + <DinouCacheSlot>

What happens when you nest <DinouCacheSlot> inside a React <Suspense> boundary on a page with Partial Prerendering (PPR)? You unlock the highest performance profile possible in full-stack web engineering: zero-delay initial delivery paired with near-zero streaming delay and 99.9% database query offloading.

MetricPPR Alone (Without Slot)PPR + <DinouCacheSlot>
ArchitectureStatic shell + on-demand dynamic executionInstant shell + cached streaming hole
TTFB (First Byte)0ms from CDN edge (instant outer layout)0ms from CDN edge (instant outer layout)
Skeleton DurationThe user waits the full query duration (e.g. 1,200ms) on every single request while the server executes the query.~5ms! Subsequent visitors receive the cached RSC stream chunk almost immediately; the skeleton barely blinks.
Database Load10,000 visitors per minute = 10,000 database queries executed on the server.10,000 visitors per minute with duration={60} = 1 database query per minute (99.99% reduction).
Implementation Example

Wrap the slow asynchronous Server Component with <DinouCacheSlot>, and then enclose both inside React <Suspense>:

// src/dashboard/page.tsx - High-traffic operational dashboard
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou";
import { db } from "@/lib/db";
import { MetricsSkeleton } from "./MetricsSkeleton";
import { InteractiveMetricsChart } from "./InteractiveMetricsChart"; // "use client"

// Heavy async Server Component
async function HeavyMetricsSubtree() {
  // Heavy aggregation across millions of database rows (e.g. 1.5 seconds)
  const aggregatedStats = await db.telemetry.aggregateOperations();
  return <InteractiveMetricsChart data={aggregatedStats} />;
}

export default function DashboardPage() {
  return (
    <main className="p-8 space-y-6 max-w-5xl mx-auto">
      {/* 1. PPR Static Shell: Delivered instantly with 0ms TTFB from edge CDN */}
      <header className="border-b pb-4">
        <h1 className="text-3xl font-extrabold tracking-tight">Operations Command Center</h1>
        <p className="text-sm text-muted-foreground">Real-time system telemetry and infrastructure health.</p>
      </header>

      {/* 2. PPR Dynamic Boundary + Cache Slot:
             - Fallback skeleton sent immediately in initial layout
             - Dynamic hole streams from server cache in ~5ms for subsequent users
             - Database query executes only ONCE every 60 seconds */}
      <Suspense fallback={<MetricsSkeleton />}>
        <DinouCacheSlot id="operations-telemetry" duration={60} tags={["telemetry"]}>
          <HeavyMetricsSubtree />
        </DinouCacheSlot>
      </Suspense>
    </main>
  );
}
  • Instant TTFB: Outer shell and fallback skeleton arrive at 0ms from CDN edge.
  • 99.9% Query Relief: Caches the dynamic stream chunk on the server during the TTL window.
  • Surgical Invalidation: Call revalidateTag("telemetry") or refreshSlot() to update without full-page reloads.

6.6. When Should You Use <DinouCacheSlot>?

A common question in full-stack architecture is: Does <DinouCacheSlot> only make sense for dynamic components or subtrees?

The short answer: While dynamic routes and PPR are where it delivers exponential returns, it also unlocks unique capabilities for static pages (such as asymmetric revalidation and in-place client slot refreshes). Understanding when to use it boils down to the distinction between Horizontal Segmentation (route/layout-level) and Vertical Segmentation (component-level).

When NOT to Use <DinouCacheSlot> (Redundant or Anti-Patterns)
  1. 100% Static Pages (SSG): If an entire route is pre-rendered at build time with no revalidation, it is already cached as immutable HTML at the CDN Edge. Wrapping parts of it in a slot adds zero caching benefit.
  2. Wrapping the Entire Page: Wrapping your entire page.tsx body inside a slot reinvents horizontal caching. Use export const revalidate = 300 in page_functions.ts instead.
  3. Unpartitioned Private Data: Wrapping personal user data (e.g. private user carts or session tokens) with a static slot ID will cache that private data for the first user and leak it to subsequent visitors.
  1. Heavy, Computationally Expensive Widgets in Dynamic / Authenticated Pages: Dynamic pages (reading cookies, auth tokens, or search params) cannot be cached at the CDN or route level because each user receives a customized response. However, many widgets on that page (stock tickers, trending feeds, heavy SQL analytics) are identical across users. <DinouCacheSlot> isolates the expensive widget with its own TTL:
// src/routes/dashboard/page.tsx (Dynamic authenticated SSR)
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou";
import { getSession } from "@/lib/auth";
import { GlobalMetricsWidget } from "./metrics";

export default async function DashboardPage() {
  const session = await getSession(); // Evaluated dynamically per user

  return (
    <div className="p-6">
      <h1>Welcome back, {session.user.name}</h1> {/* 100% Live & personal */}

      {/* Global widget: cached for 10 minutes on the server across all users */}
      <Suspense fallback={<div>Loading global telemetry...</div>}>
        <DinouCacheSlot id="global-metrics" duration={600} tags={["metrics"]}>
          <GlobalMetricsWidget />
        </DinouCacheSlot>
      </Suspense>
    </div>
  );
}
  1. Dynamic Holes in Partial Prerendering (PPR) — The Ultimate Synergy: In PPR, the static shell ships instantly from CDN (0ms TTFB), while dynamic holes stream in via React Suspense. Without a slot, 100,000 visitors trigger 100,000 database hits. Placing <DinouCacheSlot> inside the hole means only the first request queries the DB; the other 99,999 stream the cached RSC Flight payload in ~2-5ms.

  2. Static or ISR Pages with Asymmetric Lifecycles & Targeted Refreshes: This is where <DinouCacheSlot> proves it is not limited to dynamic pages. You can use it in static/ISR pages for two critical architectural advantages:

    • A. Asymmetric Revalidation Lifecycles: Suppose a product page has heavy media and specifications that rarely change (cached for 24 hours: revalidate = 86400). But live stock pricing fluctuates every 60 seconds. A slot lets the pricing widget expire every 60s without re-rendering the whole page!
    • B. Surgical In-Place Client Refresh (refreshSlot): A Client Component can trigger an on-demand re-render of only this Server Component using refreshSlot(slotId) without refreshing the browser page or losing client state (scroll position, inputs, audio):
// 1. In your Page (Server Component): Asymmetric slot inside static route
<DinouCacheSlot id={`price-${product.id}`} duration={60} tags={["pricing"]}>
  <LivePriceCalculator productId={product.id} />
</DinouCacheSlot>

// 2. In an interactive Client Component: refresh ONLY that slot
"use client";
import { refreshSlot } from "dinou";

export function RefreshPriceButton({ productId }: { productId: string }) {
  return (
    <button onClick={() => refreshSlot(`price-${productId}`, { fresh: true })}>
      Check Latest Stock & Price
    </button>
  );
}
Quick Decision Checklist
Question / RequirementRecommended ActionReason
Dynamic or PPR route with heavy queries?Wrap with <DinouCacheSlot> inside <Suspense>Protects database from repetitive execution and streams cached RSC Flight chunks in ~2-5ms.
Asymmetric expiration within a page?Use <DinouCacheSlot>Decouples widget TTL (e.g. 60s) from overall page TTL (e.g. 24h) without re-rendering the entire page.
Need surgical, targeted client refreshes?Use <DinouCacheSlot> + refreshSlot()Updates just that server subtree in-place without page reload or state loss.
Pure SSG with uniform expiration?Do NOT use <DinouCacheSlot>Let build-time SSG and CDN Edge caching handle the entire page with zero runtime overhead.

6.7. Common Traps & Anti-Patterns to Avoid

  • Trap 1: Placing "use client" at the root layout. Marking src/layout.tsx as a Client Component cascades the client boundary to every single page in your application. Root layouts should always be Server Components to protect secrets and enable streaming.
  • Trap 2: Direct Server Component imports in Client files. Never write import ServerComponent from "./ServerComponent" inside a file containing "use client". Use the Composition Pattern (Section 6.2) and pass it as children instead.
  • Trap 3: Accessing browser APIs in component body during SSR. Calling window.innerWidth or localStorage.getItem() outside of a useEffect in a Client Component causes server pre-rendering crashes. Always wrap browser calls in useEffect or an environment check (if (typeof window !== "undefined")).
  • Trap 4: Passing non-serializable objects across boundaries. Do not pass callback functions or ORM model class instances with methods as props to Client Components. Extract plain JavaScript objects or use Server Functions ("use server") for actions.
  • Trap 5: Fetching data in Client Components via useEffect on mount. In modern RSC architectures, fetching on mount via useEffect creates network waterfalls, causes layout shifts, and misses server streaming. Pass data from Server Components or use Strategy 9 (RSC Promise) / Strategy 12 (Server Function RPC).

Page & Layout Functions (page_functions.ts & layout_functions.ts)

In Dinou, every route segment can independently configure its data fetching, static pre-rendering, cache lifecycles, and parameter validation without cluttering component UI code.

  • page_functions.ts: Placed alongside page.tsx. Configures data fetching (getProps), static paths (getStaticPaths), cache strategy (revalidate), Partial Prerendering (ppr), cache tags (getCacheTags), and parameter validation (validateParams) specifically for that page.
  • layout_functions.ts: Placed alongside layout.tsx. Supplies data directly to the layout component (getProps) and manages segment-level cache strategy (revalidate, ppr) independently from child pages.

Overview & Horizontal Segmentation

Dinou strictly separates server-side data orchestration and cache configuration from presentation UI:

src/blog/
├── layout.tsx                # Presentation shell (RSC)
├── layout_functions.ts       # Layout cache strategy (revalidate = 300) & getProps
└── [slug]/
    ├── page.tsx              # Page presentation (RSC or "use client")
    └── page_functions.ts     # Page data fetching (getProps), paths, and validation

The Unified Architectural Contract: RouteSegmentConfig<TParams, TProps>

In Dinou, every server-side configuration file implements the exact same foundational contract: RouteSegmentConfig. This is the framework's underlying "abstract" interface defining all segment capabilities. For day-to-day application code, Dinou exports two concrete, role-based aliases: PageFunctions (for pages) and LayoutFunctions (for layouts):

// Exported directly from "dinou":

/**
 * Foundational "abstract" interface governing all route segments in Dinou.
 */
export interface RouteSegmentConfig<TParams = any, TProps = any> {
  // Partial Prerendering (PPR) configuration
  ppr?: boolean | (() => boolean | Promise<boolean>);

  // Cache & revalidation strategy (< 1 = Dynamic SSR, >= 1 = ISR, false = Static)
  revalidate?: number | false | (() => number | false | Promise<number | false>);

  // Cache tags for on-demand invalidation via revalidateTag(tag)
  getCacheTags?: string[] | ((params?: TParams) => string[] | Promise<string[]>);

  // Path generator for SSG (constant array or sync/async function)
  getStaticPaths?: StaticPathItem<TParams>[] | (() => StaticPathsResult<TParams>);

  // Server loader providing direct props to the component
  getProps?: (params?: TParams) => TProps | Promise<TProps>;

  // Parameter validator guard (returns false to halt and return 404)
  validateParams?: (params?: TParams) => boolean | Promise<boolean>;

  // Controls whether Incremental Static Generation (ISG) is allowed for ungenerated paths
  allowISG?: boolean | (() => boolean | Promise<boolean>);
}

/**
 * Concrete semantic alias to import and use in 'page_functions.ts'
 */
export type PageFunctions<TParams = any, TProps = any> = RouteSegmentConfig<TParams, TProps>;

/**
 * Concrete semantic alias to import and use in 'layout_functions.ts'
 */
export type LayoutFunctions<TParams = any, TProps = any> = RouteSegmentConfig<TParams, TProps>;
Where to useType to importPurpose
src/.../page_functions.tsPageFunctionsTypes data loading (getProps), static paths, and cache for the page.
src/.../layout_functions.tsLayoutFunctionsTypes data loading (getProps) and independent cache for the layout shell.
Plugins & Shared UtilsRouteSegmentConfigBase "abstract" interface for generic utilities that process arbitrary route segments.
3 Architectural Reasons for Separation
  1. Semantic Intent: Seeing import type { PageFunctions } in a page file immediately clarifies intent and self-documents that the functions belong to the page tier.
  2. Mental Separation: Reinforces Dinou's horizontal segment isolation: layouts have independent cache lifetimes and props from child pages.
  3. Future Extensibility: If future Dinou versions introduce layout-only properties (e.g. scroll boundaries) or page-only hooks, your application code remains completely decoupled and non-breaking.

1. getProps (Direct Component Props)

Use getProps to load data on the server based on route params and pass it directly to your component.

[!NOTE] v7 Direct Return Contract: In Dinou v7, getProps directly returns the props object ({ ... }). The legacy wrapper { page: { ... }, layout: { ... } } has been deprecated because pages and layouts now have their own dedicated configuration files.

  • Arguments: params?: TParams (The normalized dynamic route parameters)
  • Returns: TProps | Promise<TProps> (Injected directly as the component's props)

Example: Type-Safe Page Component Loader

// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db, type Post } from "@/lib/db";

export interface BlogPostData {
  post: Post;
}

// Best Practice: Type getProps using PageFunctions for strict parameters & return contract
export const getProps: PageFunctions<RouteParams<"/blog/[slug]">, BlogPostData>["getProps"] = async (params) => {
  const post = await db.getPost(params?.slug);
  // Directly returns BlogPostData for page.tsx
  return { post };
};
// src/blog/[slug]/page.tsx
import type { PageProps } from "dinou";
import type { BlogPostData } from "./page_functions";

// Best Practice: Extend PageProps with the loader's returned BlogPostData
interface BlogPostProps extends PageProps<"/blog/[slug]">, BlogPostData {}

export default function BlogPost({ params, post }: BlogPostProps) {
  return (
    <article>
      <span className="text-xs text-muted-foreground">Route: /blog/{params.slug}</span>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}

Example: Type-Safe Layout Component Loader

// src/blog/layout_functions.ts
import type { LayoutFunctions } from "dinou";
import { db } from "@/lib/db";

export interface BlogLayoutData {
  categories: string[];
}

// Best Practice: Type layout server functions with LayoutFunctions
export const getProps: LayoutFunctions<any, BlogLayoutData>["getProps"] = async () => {
  const categories = await db.getCategories();
  // Directly returns props for layout.tsx
  return { categories };
};
// src/blog/layout.tsx
import type { LayoutProps } from "dinou";
import type { BlogLayoutData } from "./layout_functions";

// Best Practice: Extend LayoutProps for automatic typing of children and params
interface BlogLayoutProps extends LayoutProps, BlogLayoutData {}

export default function BlogLayout({
  children,
  categories,
}: BlogLayoutProps) {
  return (
    <div className="blog-shell">
      <aside className="blog-sidebar">
        <h3>Categories</h3>
        <ul>
          {categories.map((c) => (
            <li key={c}>{c}</li>
          ))}
        </ul>
      </aside>
      <main>{children}</main>
    </div>
  );
}

Deep-Dive Architecture: Why getProps Only Receives params & The Dynamic Bailout Mechanism

Resource Identity & Cache Integrity

Route params (e.g. /blog/[slug]) identify the resource, not the visitor. Because the content for a given route is identical for everyone, Dinou can run getProps in advance—at build time (SSG) or on first request (ISR)—and safely pre-render and cache the HTML (in static storage, server memory, reverse proxies, or CDNs).

In contrast, cookies, headers, and query params belong to a specific HTTP connection. If getProps relied on them, the output could contain private user data, making shared caching impossible and forcing the server to render every request on demand.

The Dynamic Bailout Mechanism

If you call getContext() inside getProps to read request-time connection data:

Property AccessedBehaviorWhy?
context.request.headers❌ Build Error (SSG) / Dynamic Bailout (SSR)Headers vary per client request. Accessing them invalidates shared static caching.
context.request.cookies❌ Build Error (SSG) / Dynamic Bailout (SSR)Cookies are user-specific (sessions, auth). Cannot be baked into shared HTML.
context.searchParams❌ Build Error (SSG) / Dynamic Bailout (SSR)Query strings vary infinitely (?ref=..., ?utm_source=...). Cannot be pre-rendered at build time.
context.params✅ Allowed Everywhere (SSG, ISR, SSR, PPR)Identifies the route resource ([slug]). Pure and deterministically cacheable.

To keep pages pre-renderable (SSG/ISR) and instant to load, organize your data into its natural layer: route data in getProps, session cookies inside <Suspense>, and query parameters via the universal useSearchParams() hook:

// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db, type Post } from "@/lib/db";

export interface BlogPostData {
  post: Post;
}

// 1. Pure route loader: depends solely on params
// Cached statically for all visitors (SSG / ISR)
export const getProps: PageFunctions<RouteParams<"/blog/[slug]">, BlogPostData>["getProps"] = async (params) => {
  const post = await db.getPost(params?.slug);
  return { post };
};
// src/blog/[slug]/page.tsx
import { Suspense } from "react";
import type { PageProps } from "dinou";
import type { BlogPostData } from "./page_functions";
import { UserSession } from "./user-session";
import { RelatedPosts } from "./related-posts";
import { SearchFilter } from "./search-filter";

interface BlogPostProps extends PageProps<"/blog/[slug]">, BlogPostData {}

export default function BlogPostPage({ params, post }: BlogPostProps) {
  return (
    <article className="post-container">
      {/* 1. Instant static shell: delivered immediately from cache */}
      <header>
        <span className="text-xs text-muted-foreground">Route: /blog/{params.slug}</span>
        <h1>{post.title}</h1>
        {/* Client Component using useSearchParams() for instant input sync */}
        <SearchFilter />
      </header>

      <div className="post-body">{post.content}</div>

      {/* 2. Dynamic cookie component streamed via Suspense */}
      <Suspense fallback={<div className="text-xs text-muted-foreground">Loading session...</div>}>
        <UserSession />
      </Suspense>

      {/* 3. Server Component using useSearchParams() streamed via Suspense */}
      <Suspense fallback={<div className="text-xs text-muted-foreground">Filtering articles...</div>}>
        <RelatedPosts />
      </Suspense>
    </article>
  );
}
// src/blog/[slug]/user-session.tsx (Server Component)
import { getContext } from "dinou";
import { auth } from "@/lib/auth";

// Cookie & auth access isolated inside Suspense:
// Streaming this component prevents getProps from bailing out to Dynamic SSR!
export async function UserSession() {
  const { req } = getContext();
  const sessionToken = req?.cookies?.session;
  const user = sessionToken ? await auth.verify(sessionToken) : null;

  if (!user) {
    return <a href="/login" className="text-xs font-semibold">Sign In</a>;
  }

  return (
    <aside className="user-badge">
      <span>Welcome back, <strong>{user.name}</strong>!</span>
    </aside>
  );
}
// src/blog/[slug]/related-posts.tsx (Server Component)
import { useSearchParams } from "dinou";
import { db } from "@/lib/db";

// Universal hook in a Server Component:
// Reads ctx.req.query to fetch filtered data from the DB on the server.
// Because it reads query, wrap it in <Suspense> in page.tsx so the post shell stays static!
export async function RelatedPosts() {
  const searchParams = useSearchParams();
  const tag = searchParams.get("tag") ?? "featured";
  const articles = await db.getPostsByTag(tag);

  return (
    <section className="related-posts">
      <h3>Articles matching "?tag={tag}"</h3>
      <ul>
        {articles.map((item) => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </section>
  );
}
// src/blog/[slug]/search-filter.tsx (Client Component)
"use client";

import { useSearchParams } from "dinou";

// Universal hook in a Client Component:
// Reads URL params on the browser for reactive input state without contacting the server
export function SearchFilter() {
  const searchParams = useSearchParams();
  const currentTag = searchParams.get("tag") ?? "";

  return (
    <input
      type="search"
      defaultValue={currentTag}
      placeholder="Filter by tag (?tag=...)..."
      className="text-xs border rounded px-2 py-1"
    />
  );
}
Data LayerHandled InLifecycle & Strategy
1. Route Data (Params)getProps in page_functions.tsPure and cacheable for all visitors (SSG / ISR).
2. Cookies & HeadersServer Components wrapped in <Suspense>Streams dynamically without bailing out the static page shell.
3. Universal useSearchParams()Server or Client Components with <Suspense>Works in Server Components (streamed) and Client Components (instant UI).

2. getStaticPaths (Static Generation)

Defines which dynamic parameter combinations should be pre-rendered at build time (SSG). By default, any dynamic path not returned is generated on-demand when first requested and cached statically for all subsequent visitors (Incremental Static Generation / ISG), unless disabled via allowISG: false (which strictly restricts routes to the pre-rendered list and returns 404 for any other path).

2.1. Constant Array vs Function Export

In Dinou v7, getStaticPaths supports both a constant array export (for known, fixed paths) and a synchronous or asynchronous function export (for dynamic data sources like databases or headless CMSs):

// 1. Constant Array Export (Zero runtime overhead, ideal for fixed routes or locales)
// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";

export const getStaticPaths: PageFunctions<RouteParams<"/blog/[slug]">]["getStaticPaths"] = [
  "welcome",
  "getting-started",
  "release-notes",
];
// 2. Async Function Export (Queries databases or headless CMSs at build time)
// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db } from "@/lib/db";

export const getStaticPaths: PageFunctions<RouteParams<"/blog/[slug]">]["getStaticPaths"] = async () => {
  const posts = await db.post.findMany({ select: { slug: true } });
  return posts.map((p) => p.slug);
};

2.2. Return Formats

Dinou accommodates simple, catch-all, and multi-segment architectures with flexible item formats:

Route TypeAccepted FormatExample ReturnPre-rendered URLs
Simple ([id] or [slug])string[]number[]["foo", "bar"] or [1, 2, 3]
Optional Simple ([[id]])`(stringnumberundefined)[]`
Catch-all ([...slug])string[][][["a", "b"], ["c"]]/docs/a/b, /docs/c
Optional Catch-all ([[...slug]])`(string[]undefined)[]`[undefined, ["a"], ["a", "b"]]
Multi-Segment / Nested ([cat]/[id])`Record<string, stringstring[]>[]`[{ cat: "shoes", id: "100" }]

2.3. Simple Dynamic Routes

Simple dynamic routes match a single URL segment. Dinou supports both required segments ([id]) and optional segments ([[category]]):

// src/blog/[id]/page_functions.ts (Required segment [id])
import type { PageFunctions, RouteParams } from "dinou";

export const getStaticPaths: PageFunctions<RouteParams<"/blog/[id]">]["getStaticPaths"] = [
  "1",
  "2",
  "3",
]; // Generates: /blog/1, /blog/2, /blog/3
// src/shop/[[category]]/page_functions.ts (Optional segment [[category]])
import type { PageFunctions, RouteParams } from "dinou";

// Pass undefined to pre-render the root route (/shop) without a parameter
export const getStaticPaths: PageFunctions<RouteParams<"/shop/[[category]]">]["getStaticPaths"] = [
  undefined, // Generates: /shop
  "shoes",   // Generates: /shop/shoes
  "shirts",  // Generates: /shop/shirts
];

2.4. Catch-all Routes

Catch-all routes match multiple dynamic path segments. Dinou supports both standard catch-alls ([...slug]) and optional catch-alls ([[...slug]]):

// src/docs/[...slug]/page_functions.ts (Required catch-all [...slug])
import type { PageFunctions, RouteParams } from "dinou";

export const getStaticPaths: PageFunctions<RouteParams<"/docs/[...slug]">]["getStaticPaths"] = [
  ["intro"],              // Generates: /docs/intro
  ["api", "v1", "auth"],  // Generates: /docs/api/v1/auth
];
// src/docs/[[...slug]]/page_functions.ts (Optional catch-all [[...slug]])
import type { PageFunctions, RouteParams } from "dinou";

export const getStaticPaths: PageFunctions<RouteParams<"/docs/[[...slug]]">]["getStaticPaths"] = [
  undefined,              // Generates: /docs (root)
  ["intro"],              // Generates: /docs/intro
  ["api", "v1", "auth"],  // Generates: /docs/api/v1/auth
];

2.5. Automatic Route Propagation (Recursion)

Static parameters automatically propagate downwards through the directory tree. If you define values for a segment, Dinou will recursively generate all nested static sub-pages under those values.

src/blog/[slug]/
├── page.tsx          # Page: /blog/[slug]
├── page_functions.ts # Returns ["post-a", "post-b"]
└── details/
    └── page.tsx      # Nested static page

If getStaticPaths in blog/[slug] returns ["post-a", "post-b"], Dinou generates 4 static pages:

  1. /blog/post-a
  2. /blog/post-a/details
  3. /blog/post-b
  4. /blog/post-b/details

2.6. Nested Pages & The "Chain of Responsibility"

When nesting routes where both the parent and the child segment have their own page.tsx, static generation follows a strict top-down "chain of responsibility":

src/shop/[category]/
├── page.tsx              # Parent page: /shop/[category]
├── page_functions.ts     # Parent gatekeeper
└── [id]/
    ├── page.tsx          # Child page: /shop/[category]/[id]
    └── page_functions.ts # Child generator

Because the parent folder contains a page.tsx, Dinou must generate valid parent pages first before it can traverse down to the child level. The parent acts as a mandatory gatekeeper:

  1. If parent returns []: Dinou halts at the parent level and never invokes the child [id]/page_functions.ts. No product pages are generated.
  2. When parent resolves its categories: It unlocks the child level:
// 1. Parent: src/shop/[category]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";

export const getStaticPaths: PageFunctions<RouteParams<"/shop/[category]">]["getStaticPaths"] = [
  "shoes",
  "shirts",
];

// 2. Child: src/shop/[category]/[id]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";

export const getStaticPaths: PageFunctions<RouteParams<"/shop/[category]/[id]">]["getStaticPaths"] = [
  "100",
  "200",
];

// Automatically generates 4 static pages:
// /shop/shoes/100, /shop/shoes/200
// /shop/shirts/100, /shop/shirts/200

[!TIP] Rule of Thumb:

  • If an intermediate folder has its own page.tsx, define a page_functions.ts at that folder level with scalar arrays (string[]).
  • If an intermediate folder does not have a page.tsx, you can define all nested parameters together in the leaf page_functions.ts using an Object Array.

2.7. Nested & Pass-through Segments

When you have multiple dynamic segments in a path without intermediate pages (the intermediate folders do not have a page.tsx), the leaf segment returns an Object mapping all parameter names at once:

src/shop/[category]/[brand]/[...specs]/
└── page.tsx # Only leaf has a page.tsx
// src/shop/[category]/[brand]/[...specs]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";

export const getStaticPaths: PageFunctions<RouteParams<"/shop/[category]/[brand]/[...specs]">]["getStaticPaths"] = [
  {
    category: "electronics",
    brand: "apple",
    specs: ["m3", "16gb"], // → Pre-renders: /shop/electronics/apple/m3/16gb
  },
  {
    category: "clothing",
    brand: "nike",
    specs: ["running", "shoes", "size-10"], // → Pre-renders: /shop/clothing/nike/running/shoes/size-10
  },
];

2.8. Async Support (CMS & DB)

getStaticPaths can be an async function, allowing you to query external headless CMSs, databases, or REST APIs during the build:

// src/products/[id]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db } from "@/lib/db";

export const getStaticPaths: PageFunctions<RouteParams<"/products/[id]">]["getStaticPaths"] = async () => {
  // Queries your database at build time (npm run build)
  const products = await db.products.findMany({ select: { id: true } });
  return products.map((product) => String(product.id));
};

3. validateParams (Route Parameter Validation)

Validates dynamic parameters (such as [id] or [...slug]) on the server prior to rendering. If validateParams returns false (or a falsy value), Dinou immediately terminates request processing and issues a 404 Not Found response.

  • Arguments: params?: TParams (The normalized route parameters object)
  • Returns: boolean | Promise<boolean> (Return false to abort and render 404)
// src/products/[id]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";

type ProductParams = RouteParams<"/products/[id]">;

// 1. Synchronous validation: guarantees id is purely numeric
export const validateParams: PageFunctions<ProductParams>["validateParams"] = (params) => {
  return /^[0-9]+$/.test(params?.id ?? "");
};
// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db } from "@/lib/db";

type BlogParams = RouteParams<"/blog/[slug]">;

// 2. Asynchronous validation: verifies record existence before rendering
export const validateParams: PageFunctions<BlogParams>["validateParams"] = async (params) => {
  if (!params?.slug) return false;
  const exists = await db.posts.exists({ slug: params.slug });
  return exists;
};

[!NOTE] Execution Timing: validateParams executes before getProps. If validation fails, getProps will never run, preventing unnecessary database lookups.


4. allowISG (Incremental Static Opt-Out)

Controls whether dynamic paths that were not pre-rendered at build time (via getStaticPaths during dinou build) are allowed to be generated on-demand on the first incoming user request.

In Dinou v7, pre-rendering happens strictly during the build phase, ensuring production server startup is instantaneous.

  • Default (true): Any valid path not pre-generated at build time is generated on-demand when the first visitor requests it, and subsequently cached statically (Incremental Static Generation / ISG).
  • Opt-Out (false): Strictly restricts accessible routes to those explicitly returned by getStaticPaths. Any request for a path not pre-rendered immediately returns a 404 Not Found without invoking getProps or running SSR.
// src/docs/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";

// Strict SSG: only these two articles exist; all other /docs/[slug] return 404
export const getStaticPaths: PageFunctions<RouteParams<"/docs/[slug]">]["getStaticPaths"] = [
  "installation",
  "architecture",
];

// Disallows on-demand generation for unlisted slugs
export const allowISG: PageFunctions["allowISG"] = false;

5. revalidate (ISR & Dynamic SSR Escape)

The revalidate export is Dinou's single source of truth for route caching and rendering behavior. It is measured in seconds (s).

Revalidate ValueStrategyLifecycle & Behavior
revalidate < 1 (or 0)Dynamic SSREscapes static generation. The page is rendered dynamically on every incoming request with live database reads.
revalidate >= 1Incremental Static Regeneration (ISR)The page is cached statically for N seconds. Requests within the window receive instant cached HTML (0ms TTFB). After N seconds, a background regeneration is triggered.
revalidate = falsePure SSG (Default)The page is rendered once at build time and cached indefinitely until the next build or manual purge.
// src/metrics/page_functions.ts - Real-time dynamic SSR
import type { PageFunctions } from "dinou";

// Escapes SSG: executes getProps on every request
export const revalidate: PageFunctions["revalidate"] = 0;
// src/store/[id]/page_functions.ts - 60-second ISR
import type { PageFunctions, RouteParams } from "dinou";

export const revalidate: PageFunctions["revalidate"] = 60; // 1 minute ISR
// Conditional revalidation based on environment
export const revalidate: PageFunctions["revalidate"] = () => {
  return process.env.NODE_ENV === "development" ? 0 : 3600;
};

6. getCacheTags (Cache Tagging)

Assigns custom tags to the static page cache files, allowing the page to be purged and regenerated on-demand using revalidateTag(tag) (On-Demand Revalidation).

// src/products/[id]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";

type ProductParams = RouteParams<"/products/[id]">;

// 1. Constant Array
export const getCacheTags: PageFunctions["getCacheTags"] = ["catalog", "products"];

// 2. Synchronous Function (derives tags from route parameters)
export const getCacheTags: PageFunctions<ProductParams>["getCacheTags"] = (params) => {
  return ["catalog", `product-${params?.id}`];
};

// 3. Asynchronous Function (queries external tags or taxonomy)
export const getCacheTags: PageFunctions<ProductParams>["getCacheTags"] = async (params) => {
  const categories = await db.categories.forProduct(params?.id);
  return ["catalog", ...categories.map((c) => `category-${c.slug}`)];
};

On-Demand Invalidation via Server Function

When data changes, call revalidateTag() in a Server Function or webhook to instantly invalidate all cached files tagged with that identifier:

// src/actions/products.ts
"use server";

import { revalidateTag } from "dinou";
import { db } from "@/lib/db";

export async function updateProduct(id: string, data: any) {
  await db.products.update({ where: { id }, data });
  // Instantly invalidates /products/[id] and any catalog pages sharing the tag
  await revalidateTag(`product-${id}`);
}

7. ppr (Partial Prerendering)

Partial Prerendering (PPR) combines the instant TTFB (Time to First Byte) of a static page with the real-time capabilities of dynamic server-side rendering over a single HTTP response.

How PPR Works: Instant Shell + Progressive Streaming

PPR eliminates the traditional trade-off between static and dynamic rendering by dividing the page into two phases over a single HTTP connection:

  1. Instant Static Shell: Delivered immediately (0ms TTFB) from CDN edge or server cache. Includes layouts, navigation, and fallback skeletons.
  2. Dynamic Progressive Streaming: Live components wrapped in React <Suspense> execute concurrently on the server and stream their resolved HTML chunks directly into the page.
// src/dashboard/page_functions.ts
import type { PageFunctions } from "dinou";

// Enables Partial Prerendering for this route segment
export const ppr: PageFunctions["ppr"] = true;
// src/dashboard/page.tsx
import { Suspense } from "react";
import { UserGreeting, UserGreetingSkeleton } from "./user-greeting";
import { LiveMetrics, MetricsSkeleton } from "./live-metrics";

export default function DashboardPage() {
  return (
    <div className="dashboard-layout">
      {/* Static Shell: Pre-rendered at build time (0ms TTFB) */}
      <header className="dashboard-header">
        <h1>Management Console</h1>
      </header>

      {/* Dynamic Holes: Streamed progressively over the open HTTP connection */}
      <Suspense fallback={<UserGreetingSkeleton />}>
        <UserGreeting />
      </Suspense>

      <Suspense fallback={<MetricsSkeleton />}>
        <LiveMetrics />
      </Suspense>
    </div>
  );
}

8. End-to-End Type-Safe Example

To achieve complete type safety, autocompletion, and compile-time validation in your routes, import PageFunctions and LayoutFunctions from "dinou" and pass your parameter and props types:

1. Defining src/blog/[slug]/page_functions.ts

// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db, type Post } from "@/lib/db";

// 1. Strict route parameters derived from URL pattern
export type BlogParams = RouteParams<"/blog/[slug]">;

// 2. Define data contract passed to the component
export interface BlogPageProps {
  post: Post;
  views: number;
}

// 3. Page configuration with full type inference
export const ppr: PageFunctions["ppr"] = true;
export const revalidate: PageFunctions["revalidate"] = 60; // 1 min ISR

export const getStaticPaths: PageFunctions<BlogParams>["getStaticPaths"] = [
  "welcome",
  "announcement",
];

export const getProps: PageFunctions<BlogParams, BlogPageProps>["getProps"] = async (params) => {
  const post = await db.posts.findUnique({ where: { slug: params?.slug } });
  if (!post) throw new Error("Post not found");
  return { post, views: 100 };
};

export const validateParams: PageFunctions<BlogParams>["validateParams"] = (params) => {
  return Boolean(params?.slug && params.slug.length > 2);
};

export const getCacheTags: PageFunctions<BlogParams>["getCacheTags"] = (params) => {
  return ["blog", `post-${params?.slug}`];
};

2. Consuming Props in src/blog/[slug]/page.tsx

// src/blog/[slug]/page.tsx
import { Suspense } from "react";
import type { PageProps } from "dinou";
import type { BlogPageProps } from "./page_functions";
import { RealtimeViews, ViewsSkeleton } from "./components";

// Extend PageProps to receive both route params and loader data with full type safety
interface BlogPostPageProps extends PageProps<"/blog/[slug]">, BlogPageProps {}

export default function BlogPostPage({ params, post }: BlogPostPageProps) {
  return (
    <article className="max-w-2xl mx-auto py-8">
      {/* 1. Static shell (pre-rendered at build time, delivered with 0ms TTFB) */}
      <span className="text-xs text-muted-foreground font-mono">/blog/{params.slug}</span>
      <h1 className="text-3xl font-bold">{post.title}</h1>
      <div className="text-sm text-muted-foreground my-2">
        By {post.author} • published {post.publishedAt}
      </div>
      <div className="prose mt-6">{post.content}</div>

      {/* 2. PPR Dynamic Stream: Real-time analytics streamed in via Suspense */}
      <Suspense fallback={<ViewsSkeleton />}>
        <RealtimeViews slug={params.slug} />
      </Suspense>
    </article>
  );
}

3. Defining src/blog/layout_functions.ts

// src/blog/layout_functions.ts
import type { LayoutFunctions, RouteParams } from "dinou";
import { db } from "@/lib/db";

// 1. Layout parameters for /blog (static route segment without dynamic params = {})
type BlogLayoutParams = RouteParams<"/blog">;

// 2. Define layout data contract
export interface BlogCategory {
  id: string;
  name: string;
  count: number;
}

export interface BlogLayoutData {
  categories: BlogCategory[];
  featuredSlug: string;
}

// 3. Dedicated layout loader typed via LayoutFunctions
export const getProps: LayoutFunctions<BlogLayoutParams, BlogLayoutData>["getProps"] = async () => {
  const categories = await db.categories.findMany();
  const featured = await db.posts.findFirst({ where: { featured: true } });

  // Injected directly as props into layout.tsx
  return {
    categories,
    featuredSlug: featured?.slug ?? "welcome",
  };
};

// 4. Segment-level caching strategy for the layout shell
export const revalidate: LayoutFunctions["revalidate"] = 300; // Layout revalidates every 5 mins
export const ppr: LayoutFunctions["ppr"] = true;

4. Consuming Props in src/blog/layout.tsx

// src/blog/layout.tsx
import type { LayoutProps } from "dinou";
import type { BlogLayoutData } from "./layout_functions";

// Extend LayoutProps to receive children, segment params, and layout loader data
interface BlogLayoutComponentProps extends LayoutProps<"/blog">, BlogLayoutData {}

export default function BlogLayout({
  children,
  categories,
  featuredSlug,
}: BlogLayoutComponentProps) {
  return (
    <div className="blog-container">
      <aside className="blog-sidebar">
        <h3>Categories</h3>
        <ul>
          {categories.map((c) => (
            <li key={c.id}>
              {c.name} ({c.count})
            </li>
          ))}
        </ul>
      </aside>
      <main className="blog-content">
        {children}
      </main>
    </div>
  );
}

Alternative Object Destructuring Pattern

If you prefer defining all configurations in a single typed object before exporting, use object destructuring:

// 1. Page Config Object in src/blog/[slug]/page_functions.ts
const pageConfig: PageFunctions<BlogParams, BlogPageProps> = {
  ppr: true,
  revalidate: 60,
  getStaticPaths: ["welcome", "announcement"],
  getProps: async (params) => {
    const post = await fetchPost(params?.slug);
    return { post, views: 100 };
  },
  validateParams: (params) => Boolean(params?.slug),
  getCacheTags: (params) => ["blog", `post-${params?.slug}`],
};

export const { ppr, revalidate, getStaticPaths, getProps, validateParams, getCacheTags } = pageConfig;

// 2. Layout Config Object in src/blog/layout_functions.ts
const layoutConfig: LayoutFunctions<any, BlogLayoutData> = {
  revalidate: 300,
  ppr: true,
  getProps: async () => ({
    categories: await fetchCategories(),
    featuredSlug: "welcome",
  }),
};

export const { revalidate, ppr, getProps } = layoutConfig;

Static & Dynamic Rendering Strategies

Dinou is Static by Default. It provides a continuum of rendering strategies—from build-time SSG and stale-while-revalidate ISR to real-time Dynamic SSR, revolutionary Partial Prerendering (PPR), and component-level Micro-ISR with <DinouCacheSlot>.

Overview: Static by Default Architecture

In Dinou, every route is assumed to be static until proven otherwise. During dinou build, the compiler analyzes route segments, components, and loader functions to evaluate whether a route can be pre-rendered or requires real-time request data:

  1. Static by Default: Routes without dynamic dependencies are pre-rendered into immutable HTML and RSC payloads at build time, achieving 0ms TTFB at the CDN edge.
  2. Dynamic Bailout: When a page reads cookies, request headers, or sets revalidate: 0, Dinou automatically bails out to Live SSR on every incoming request.
  3. Hybrid Continuum: Escape the binary choice: stream dynamic components into a static shell with PPR, or isolate component caching with <DinouCacheSlot>.

1. Static Site Generation (SSG) & Bailout

Pure static rendering pre-generates HTML and RSC files during the build phase (npm run build). Production server startup is instantaneous, and static pages achieve the fastest possible Time to First Byte (TTFB) cached by Edge CDNs globally.

1.1. Pure Static Pre-rendering

Any page that does not consume request-specific headers, cookies, or query parameters and does not set revalidate < 1 is automatically generated as static HTML:

// src/about/page.tsx
// Automatically rendered as static HTML during build time ('npm run build')
export default function AboutPage() {
  return (
    <article className="prose">
      <h1>About Us</h1>
      <p>This page is pre-rendered once and served statically forever.</p>
    </article>
  );
}

1.2. Automatic Static Bailout

If a Server Component reads request-specific data at render time without wrapping it inside a dynamic streaming boundary (React <Suspense> with Partial Prerendering / PPR enabled), Dinou automatically "bails out" of static generation and transitions the entire page into Dynamic SSR for that request:

// src/profile/page.tsx
import { getContext } from "dinou";
import db from "@/lib/db";

export default async function ProfilePage() {
  const context = getContext();

  // ⚠️ Reading cookies outside <Suspense> triggers an Automatic Static Bailout
  // This page will now be rendered dynamically on every incoming request
  const sessionToken = context?.req.cookies["session_token"];
  const user = await db.users.findByToken(sessionToken);

  return <h1>Welcome back, {user?.name}</h1>;
}
Bailout Triggers in Server Components

Automatic bailout occurs when reading context.req.cookies, context.req.headers, or context.req.query (URL search parameters) during server component execution without wrapping the access in a React <Suspense> boundary.

What about Client Components and useSearchParams()?

Client Components ("use client") that call useSearchParams() do not trigger dynamic bailout. The page remains 100% static (SSG). During build time, the component is pre-rendered with an empty parameter set, and live URL parameters are synchronized in the browser upon hydration.

[!TIP] Preventing Hydration Mismatches with Search Parameters:
If a Client Component immediately renders searchParams.get("q") in its initial JSX markup, the server-generated static HTML (where the query was empty at build time) will differ from the live browser DOM (e.g. ?q=react), triggering a React hydration mismatch error.

Recommended Fix: Keep initial render deterministic, then read and apply live search parameters inside useEffect() after mounting:

// src/components/search-filter.tsx
"use client";

import { useSearchParams } from "dinou";
import { useState, useEffect } from "react";

export function SearchFilter() {
  const searchParams = useSearchParams();
  const [query, setQuery] = useState<string | null>(null);

  useEffect(() => {
    // Synchronize client-side only after mounting -> eliminates hydration mismatch
    setQuery(searchParams.get("q") ?? "");
  }, [searchParams]);

  if (query === null) {
    return <span className="text-muted-foreground">Loading filter...</span>;
  }

  return <span>Filter: <strong>{query}</strong></span>;
}

Alternative (Live SSR): If the initial HTML sent over the wire must contain search parameters for SEO, switch the page to Live SSR by exporting export const revalidate = 0; in page_functions.ts.


2. The Unified revalidate Strategy (Strict Seconds)

In Dinou v7, the revalidate export in page_functions.ts (or layout_functions.ts) serves as the single source of truth for route rendering behavior. It is measured strictly in seconds (s):

Value of revalidateStrategyDescription
false (or omitted)Pure Static (SSG)Pre-rendered once at build time. Cached indefinitely until manual invalidation.
0 (or number < 1)Dynamic SSRRendered on every incoming HTTP request on the fly.
N (number >= 1)Time-Based ISRPre-rendered, served instantly from cache, and revalidated in the background every N seconds.

2.1. Dynamic SSR (revalidate < 1 or 0)

To force a page to render dynamically on every single request (escaping the default static generation), set revalidate to 0:

// src/analytics/live/page_functions.ts

// Escapes static generation: executes dynamic SSR on every request
export const revalidate = 0;

[!NOTE] Migration from Legacy dynamic Export: In older versions of Dinou, dynamic rendering was toggled via export function dynamic(). In Dinou v7, the dynamic export has been completely removed. Setting export const revalidate = 0; is the official unified mechanism to escape static generation.

2.2. Incremental Static Regeneration (revalidate >= 1)

When revalidate >= 1, Dinou serves pre-rendered static HTML/RSC payloads while regenerating the page in the background at most once every N seconds (stale-while-revalidate):

// src/blog/page_functions.ts

// Rebuilds the cached page in the background at most once every 60 seconds
export const revalidate = 60; // Strict seconds (1 minute)
  • Background Revalidation Flow: The first request after the 60s window receives the cached page immediately. Dinou re-executes the server render in the background. Subsequent visitors see the freshly generated version.
  • Edge ISR Storage Adapters: On edge platforms like Deno Deploy and Cloudflare Workers, ISR seamlessly uses distributed key-value storage (DenoKVStorage, CloudflareKVStorage, MemoryStorage) without needing a persistent disk.

3. Partial Prerendering (PPR)

Partial Prerendering (PPR) resolves the decades-old dilemma between static and dynamic rendering. Instead of forcing an entire page to be 100% static or 100% dynamic, PPR delivers a pre-rendered static shell instantly (0ms TTFB), while streaming dynamic holes in parallel over the same HTTP response.

3.1. Hybrid Architecture: Static Shell + Dynamic Holes

At build time, Dinou compiles the layout, navigation, and static components into an immutable HTML shell. Any dynamic component wrapped in React <Suspense> is saved as a streaming placeholder hole (<div data-ppr-hole="slot-id">...fallback...</div>).

The Single HTTP Response Timeline
  1. 0ms (Instant TTFB): The browser receives the static shell with skeleton fallbacks directly from Edge CDN cache. Visual layout is painted immediately.
  2. Request Processing: Concurrently, Dinou executes the async Server Components inside <Suspense> on the server.
  3. Streaming In-Place: As each Suspense promise resolves, the HTML chunk and RSC payload are streamed down the same HTTP connection, replacing the skeleton with real content without full page reloads.
Data Freshness: Pure SSG vs. Partial Prerendering
  • Without PPR (Frozen Build Data): Data is baked into the build artifact. Live inventory or user carts are stale upon deployment. Applications historically resorted to legacy client-side useEffect() + fetch() anti-patterns, suffering waterfalls, layout shifts, and spinners.
  • With PPR (Request-Time Fresh Data): The visual shell is delivered at 0ms TTFB from the edge, while the component inside <Suspense> executes on the server on every incoming request. You get 100% fresh, live database data streamed directly to the browser without client fetch waterfalls.
PPR as a Bailout Boundary: Containing Dynamic De-optimization

Without PPR, dynamic data access is infectious: if a single nested component reads request cookies or headers, Dinou triggers an Automatic Static Bailout for the entire page, destroying static pre-rendering and edge caching for the whole route.

With PPR enabled (ppr = true), React <Suspense> acts as an isolated sandbox. Dynamic data consumption inside the boundary does not escape to parent or sibling components:

// src/store/page.tsx
import { Suspense } from "react";
import { getContext } from "dinou";
import db from "@/lib/db";
import { ProductGallery } from "@/components/gallery";
import { CartBadgeSkeleton } from "@/components/skeletons";

// ⚠️ Dynamic component: reads request-specific session cookie
async function LiveCartBadge() {
  const context = getContext();
  const sessionToken = context?.req.cookies["session_id"];
  const cart = await db.carts.findActive(sessionToken);

  return <div className="cart-badge">Items: {cart?.itemCount ?? 0}</div>;
}

export default function StorePage() {
  return (
    <main className="container">
      {/* 1. Static Shell: 0ms TTFB, pre-rendered during build */}
      <header className="navbar">
        <h1>Flagship Store</h1>
        
        {/* 2. PPR Boundary: Isolates cookie bailout exclusively to this hole */}
        <Suspense fallback={<CartBadgeSkeleton />}>
          <LiveCartBadge />
        </Suspense>
      </header>

      {/* 3. Static Sibling: Remains 100% static and edge-cached */}
      <ProductGallery />
    </main>
  );
}
  • ❌ Without PPR (Full-Page Bailout): Reading cookies["session_id"] turns the entire page into dynamic SSR. Edge caching is lost, and the visitor waits with a blank screen.
  • ✅ With PPR (Isolated Hole Bailout): The cookie bailout is quarantined inside <Suspense>. The navbar and product gallery are delivered instantly at 0ms TTFB. Only the cart badge executes on the server and streams into place.

3.2. Enabling PPR & Specificity Hierarchy

Dinou provides granular control over Partial Prerendering across three hierarchical levels:

  1. Page Level (page_functions.ts) — Highest Priority: A page can override any parent layout or global configuration:
    // src/shop/products/[slug]/page_functions.ts
    export const ppr = true; // Guarantees PPR on product details
    
  2. Layout Level (layout_functions.ts) — Subtree Cascading: Applies to that layout and cascades to all its descendant subroutes:
    // src/admin/layout_functions.ts
    export const ppr = false; // Disables PPR for /admin and ALL subroutes
    
  3. Global Configuration (dinou.config.mjs): Enables PPR globally across all eligible routes:
    // dinou.config.mjs
    import { defineConfig } from "dinou";
    export default defineConfig({ ppr: true });
    

4. Vertical Segmentation: Micro-ISR (<DinouCacheSlot>)

Traditional full-stack frameworks only offer Horizontal Segmentation (caching an entire URL route or layout shell). When a dynamic page contains an expensive database widget, developers are often forced into an architectural trap: converting the widget to a Client Component with client-side cache libraries (SWR, TanStack Query), introducing bundle bloat, waterfall requests, and layout shifts.

Dinou v7 introduces Vertical Segmentation with <DinouCacheSlot> from "dinou/server". It keeps components securely on the server as React Server Components (RSC) while caching their rendered serialized Flight payload in server storage with dedicated TTLs and invalidation tags.

4.1. Component Caching: Dynamic vs. Static Patterns

<DinouCacheSlot> functions as a universal caching primitive across two core architectural patterns:

Implementation Pattern 1: Dynamic SSR Dashboard with Cached Slot

The route runs in Live SSR mode (evaluating user cookies and personal greetings live). The heavy analytics calculation is cached on the server for 300 seconds and wrapped in React <Suspense>:

// src/dashboard/page.tsx
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou/server";
import { FastUserProfile, SlowAnalyticsWidget } from "./widgets";
import { AnalyticsSkeleton } from "./skeletons";

export default function DashboardPage() {
  return (
    <main className="grid grid-cols-1 md:grid-cols-2 gap-6 p-6">
      {/* 1. Dynamic Component: Evaluated live on every request (reads session/cookies) */}
      <FastUserProfile />

      {/* 2. Micro-ISR Slot: Cached on the server for 300 seconds */}
      {/* ⚠️ Wrapped in <Suspense> so cache misses stream without blocking TTFB */}
      <Suspense fallback={<AnalyticsSkeleton />}>
        <DinouCacheSlot id="analytics-widget" tag="analytics" revalidate={300}>
          <SlowAnalyticsWidget />
        </DinouCacheSlot>
      </Suspense>
    </main>
  );
}
Implementation Pattern 2: Static / ISR Product Page with Asymmetric Slot

The parent route has a slow horizontal revalidation of 24 hours (revalidate: 86400). Inside it, a volatile inventory counter revalidates vertically every 60 seconds (revalidate={60}) or on-demand:

// src/products/[slug]/page_functions.ts
export const revalidate = 86400; // Slow horizontal revalidation (24h)

// src/products/[slug]/page.tsx
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou/server";
import { ProductHero, ProductDescription } from "./static-widgets";
import { LiveStockCounter, StockSkeleton } from "./stock-widgets";

export default function ProductDetailPage({ params }: { params: { slug: string } }) {
  return (
    <main className="container py-8 space-y-6">
      {/* 1. Heavy Static Content: Cached at the CDN edge for 24 hours */}
      <ProductHero slug={params.slug} />
      <ProductDescription slug={params.slug} />

      {/* 2. Asymmetric Micro-ISR Slot: Revalidates vertically every 60s */}
      <Suspense fallback={<StockSkeleton />}>
        <DinouCacheSlot 
          id={`stock-${params.slug}`} 
          tag={`stock-${params.slug}`} 
          revalidate={60}
        >
          <LiveStockCounter slug={params.slug} />
        </DinouCacheSlot>
      </Suspense>
    </main>
  );
}

[!IMPORTANT] Boundary Placement: Why <Suspense> Must Wrap <DinouCacheSlot>
<DinouCacheSlot> is itself an asynchronous Server Component. When it awaits storage or renders on a cache miss, React encounters a pending promise in the parent page tree. Without an outer <Suspense>, suspension bubbles to the root, blocking the entire HTTP response and delaying fast sibling components.

The Rule of Hierarchy:

  1. <Suspense fallback={<Skeleton />}> → Controls browser streaming & instant visual layout.
  2. <DinouCacheSlot revalidate={300}> → Controls server cache storage, TTL, and cache tags.
  3. <SlowAnalyticsWidget /> → Executes database queries and business logic.

4.2. Interactive Revalidation & On-Demand Purging

Dinou provides specialized mechanisms to refresh or purge <DinouCacheSlot> instances without reloading the browser page:

Client-Side In-Place Refresh (refreshSlot)

Any interactive Client Component can trigger a surgical re-render of only that slot without full page reload or state loss:

"use client";
import { refreshSlot, useRouter } from "dinou";

export function RefreshButton() {
  const router = useRouter();

  return (
    <div className="flex gap-2">
      {/* Option 1: Standalone (respects server slot TTL) */}
      <button onClick={() => refreshSlot("analytics-widget")}>
        Refresh Metrics
      </button>

      {/* Option 2: Force fresh (bypasses server slot TTL immediately) */}
      <button onClick={() => router.refreshSlot("analytics-widget", { fresh: true })}>
        Force Fresh
      </button>
    </div>
  );
}
  • refreshSlot(id) (Default): Refreshes the client DOM with the server's current state. If the slot's server TTL has not expired, it returns cached server JSX without touching your database (protecting against button spam and traffic spikes).
  • refreshSlot(id, { fresh: true }) (Bypass): Hard revalidation. Forces the server to ignore unexpired TTL, immediately re-run queries against the database, and update both cache and client.
Server-Side Tag Invalidation (revalidateTag)

Mutations purge slots globally via revalidateTag() on the server, while the client immediately pulls the fresh result via refreshSlot():

// src/app/feed/actions.ts
"use server";
import { revalidateTag } from "dinou/server";
import db from "@/lib/db";

export async function publishPost(formData: FormData) {
  const content = formData.get("content") as string;
  await db.posts.insert({ content, createdAt: new Date() });

  // Purges cache globally for ALL future visitors across CDNs and Micro-ISR slots
  await revalidateTag("feed");
}
Zero Skeleton Flash (startTransition + Stale-Preserving Fallback)

While the server re-renders an expired slot during an interactive refresh, the user does NOT see a blank skeleton flash or layout shift. Dinou's internal client boundary wraps slot updates in React.startTransition and sets fallback: children (the previous DOM content). The existing widget stays interactive and visible until the new server stream arrives, then swaps seamlessly.

To display an inline loading indicator on the button itself, wrap refreshSlot inside React's standard useTransition():

"use client";
import { useTransition } from "react";
import { refreshSlot } from "dinou";

export function RefreshAnalyticsButton() {
  const [isPending, startTransition] = useTransition();

  const handleRefresh = () => {
    startTransition(() => {
      refreshSlot("analytics-widget");
    });
  };

  return (
    <button onClick={handleRefresh} disabled={isPending}>
      {isPending ? "Refreshing Metrics..." : "Refresh Metrics"}
    </button>
  );
}

4.3. Horizontal vs. Vertical Segmentation & Anti-Patterns

DimensionScopeConfigurationBest For
Horizontal SegmentationRoute / Segment Levelpage_functions.ts, layout_functions.tsWhole-page SSG, ISR, and layout persistence.
Vertical SegmentationComponent Subtree Level<DinouCacheSlot> from dinou/serverIndependent widget TTLs, asymmetric revalidation, and in-place client refreshes.
When NOT to Use <DinouCacheSlot>
  1. 100% Static Pages (SSG): If an entire route is pre-rendered at build time with no revalidation, it is already cached as immutable HTML at the CDN. Slots add zero benefit.
  2. Wrapping the Entire Page: Wrapping the entire page.tsx body inside a slot reinvents horizontal caching. Declare export const revalidate = 300 in page_functions.ts instead.
  3. Unpartitioned Private User Data: Never wrap private user data (such as a specific user's cart or session token) with a static slot ID. This would cache user A's private data and leak it to user B.

5. High-Traffic Synergy: PPR + <DinouCacheSlot>

Nesting <DinouCacheSlot> inside a React <Suspense> boundary on a route with Partial Prerendering (PPR) unlocks the highest performance profile possible: zero-delay initial delivery (0ms TTFB) paired with near-zero streaming delay (~2ms) and 99.9% database query offloading.

  • PPR Alone (Uncached Dynamic Hole): 0ms static shell from Edge CDN, but the hole waits for live DB execution (e.g. 1.5s) on every single request. 10,000 visitors/min = 10,000 live DB queries.
  • PPR + <DinouCacheSlot> (Cached Streamed Hole): 0ms static shell from Edge CDN + hole resolves from server cache in ~2ms. 10,000 visitors/min = 1 database query per TTL window (99.9% database offload).

Implementation Pattern: High-Traffic Telemetry Dashboard

// src/telemetry/page_functions.ts
import type { PageFunctions } from "dinou";

// Enable Partial Prerendering on this high-traffic route
export const ppr: PageFunctions["ppr"] = true;
// src/telemetry/page.tsx
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou/server";
import { TelemetryHeader, TelemetryFooter } from "./static-shell";
import { HeavyMetricsGraph, GraphSkeleton } from "./graph-widgets";

export default function TelemetryPage() {
  return (
    <main className="container py-8 space-y-6">
      {/* 1. Static Shell: Delivered instantly in 0ms TTFB from Edge CDN */}
      <TelemetryHeader />

      {/* 2. PPR Dynamic Hole + Micro-ISR Slot:
             - Fallback skeleton sent immediately in initial layout
             - Hole streams from server cache in ~2ms for subsequent visitors
             - Database query runs only ONCE every 60 seconds */}
      <Suspense fallback={<GraphSkeleton />}>
        <DinouCacheSlot id="operations-telemetry" tag="telemetry" revalidate={60}>
          <HeavyMetricsGraph />
        </DinouCacheSlot>
      </Suspense>

      <TelemetryFooter />
    </main>
  );
}

6. Incremental Static Generation (ISG on-demand)

While ISR updates existing pre-rendered pages, ISG (Incremental Static Generation) generates new dynamic paths on demand that were omitted from the build-time getStaticPaths list.

6.1. On-Demand Path Generation

Any route path omitted from getStaticPaths will be generated dynamically when the first visitor requests it, cached, and then served as static HTML to subsequent visitors:

// src/products/[id]/page_functions.ts

// Pre-render only the top 10 products at build time:
export const getStaticPaths = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"];

// Any other ID (e.g. /products/452) will be generated on-demand
// on its first visitor and cached permanently.

6.2. Governance APIs (validateParams & allowISG)

Dinou provides strict server-side governance exports to secure on-demand generation against cache pollution and DDoS:

  • validateParams(params): Validates route parameters before generation begins. Returning false halts rendering immediately and serves an instant 404:
    export function validateParams(params: { id: string }) {
      return /^\d+$/.test(params.id); // Only allow numeric IDs
    }
    
  • allowISG: Disables on-demand generation entirely. When set to false, any path not pre-rendered in getStaticPaths returns 404 without querying the server:
    export const allowISG = false; // Strict SSG only
    

7. Complete Rendering Strategies Matrix

Summary of all rendering strategies supported natively by Dinou v7:

StrategyHow to ConfigureTTFBFreshnessDatabase LoadBest For
Pure SSGDefault / revalidate = false0ms (Instant)Build-time0 queries (Build-time only)Landing pages, marketing, documentation
ISRrevalidate = N (N >= 1)0ms (Cached)Stale-while-revalidate1 query per TTL (0 on cache hits)Blogs, catalogs, news feeds
Dynamic SSRrevalidate = 0Request-time (Blocked)100% Real-timeHeavy: 100% queries on every requestUser settings, banking, checkout
PPR (Partial Prerendering)ppr = true + <Suspense>0ms (Shell from Edge)100% Real-time (Holes)Heavy: 100% Hole queries on every requestE-commerce, dashboards with static shells
Micro-ISR (Static / Asymmetric ISR)<DinouCacheSlot> (in Static/ISR page)0ms (CDN Edge)Decoupled Slot TTL / TagsLow: Isolated to expired slotsStatic product/catalog pages with volatile widgets (stock, prices)
Micro-ISR (Dynamic SSR)<DinouCacheSlot> (in Dynamic SSR page)Fast (Early Layout / Slot-cached)Component TTL / TagsLow: Isolated to expired slotsDynamic user dashboards with heavy SQL calculations
PPR + Micro-ISR (Synergy)ppr = true + <DinouCacheSlot>0ms (Edge) + ~2ms (Hole)Cached Streamed HoleNear-Zero: 1 query per slot TTL (99.9% reduction)High-concurrency e-commerce, telemetry dashboards
ISG (Incremental Static)allowISG = true (default)0ms (after 1st hit)Cached permanently1 query on 1st generation (0 thereafter)Long-tail e-commerce products, dynamic paths

[!TIP] For deep-dive code recipes, runtime execution timing, TTFB charts, and production trade-offs across all 13 rendering and data fetching patterns, see Data Fetching: The 13 Strategies below.


Server Functions ("use server")

Server Functions are asynchronous RPC endpoints that run exclusively in the server runtime. In Dinou, you declare them by adding the "use server" directive at the top of a dedicated action file (module-level directive).

Overview & "use server" Directive

CapabilityArchitectural Benefit
Zero API BoilerplateNo manual route handlers, fetch() boilerplate, or JSON serialization required. Dinou generates typed Flight RPC endpoints (POST /____server_function____) automatically.
JSX & Data ReturnsUnlike standard RPCs that only return JSON, Dinou Server Functions can return fully rendered React components streamed over Flight RPC (The Dinou Pattern).
Security & SecretsSecret keys, database connections, and ORM calls are never bundled into the client-side JavaScript chunk.
Form & Action SynergyNative integration with HTML <form action={...}> and React 19 primitives (useActionState, useFormStatus, useOptimistic).

1. Returning Live React Components (UI Streaming)

Dinou uniquely allows Server Functions to return fully rendered React component trees directly over Flight RPC. In Server Components, the component is awaited directly inside native <Suspense>; in Client Components, the returned promise is unwrapped with React 19's use() hook:

Pattern A: Server Function (Returning Rendered JSX)

// src/actions/get-post-card.tsx
"use server"; // 👈 Module-level directive in Dinou

import db from "@/lib/db";
import { PostCard } from "@/components/post-card";

export async function getPostCard(postId: string) {
  const post = await db.posts.findUnique({ where: { id: postId } });
  if (!post) throw new Error("Post not found");

  // Returns a rendered JSX component directly over Flight RPC!
  return <PostCard post={post} />;
}

Pattern B: Consuming in Server Components (Async / Await)

Directly await the Server Function inside an async component. React streams HTML progressively as promises resolve:

// src/blog/page.tsx
// Server Component: directly await the Server Function with React 19 Suspense
import { Suspense } from "react";
import { getPostCard } from "@/actions/get-post-card";

async function PostSlot({ id }: { id: string }) {
  // Directly awaited on the server during SSR / streaming
  return await getPostCard(id);
}

export default function BlogPage() {
  return (
    <section className="space-y-4">
      <h1 className="text-2xl font-bold">Featured Story</h1>
      
      {/* Native React 19 Suspense fallback */}
      <Suspense fallback={<div className="skeleton h-48 w-full rounded" />}>
        <PostSlot id="post-101" />
      </Suspense>
    </section>
  );
}

Pattern C: Consuming in Client Components (with React 19 use())

Dispatches Flight RPC returning a Promise. React 19's native use(promise) unrolls it synchronously inside <Suspense> without external state libraries, and re-suspends cleanly on promise update:

// src/blog/page.tsx
// Client Component: unwrap the Flight RPC promise with React 19 use()
"use client";

import { useState, use, Suspense } from "react";
import { getPostCard } from "@/actions/get-post-card";

function PostStream({ promise }: { promise: Promise<React.ReactNode> }) {
  // Pure React 19 'use()' unrolls the promise within Suspense
  const content = use(promise);
  return <>{content}</>;
}

export default function BlogPage() {
  const [postId, setPostId] = useState("post-1");
  const [postPromise, setPostPromise] = useState(() => getPostCard("post-1"));

  const loadPost = (id: string) => {
    setPostId(id);
    setPostPromise(getPostCard(id));
  };

  return (
    <section className="space-y-4">
      <h1 className="text-2xl font-bold">Featured Story</h1>

      <div className="flex gap-2">
        <button onClick={() => loadPost("post-1")} className="btn">Post 1</button>
        <button onClick={() => loadPost("post-2")} className="btn">Post 2</button>
      </div>

      {/* Re-suspends cleanly on promise update */}
      <Suspense key={postId} fallback={<p>Streaming post from server...</p>}>
        <PostStream promise={postPromise} />
      </Suspense>
    </section>
  );
}

[!NOTE] Execution Comparison: Server vs. Client Components

  • In Server Components (Async / Await): Directly await the Server Function inside an async component. React streams HTML progressively as promises resolve.
  • In Client Components (with use()): Dispatches Flight RPC returning a Promise. React 19's native use(promise) unrolls it synchronously inside <Suspense> without external state libraries.

2. Server Actions & Form Mutations

Dinou integrates seamlessly with standard HTML forms and React 19 form actions for data mutations.

2.1. Server Actions in Forms

You can pass a Server Action directly to the native HTML <form action={...}> attribute:

// src/components/newsletter-page.tsx
"use client";

import { subscribeNewsletter } from "@/actions/subscribe";

export default function NewsletterPage() {
  return (
    <form action={subscribeNewsletter} className="flex gap-2">
      <input name="email" type="email" placeholder="[email protected]" required className="input" />
      <button type="submit" className="btn-primary">Subscribe</button>
    </form>
  );
}
// src/actions/subscribe.ts
"use server"; // 👈 Required at the module level in Dinou

import db from "@/lib/db";

export async function subscribeNewsletter(formData: FormData) {
  const email = formData.get("email") as string;
  await db.subscribers.create({ data: { email } });
}

[!NOTE] Module-Level Directives & Client Execution Dinou requires the "use server" directive to be placed at the top of the action file (not inline within function bodies). When a form submits, Dinou's client runtime transparently dispatches a Flight RPC request (POST /____server_function____) to execute the server action and receive updated UI state.

[!TIP] What does subscribeNewsletter return? (Understanding void / undefined) This action returns Promise<void>, which resolves to undefined. In pure React 19 form actions (<form action={subscribeNewsletter}>), this is completely standard for fire-and-forget mutations: the browser submits the form data, the server executes the database write, and no state payload is needed by the UI.

When should a Server Action return a value? If your UI needs to display interactive feedback (such as "Subscribed successfully!" or validation errors), the action should return a serializable payload (e.g. { success: true, message: "..." }) and be wired to React 19's useActionState hook (as shown below in Section 2.2). Other common patterns include performing a server-side redirect() or calling revalidatePath() before completing.


2.2. React 19 Hooks: useActionState & useFormStatus

For rich interactive forms with validation feedback and loading spinners, use React 19's standard hooks:

  • useActionState (React 19): Captures the returned state (errors, success payload) and pending status of the Server Action across submissions.
  • useFormStatus (react-dom): Reads the pending submission status of the parent <form> without prop drilling.
// src/components/create-post-form.tsx
"use client";

import { useActionState } from "react";
import { useFormStatus } from "react-dom";
import { createPostAction, type FormState } from "@/actions/posts";

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending} className="btn-primary">
      {pending ? "Saving..." : "Publish Post"}
    </button>
  );
}

const initialState: FormState = { success: false, message: "" };

export function CreatePostForm() {
  // useActionState binds the server action to form state and tracks submission
  const [state, formAction, isPending] = useActionState(createPostAction, initialState);

  return (
    <form action={formAction} className="flex flex-col gap-4">
      {state.message && (
        <div className={state.success ? "text-green-500" : "text-red-500"}>
          {state.message}
        </div>
      )}

      <input name="title" placeholder="Post title" required className="input" />
      <textarea name="content" placeholder="Write content..." required className="textarea" />
      
      <SubmitButton />
    </form>
  );
}
// src/actions/posts.ts
"use server"; // 👈 Module-level directive required in Dinou

import db from "@/lib/db";

export type FormState = {
  success: boolean;
  message: string;
};

// Server Action receives (prevState, formData) when bound to useActionState
export async function createPostAction(
  prevState: FormState,
  formData: FormData
): Promise<FormState> {
  const title = (formData.get("title") as string)?.trim();
  const content = (formData.get("content") as string)?.trim();

  if (!title || title.length < 3) {
    return { success: false, message: "Title must be at least 3 characters." };
  }

  if (!content) {
    return { success: false, message: "Content cannot be empty." };
  }

  await db.posts.create({ data: { title, content } });
  return { success: true, message: "Post published successfully!" };
}

2.3. Optimistic Updates with useOptimistic

With React 19's useOptimistic, you can update the user interface immediately before the server responds:

// src/components/like-button.tsx
"use client";

import { useOptimistic, useTransition } from "react";
import { toggleLikeAction } from "@/actions/likes";

export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
  const [isPending, startTransition] = useTransition();
  const [optimisticLikes, setOptimisticLikes] = useOptimistic(
    initialLikes,
    (current, update: number) => current + update
  );

  const handleClick = () => {
    startTransition(async () => {
      setOptimisticLikes(1); // Optimistically increment in 0ms on client
      await toggleLikeAction(postId); // Dispatches Server Action via Flight RPC
    });
  };

  return (
    <button onClick={handleClick} disabled={isPending} className="btn-like">
      ❤️ {optimisticLikes}
    </button>
  );
}
// src/actions/likes.ts
"use server"; // 👈 Module-level directive required in Dinou

import db from "@/lib/db";

export async function toggleLikeAction(postId: string) {
  // Updates like count on the database
  await db.posts.update({
    where: { id: postId },
    data: { likes: { increment: 1 } },
  });

  return { success: true };
}

[!NOTE] Why is there no <Suspense> boundary here? <Suspense> only activates when a component suspends (by throwing a pending Promise, such as with React.use(promise) or an async Server Component).

In contrast, useOptimistic does the exact opposite of suspending: it synchronously paints the temporary new value (e.g. switching immediately from 10 to 11) in 0ms on the client, avoiding loading spinners or skeleton fallbacks entirely while the Server Action runs in the background. It relies strictly on React 19 transitions (useTransition or Form Actions). If the server call succeeds, React seamlessly commits the real server state; if it fails, React automatically rolls back to the initial value with zero boilerplate.


3. Request Context, Cookies & Headers (getContext)

Dinou provides getContext() (exported from "dinou") to access the underlying request and response lifecycle synchronously within any Server Function or SSR component.

RequestContextStore API Reference:

  • context.req.cookies: Dictionary of incoming client cookies (e.g. cookies["session_token"]).
  • context.req.headers: Incoming HTTP headers dictionary (e.g. headers["user-agent"]).
  • context.req.query: URL search parameters.
  • context.res.cookie(name, value, options): Sets an HTTP cookie with options (httpOnly, secure, maxAge, sameSite, path, etc.).
  • context.res.clearCookie(name, options): Clears a cookie from the client.

3.1. Reading Query, Headers & Cookies

// src/actions/auth-check.ts
"use server";

import { getContext } from "dinou";

export async function getCurrentUser() {
  const context = getContext();
  if (!context) return null;

  const sessionToken = context.req.cookies["session_token"];
  const userAgent = context.req.headers["user-agent"];

  if (!sessionToken) return null;
  return await verifySession(sessionToken);
}

3.2. Setting & Clearing Cookies

// src/actions/login.ts
"use server";

import { getContext } from "dinou";

export async function loginAction(formData: FormData) {
  const context = getContext();
  const email = formData.get("email") as string;
  const password = formData.get("password") as string;

  const session = await authenticateUser(email, password);

  // Set secure HTTP-only session cookie
  context?.res.cookie("session_token", session.token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax",
    maxAge: 60 * 60 * 24 * 7, // 7 days in seconds
    path: "/",
  });

  return { success: true };
}

export async function logoutAction() {
  const context = getContext();
  // Clear the cookie upon logout
  context?.res.clearCookie("session_token", { path: "/" });
  return { success: true };
}

4. Redirections (redirect)

Dinou provides a universal redirect(destination) utility exported from "dinou":

  • In Server Functions (Soft SPA Navigation): When called inside a Server Function (e.g. form mutations or RPC calls), Dinou automatically performs a smooth client-side transition to the destination route via the SPA router without a full page reload.
  • In Server Components (Initial SSR): If headers have not yet been sent, Dinou issues a clean HTTP 302 redirect for SEO and search engine crawlers. If response streaming has already started, it completes the transition on the client upon hydration.
  • In Client Components (Declarative Redirects): When called directly in client components during render, it triggers an immediate client router replacement (router.replace()) without reloading the page.
// src/actions/checkout.ts
"use server";

import { redirect } from "dinou";
import db from "@/lib/db";

export async function completeOrder(orderId: string) {
  await db.orders.updateStatus(orderId, "PAID");

  // Automatically navigates the client router to the success page
  return redirect(`/checkout/${orderId}/success`);
}

4.1. The 4 Execution Contexts (HTTP 302 vs. Client Transition)

A foundational question when handling redirects across hybrid architectures is: How does Dinou determine whether redirect() issues an HTTP 302 vs. a client-side transition, and is calling redirect() in a Server Component equivalent to invoking it via a Server Function?

[!NOTE] The Equivalence Principle in Server Components Calling redirect() directly inside a Server Component vs. calling an imported Server Function (e.g., await verifyUser()) from that Server Component is functionally identical. Because both run inside the same server Node.js process during the same SSR request lifecycle, they share the exact same request context and streaming state.

Context 1: SSR Root (page_functions.ts, page.tsx, Plugin) -> HTTP 302

Occurs inside getProps() in page_functions.ts, at the top level of the root page.tsx Server Component (outside any <Suspense> boundary), or in a global plugin hook (onRequest) in dinou.config.mjs. Because zero HTML bytes have reached the wire (headersSent === false), Dinou issues a clean HTTP 302 with a Location header. Crucial for SEO crawlers and search indexers.

// src/dashboard/page_functions.ts
import { redirect, getContext } from "dinou";

export async function getProps() {
  const { req } = getContext();
  if (!req.cookies.session_token) {
    // Evaluated before page rendering -> HTTP 302 redirect
    return redirect("/login");
  }
  return { user: { name: "Alex" } };
}
// src/dashboard/page.tsx (Server Component root)
import { redirect, getContext } from "dinou";

export default async function DashboardPage() {
  const { req } = getContext();
  if (!req.cookies.session_token) {
    // At root before stream begins -> HTTP 302 redirect
    return redirect("/login");
  }

  return <h1>Welcome to Dashboard</h1>;
}
// dinou.config.mjs (Global Plugin Hook)
import { defineConfig } from "dinou/config";

export default defineConfig({
  plugins: [
    {
      name: "auth-guard",
      async onRequest(request, context) {
        const url = new URL(request.url);
        const cookieHeader = request.headers.get("cookie") || "";
        // Runs pre-flight before SSR or route resolution
        if (url.pathname.startsWith("/dashboard") && !cookieHeader.includes("session_token=")) {
          return Response.redirect(new URL("/login", request.url), 302);
        }
      },
    },
  ],
});
Context 2: SSR Deferred () -> Client Hydration

Occurs inside an async child wrapped in <Suspense fallback={<Skeleton />}>. React opens the HTTP connection with status 200 OK immediately to stream the initial shell. At HTTP protocol level, an open 200 OK stream cannot turn into a 302. Dinou safely emits <ClientRedirect />, completing the transition upon browser hydration without breaking the stream:

// src/reports/page.tsx (Server Component)
import { Suspense } from "react";
import { redirect } from "dinou";

async function RestrictedReport() {
  const isAllowed = await checkUserAccess();
  if (!isAllowed) {
    // 200 OK stream already open -> <ClientRedirect />
    return redirect("/unauthorized");
  }
  return <div>Financial Metrics</div>;
}

export default function Page() {
  return (
    <Suspense fallback={<p>Loading financial report...</p>}>
      <RestrictedReport />
    </Suspense>
  );
}
Context 3: Server Function from Client -> Soft SPA Navigation

Triggered from browser interactions (form actions or button clicks) via a dedicated POST request. Dinou executes the server mutation and intercepts redirect() before streaming (headers unsent), returning redirect metadata. The client proxy navigates via the SPA router without a full page reload:

// src/actions/cart.ts
"use server";

import { redirect } from "dinou";
import db from "@/lib/db";

export async function checkoutAction(formData: FormData) {
  const order = await db.orders.create({ /* ... */ });

  // Dedicated POST: soft SPA navigation in browser without page reload
  return redirect(`/orders/${order.id}`);
}
Context 4: Client Component (Render) -> Client Router Replacement

Executes 100% in the browser with zero server roundtrip. Immediately dispatches router.replace() in the client SPA router, updating route and UI in 0ms:

// src/components/client-guard.tsx
"use client";

import { redirect } from "dinou";

export function ClientGuard({ isAllowed }: { isAllowed: boolean }) {
  if (!isAllowed) {
    // In-browser render: executes router.replace() in SPA router
    return redirect("/forbidden");
  }

  return <button className="btn">Control Panel</button>;
}

4.2. Universal Redirection Matrix

Invocation ContextHeaders Sent?Resulting MechanismRecommended For
1. SSR Root
(page_functions.ts, root page.tsx, Plugin)
NoHard HTTP Redirect (Status 302)
Server issues an HTTP/1.1 302 Found with a Location header. The browser or search crawler intercepts it at the protocol level and fetches the new page as a fresh document.
Auth guards (getProps), root guards, global middleware plugins, SEO canonicals
2. SSR: Deferred
(<Suspense>)
Yes
(200 OK stream open)
Streaming Fallback (<ClientRedirect />)
Because the 200 OK chunked stream cannot be rewritten to a 302, Dinou injects <ClientRedirect /> into the stream. The browser router transitions upon hydration without aborting the connection.
Deferred conditional access, late data-dependent checks inside Suspense
3. Server Function
(from Client Action)
No
(standard POST)
Soft SPA Navigation (Status 200)
Server returns HTTP 200 OK with redirection metadata. Dinou's client runtime intercepts it and navigates via the SPA router without a full page reload or loss of client state.
Form submissions, shopping cart checkout, stateful mutations
4. Client Component
(Render)
N/A
(in-browser)
Client Router Replacement (router.replace())
Executes 100% in the browser with zero server roundtrip. Immediately dispatches router.replace() in the client SPA router, updating route and UI in 0ms.
Pure UI state redirection, client-side permission gates

[!TIP] Looking for Cache Revalidation? Learn how to purge stale SSG pages and ISR cache slots on-demand using revalidatePath and revalidateTag in On-Demand Revalidation below.


On-Demand Revalidation

High-performance web applications cannot rely exclusively on time-based expiration (TTL). On-Demand Revalidation in Dinou enables instantaneous cache purging and static regeneration across four specialized tiers:

Overview & Multi-Tier Architecture

Invalidation TierAPI & MechanismArchitectural Scope
Surgical PagerevalidatePage(path)Regenerates exclusively page.rsc and index.html, leaving all parent layout shells completely untouched.
Route SegmentrevalidatePath(path)Purges page RSC, HTML, and any layout.rsc in that segment. Supports recursive propagation with { cascade: true }.
Cross-Cutting TagrevalidateTag(tag)Selectively updates layouts, pages, or micro-slots across disparate routes sharing a common metadata tag key.
Micro-Slot<DinouCacheSlot>Isolates volatile component subtrees refreshed independently via refreshSlot() or targeted revalidateTag().

Server revalidation APIs are exported from the server-only endpoint "dinou/server", keeping browser client bundles clean, lightweight, and secure.

[!CAUTION] Strict Execution Rules: Where to Call Revalidation APIs

  • Only in Mutative Event Contexts: Revalidation APIs (revalidatePage, revalidatePath, revalidateTag) are mutative side-effects. They must only be invoked inside Server Functions ("use server") or HTTP Webhook endpoints (such as Headless CMS publishing events).
  • Never Inside Components or getProps: Calling revalidation during the component render phase or within getProps creates an infinite regeneration loop, causing severe CPU exhaustion and server crashes.

1. revalidatePage (Surgical Page Invalidation)

Exported from "dinou/server", revalidatePage(path) is specifically designed for scenarios where only the leaf page content has changed. It provides two critical advantages:

  1. Maximum Performance: Avoids the CPU a

Truncated — view the full README on GitHub.

framework
reactjs

Project health

Latest release

7.0.0 · Oct 14, 2026

Releases this year

43

Significant stargazers

Lawrence Lin

220 followers · starred Jun 2026