📚 Complete API Reference
Explore all exports across dinou, dinou/server, and route lifecycle contracts (page_functions.ts & layout_functions.ts) with TypeScript signatures, runtime behavior, and practical usage patterns.
1. Components (dinou)Universal / Client
<Link>
"use client"Universal NavigationThe primary declarative navigation primitive in Dinou. Renders a semantic HTML <a> tag enhanced with client-side SPA soft navigation, automatic background prefetching on hover, relative filesystem path resolution, and optional cache bypassing.
Intercepts clicks to perform seamless client-side page transitions via push() without triggering full browser reloads. If modifier keys (Cmd, Ctrl, Shift, Alt) are pressed or an external destination is specified, it gracefully yields to standard browser navigation (e.g. opening in a new tab).
By default, hovering over a link triggers an automatic background prefetch of the destination RSC payload and script chunks. When real-time accuracy is required, passing fresh= disables prefetching and forces an on-demand server roundtrip on click.
| Prop | Type | Default | Description |
|---|---|---|---|
| href | DinouRoute | string | — | Primary destination URL. Strongly typed with DinouRoute for compile-time route validation and IDE autocomplete. Supports absolute paths (/dashboard), relative paths (./settings, ../billing), query strings, and external URLs. |
| to | DinouRoute | string | — | Alias for href provided for compatibility with React Router and Remix codebases (evaluated as href || to). |
| prefetch | boolean | true | Whether to prefetch the destination RSC payload and code bundle on onMouseEnter. Ignored when fresh= or when targeting external URLs. |
| fresh | boolean | false | Forces an on-demand server roundtrip upon navigation, bypassing any client-side router cache. Automatically skips hover prefetching when set to true. |
| ...props | AnchorHTMLAttributes | — | All standard HTML anchor attributes (className, target, rel, style, aria-*, onClick). Custom onClick handlers execute first; calling e.preventDefault() prevents soft navigation. |
Filesystem Path Resolution
pathname:- Absolute (
/): Starts with a forward slash (e.g.,/dashboard). Resolves strictly from the application root domain. - Relative Child (
./): Resolves under the current segment (e.g., from/products, navigating to./detailsresolves to/products/details). - Relative Sibling (
../): Navigates up one directory level (e.g., from/users/settings, navigating to../profileresolves to/users/profile).
// src/components/nav-bar.tsx (Client Component)
"use client";
import { Link, usePathname } from "dinou";
export function NavigationBar() {
const pathname = usePathname();
const navItems = [
{ href: "/dashboard", label: "Dashboard" },
{ href: "/orders", label: "Orders" },
{ href: "/analytics", label: "Analytics" },
{ href: "/settings", label: "Settings" },
];
return (
<nav className="flex items-center gap-4 p-4 border-b">
{navItems.map((item) => {
const isActive = pathname === item.href || pathname.startsWith(`${item.href}/`);
return (
<Link
key={item.href}
href={item.href}
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
isActive
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
}`}
>
{item.label}
</Link>
);
})}
</nav>
);
}// src/components/order-actions.tsx (Client Component)
"use client";
import { Link } from "dinou";
import { RefreshCw, ExternalLink } from "lucide-react";
export function OrderQuickActions() {
return (
<div className="flex items-center gap-3">
{/* Normal soft navigation with hover prefetch */}
<Link
href="/orders/archive"
className="text-xs text-muted-foreground hover:underline"
>
View Archive
</Link>
{/* Forced server round-trip: bypasses client RSC cache */}
<Link
href="/orders/live"
fresh
prefetch={false}
className="inline-flex items-center gap-1.5 px-3 py-1 bg-emerald-600 text-white text-xs rounded hover:bg-emerald-700"
>
<RefreshCw className="h-3.5 w-3.5" />
<span>Live Monitor (Force Fresh)</span>
</Link>
</div>
);
}// src/app/dashboard/settings/page.tsx
"use client";
import { Link } from "dinou";
export default function SettingsSubNav() {
const handleAuditClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
// Custom logging or analytics before navigation
console.log("Navigating to security audit");
// Calling e.preventDefault() here would cancel the soft navigation
};
return (
<div className="flex flex-col gap-2">
{/* Relative Child navigation: resolves to /dashboard/settings/security */}
<Link href="./security" onClick={handleAuditClick} className="text-sm font-medium">
Security Settings
</Link>
{/* Relative Sibling navigation: resolves to /dashboard/billing */}
<Link href="../billing" className="text-sm text-muted-foreground">
Billing & Subscriptions
</Link>
{/* External Link: automatically falls back to native browser navigation */}
<Link
href="https://status.example.com"
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-500 underline"
>
System Status
</Link>
</div>
);
}<ClientRedirect>
"use client"Declarative PrimitiveA declarative Client Component that triggers an immediate, seamless route replacement upon mounting in the browser. It renders no DOM markup (null) and executes a soft router replacement (replace: true), ensuring that the redirected URL does not create trap loops in the user's browser back history.
redirect()While redirect() is the imperative function typically used with early returns (e.g. return redirect("/login")), <ClientRedirect> is the underlying JSX component returned by redirect(). It enables you to express redirections declaratively directly inside your JSX element tree.
replace: true)When executed, <ClientRedirect> performs a router.replace() rather than a push(). This replaces the current entry in the browser's history stack so users clicking the browser Back button are not immediately bounced forward again in an infinite redirect loop.
| Prop | Type | Default | Description |
|---|---|---|---|
| to | string | Required | The destination path to navigate to. Supports absolute internal paths (e.g. "/auth/sign-in"), relative paths (e.g. "./onboarding"), and query parameters (e.g. "/pricing?plan=pro"). |
// src/components/billing-gate.tsx (Client Component)
"use client";
import { ClientRedirect } from "dinou";
interface BillingGateProps {
hasActiveSubscription: boolean;
children: React.ReactNode;
}
export function BillingGate({ hasActiveSubscription, children }: BillingGateProps) {
// Declarative ternary redirection directly inside JSX
return hasActiveSubscription ? (
<>{children}</>
) : (
<ClientRedirect to="/pricing?upgrade=required" />
);
}// src/components/client-guard.tsx (Client Component)
"use client";
import { ClientRedirect } from "dinou";
import { useAuth } from "@/hooks/use-auth";
export function ClientAuthGuard({ children }: { children: React.ReactNode }) {
const { user, isLoading } = useAuth();
if (isLoading) {
return <div className="p-6 text-sm text-muted-foreground">Loading session...</div>;
}
if (!user) {
// Replaces browser history with /sign-in immediately upon mount
return <ClientRedirect to="/auth/sign-in?from=/dashboard" />;
}
return <>{children}</>;
}// Pattern A: Imperative function return (Universal: Server & Client)
import { redirect } from "dinou";
export default function PageA({ isAllowed }: { isAllowed: boolean }) {
if (!isAllowed) {
return redirect("/unauthorized");
}
return <div>Welcome to Page A</div>;
}
// Pattern B: Declarative JSX Component (Client Component)
import { ClientRedirect } from "dinou";
export function PageB({ isAllowed }: { isAllowed: boolean }) {
if (!isAllowed) {
return <ClientRedirect to="/unauthorized" />;
}
return <div>Welcome to Page B</div>;
}3. Type-Safe Routing (dinou)Zero-Runtime Type System
Dinou v7 introduces zero-runtime type safety with auto-generated declaration files and seamless IDE autocompletion for routes and parameters.
PageProps<T>
TypeScript Genericpage.tsxOfficial TypeScript interface for typing React Page components (page.tsx). Takes the route pattern string (constrained by DinouRoutePattern) as a generic parameter and automatically infers the strongly typed params object.
import type { PageProps } from "dinou";
// 1. Single dynamic route: trigger Ctrl + Space inside "" to pick from registered routes
export default function BlogPost({ params }: PageProps<"/blog/[slug]">) {
// ✅ params.slug is strongly typed as string
return <h1>Post: {params.slug}</h1>;
}
// 2. Catch-all dynamic route
export function DocsPage({ params }: PageProps<"/docs/[...slug]">) {
// ✅ params.slug is strongly typed as string[]
return <h1>Document: {params.slug.join("/")}</h1>;
}LayoutProps<T>
TypeScript Genericlayout.tsxOfficial TypeScript interface for typing React Layout components (layout.tsx). In addition to inferred params, it strictly types children: React.ReactNode.
import type { LayoutProps } from "dinou";
export default function BlogLayout({ children, params }: LayoutProps<"/blog/[slug]">) {
return (
<section>
<header>Category: {params.slug}</header>
<main>{children}</main>
</section>
);
}ErrorProps<T>
TypeScript Genericerror.tsxOfficial TypeScript interface for typing React Error Page components (error.tsx). In addition to inferred params, it strictly types the caught error: Error.
import type { ErrorProps } from "dinou";
export default function DashboardError({ error, params }: ErrorProps<"/dashboard/[teamId]">) {
return (
<div>
<h2>Failed to load team {params.teamId}</h2>
<p>{error.message}</p>
</div>
);
}PageFunctions<TParams, TProps> & LayoutFunctions<TParams, TProps>
TypeScript GenericsServer Lifecycle ContractsCanonical TypeScript interfaces for typing server lifecycle files (page_functions.ts and layout_functions.ts). Exported from dinou and re-exported from dinou/server for developer convenience. Both interfaces accept either a route pattern string directly (e.g. "/blog/[slug]") or a direct parameters dictionary, providing complete compile-time type safety, IDE autocompletion, and index-access typing (e.g. PageFunctions["revalidate"]) without boilerplate.
import type { PageFunctions, LayoutFunctions } from "dinou";
// 1. Direct route string pattern (automatically infers route parameters)
export const getProps: PageFunctions<
"/blog/[slug]",
{ postTitle: string }
>["getProps"] = async (params) => {
return { postTitle: `Article: ${params.slug}` };
};
// 2. Strict index-access typing for layout_functions.ts
export const ppr: LayoutFunctions["ppr"] = true;
export const revalidate: LayoutFunctions["revalidate"] = 3600;RouteParams<T>
TypeScript UtilityParams InferenceA standalone utility type representing the inferred parameters dictionary of a given route pattern. While PageProps and PageFunctions resolve route strings directly, RouteParams is ideal for typing Server Actions, services, or custom helper functions that do not inherit framework props automatically.
| Pattern Segment | Route Example | Inferred RouteParams<T> |
|---|---|---|
| [id] | /products/[id] | { id: string } |
| [...slug] | /docs/[...slug] | { slug: string[] } |
| [[...slug]] | /shop/[[...slug]] | { slug?: string[] } |
| [[tab]] | /settings/[[tab]] | { tab?: string } |
import type { RouteParams } from "dinou";
// 1. Single dynamic parameter
export async function getAuthorBio(params: RouteParams<"/blog/[slug]">) {
return fetchBioBySlug(params.slug); // params.slug is string
}
// 2. Catch-all dynamic parameter
export async function getDocsContent(params: RouteParams<"/docs/[...slug]">) {
return fetchDocsByPath(params.slug.join("/")); // params.slug is string[]
}
// 3. In custom helper types (note: for error.tsx, prefer native ErrorProps<T>)
interface CustomErrorWrapper {
error: Error;
params: RouteParams<"/blog/[slug]">;
}DinouRoute
Union TypeOutward NavigationA union type representing all valid destinations in the application. It powers <Link href> and router.push(href), automatically validating internal application routes, queries (?key=val), hashes (#section), relative paths (./, ../), and external URLs (https://...).
import type { DinouRoute } from "dinou";
import { Link, useRouter } from "dinou";
// 1. Typing custom navigation components
interface NavButtonProps {
href: DinouRoute;
label: string;
}
export function NavButton({ href, label }: NavButtonProps) {
return <Link href={href}>{label}</Link>;
}
// 2. Programmatic navigation with useRouter
export function ActionPanel() {
const router = useRouter();
const handleNavigate = (destination: DinouRoute) => {
router.push(destination);
};
return <button onClick={() => handleNavigate("/docs/api-reference")}>API</button>;
}DinouRoutePattern
Union TypeInward DefinitionA union type representing all registered filesystem folder patterns across your application (e.g. "/blog/[slug]", "/docs/[...slug]"). It is used as the generic constraint for PageProps<T> and LayoutProps<T>, enabling instant IDE autocomplete via Ctrl + Space and guaranteeing compliance with TypeScript declaration emit rules (TS4058).
import type { DinouRoutePattern, PageProps } from "dinou";
// 1. Creating strongly typed page helper wrappers or metadata generators
export function definePageMeta<T extends DinouRoutePattern>(
route: T,
generator: (props: PageProps<T>) => { title: string; description?: string }
) {
return generator;
}
// ✅ Valid: autocompletes route patterns and infers correct params
export const postMeta = definePageMeta("/blog/[slug]", ({ params }) => {
return { title: `Reading: ${params.slug}` };
});
// ❌ TypeScript error if using an unregistered route pattern
// definePageMeta("/invalid/route/[id]", ...);4. Server Context & Config (dinou)Server-Side Execution
getContext()
Server-Only PrimitiveReturns: RequestContextStore | undefinedSynchronously accesses the active HTTP request and response context via Node.js / V8 AsyncLocalStorage. In Dinou, getContext() operates across Server Components (RSC), Server Functions ("use server"), and route segment contracts in page_functions.ts and layout_functions.ts without prop drilling.
Request & Response Store Reference
Overview of the read-only request metadata (ctx.req) and server response mutation proxies (ctx.res).
ctx.req)Record<string, string | string[]>).Record<string, string>).IncomingHttpHeaders)."/dashboard")."GET", "POST").ctx.waitUntil()).ctx.res)httpOnly, secure, sameSite, maxAge, expires, and path.200, 201, 404).302).Any custom data attached to context inside a plugin's onRequest(request, context) hook (such as context.user, context.session, or context.tenant) is preserved across the entire request lifecycle and can be read synchronously via getContext() in any Server Component or Server Function.
In addition to ctx.req.env, platform host resources (Cloudflare D1 databases, KV namespaces, R2 buckets) and execution context methods (such as ctx.waitUntil()) are accessible directly at the root of the context object.
HTTP headers, status codes, and HTTP-only cookies can only be sent before the server flushes the initial response stream. In streaming Server Components or deferred <Suspense> blocks, invoke return redirect("/path") to trigger client-side SPA navigation instead of modifying raw headers.
Never invoke getContext() directly inside a Client Component running in the browser. In browser environments, Dinou logs an error and returns an empty object. Always read confidential cookies or database records in Server Components or getProps, sanitize the data, and pass only safe props to Client Components.
Production Implementation Patterns
Explore real-world patterns for Server Components, Server Functions, route segment lifecycle hooks, and plugin state retrieval.
// src/app/profile/page.tsx (Server Component)
import { getContext, redirect } from "dinou";
import { db } from "@/lib/db";
export default async function ProfilePage() {
const ctx = getContext();
if (!ctx) return null;
// Read authenticated session cookie securely on the server
const sessionToken = ctx.req.cookies.auth_session;
if (!sessionToken) {
return redirect("/login?from=/profile");
}
// Extract client IP and user agent for security logging
const clientIp = ctx.req.headers["x-forwarded-for"] || "127.0.0.1";
const userAgent = ctx.req.headers["user-agent"] || "unknown";
const user = await db.users.findByToken(sessionToken);
return (
<div className="p-6">
<h1 className="text-2xl font-bold">Welcome, {user.name}</h1>
<p className="text-sm text-muted-foreground">Session active from IP: {clientIp}</p>
</div>
);
}// src/actions/preferences.ts
"use server";
import { getContext } from "dinou";
export async function updateThemePreference(theme: "light" | "dark" | "system") {
const ctx = getContext();
if (!ctx) throw new Error("Context unavailable");
// Mutate an outgoing HTTP response cookie from the server action
ctx.res.cookie("preferred_theme", theme, {
path: "/",
maxAge: 60 * 60 * 24 * 365, // 1 year
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
});
return { success: true, theme };
}// src/app/dashboard/page_functions.ts
import { getContext, redirect } from "dinou";
// Evaluated before any JSX component rendering begins
export async function getProps() {
const ctx = getContext();
if (!ctx) return { tenant: null };
const tenantSlug = ctx.req.query.tenant as string;
if (!tenantSlug) {
return redirect("/onboarding");
}
// Read environment variable or platform binding
const apiSecret = ctx.req.env?.TENANT_API_KEY || process.env.TENANT_API_KEY;
return {
tenantSlug,
hasApiKey: Boolean(apiSecret),
};
}// 1. Plugin attaches authenticated user to context (dinou.config.mjs)
export const authPlugin = {
name: "auth-guard",
async onRequest(request, context) {
const token = request.headers.get("Authorization");
if (token) {
// Injected directly into the request context store
context.user = await verifySession(token);
}
},
};
// 2. Server Component retrieves plugin state with zero prop-drilling
// src/app/dashboard/page.tsx (Server Component)
import { getContext } from "dinou";
export default async function DashboardPage() {
const ctx = getContext();
// Directly access custom plugin properties from the store
const currentUser = ctx?.user;
return (
<div>
<h1>Welcome back, {currentUser?.name || "Guest"}</h1>
<p>Role: {currentUser?.role || "anonymous"}</p>
</div>
);
}defineConfig()
Configuration HelperRoot ConfigurationType-safe configuration helper providing autocomplete, IntelliSense, and schema validation for your application's root configuration file (dinou.config.mjs, dinou.config.js, or dinou.config.ts).
Configuration Schema Reference
Options accepted by the defineConfig() wrapper.
| Option | Type | Default | Description |
|---|---|---|---|
| reactStrictMode | boolean | true | Enables React 19 StrictMode double-rendering in development mode. Helps detect unexpected side effects and ensures strict concurrent rendering compatibility with React Compiler. |
| ppr | boolean | false | Global default for Partial Prerendering. When enabled, static-eligible routes pre-render an instant static HTML shell while streaming dynamic <Suspense> holes. Can be configured per layout branch in layout_functions.ts or overridden per route in page_functions.ts. |
| storage | StorageAdapter | FileSystemStorage | Configures the storage backend for ISR HTML pages, RSC flight payloads, and cache slot records. Accepts built-in adapters (such as RedisStorage or MemoryStorage) or custom implementations. |
| plugins | DinouPlugin[] | [] | Array of Web Standards lifecycle middleware plugins executed on every incoming HTTP request before route matching or React rendering. |
Production Configuration Patterns
Complete production configuration files for ES Modules and TypeScript projects.
// dinou.config.mjs
import { defineConfig } from "dinou";
import { RedisStorage } from "dinou/server";
import Redis from "ioredis";
export default defineConfig({
// Guarantee concurrent compatibility with React 19 StrictMode in dev
reactStrictMode: true,
// Global Partial Prerendering default
ppr: false,
// Distributed multi-region ISR cache storage
storage: new RedisStorage(
new Redis(process.env.REDIS_URL, {
maxRetriesPerRequest: 3,
enableReadyCheck: true,
})
),
// Universal Web Standards request plugins
plugins: [
{
name: "security-headers",
async onRequest(request, context) {
// Enforce HTTPS in production environments
const proto = request.headers.get("x-forwarded-proto");
if (process.env.NODE_ENV === "production" && proto === "http") {
const httpsUrl = request.url.replace("http://", "https://");
return Response.redirect(httpsUrl, 301);
}
},
},
],
});// dinou.config.ts
import { defineConfig, type DinouPlugin } from "dinou";
const apiAuthPlugin: DinouPlugin = {
name: "api-guard",
async onRequest(request, context) {
const url = new URL(request.url);
// Fast-path edge authorization for protected API routes
if (url.pathname.startsWith("/api/protected")) {
const apiKey = request.headers.get("x-api-key");
if (!apiKey || apiKey !== process.env.INTERNAL_API_KEY) {
return Response.json(
{ error: "Unauthorized access: invalid or missing API key" },
{ status: 401 }
);
}
}
},
};
export default defineConfig({
reactStrictMode: true,
ppr: true,
plugins: [apiAuthPlugin],
});DinouPlugin
Web Standards MiddlewareEdge & Node.js CompatiblePlugins execute sequentially on the web server entrypoint for every incoming HTTP request before React route matching or component rendering begins. Built on standard Web Request and Response primitives, plugins provide unified middleware capabilities across Node.js, Bun, Cloudflare Workers, and Edge platforms.
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | Optional | Descriptive plugin identifier used for telemetry, developer diagnostics, and error reporting in logs. |
| onRequest | (request: Request, context: RequestContextStore) => Promise<Response | void> | Response | void | Optional | Universal Web Standards middleware hook executed sequentially on every incoming HTTP request before React rendering. Returning a standard Web Response aborts the request immediately (<1ms). Returning void continues execution and preserves any custom properties attached to context for downstream Server Components. |
Execution Lifecycle & Response Contract
Understanding early exit vs. pipeline pass-through in onRequest.
Response Returned)If onRequest returns an instance of Response (such as Response.redirect(), Response.json(), or a custom Response), Dinou terminates the request immediately (<1ms).
It completely bypasses React component rendering, routing matches, and server-side database lookups. This is optimal for authentication walls, health check endpoints, and bot blocking.
void)If onRequest returns nothing (or undefined), execution proceeds seamlessly to the React rendering pipeline.
Any custom properties attached to the context parameter (e.g. context.user = session) are preserved in AsyncLocalStorage and accessible synchronously in Server Components via getContext().
Production Plugin Patterns
Practical implementations for API security, geolocation enrichment, and server-timing telemetry.
// src/plugins/auth-guard.ts
import type { DinouPlugin } from "dinou";
export const authGuardPlugin: DinouPlugin = {
name: "auth-guard",
async onRequest(request, context) {
const url = new URL(request.url);
// Intercept secured API endpoints before React loads
if (url.pathname.startsWith("/api/admin")) {
const authHeader = request.headers.get("authorization");
if (!authHeader || !authHeader.startsWith("Bearer secret_admin_token")) {
// Fast-path early exit: terminates request in <1ms
return Response.json(
{ error: "Forbidden: invalid administrative token" },
{ status: 403, headers: { "Cache-Control": "no-store" } }
);
}
}
},
};// src/plugins/geo-enrichment.ts
import type { DinouPlugin } from "dinou";
export const geoPlugin: DinouPlugin = {
name: "geo-enrichment",
async onRequest(request, context) {
// Extract Edge CDN headers (Cloudflare, Vercel, AWS CloudFront)
const country = request.headers.get("cf-ipcountry") || "US";
const city = request.headers.get("cf-ipcity") || "San Francisco";
// Enrich the request context object with geo metadata
context.geo = {
country,
city,
isEu: ["DE", "FR", "ES", "IT", "NL"].includes(country),
};
// Return nothing (void) -> execution proceeds to React rendering
// Downstream Server Components can read context.geo via getContext()!
},
};// src/plugins/server-timing.ts
import type { DinouPlugin } from "dinou";
export const telemetryPlugin: DinouPlugin = {
name: "server-timing",
async onRequest(request, context) {
const start = performance.now();
// Fast-path health check endpoint
if (new URL(request.url).pathname === "/healthz") {
const duration = (performance.now() - start).toFixed(2);
return Response.json({
status: "healthy",
uptime: process.uptime ? process.uptime() : 0,
latencyMs: Number(duration),
});
}
// Attach request start timestamp for downstream telemetry
context.startTime = start;
},
};5. Server Cache & Revalidation (dinou/server)Micro-ISR & Invalidation
revalidatePage()
Server FunctionTarget: Page Component OnlyAsynchronously invalidates and regenerates only the targeted page component (its page.rsc payload and static HTML), while leaving parent and ancestor layouts (layout.rsc) completely untouched.
Page-Only Invalidation Scope
Why targeted page revalidation saves server compute and bandwidth.
When updating an individual blog post or product price, re-rendering global headers, user navigation menus, and shared footer layouts is unnecessary overhead.
revalidatePage(path) recalculates strictly the page JSX artifact, maximizing cache reuse for all wrapping layout hierarchies.
Supports absolute routes (e.g. "/products/laptop-pro"), parameterized URLs, or relative paths (e.g. "./" relative to the active request).
Trailing slashes are automatically normalized to guarantee consistent cache purging on both Node.js and Edge storage backends.
| Parameter | Type | Default | Description |
|---|---|---|---|
| path | string | Required | The target route path to revalidate (e.g. "/blog/post-1", "/products/123", or "./" for the active URL). Trailing slashes are automatically normalized. |
// src/actions/update-article.ts
"use server";
import { revalidatePage } from "dinou/server";
import { db } from "@/lib/db";
export async function updateArticle(slug: string, content: string) {
// 1. Update database record
await db.articles.update({ where: { slug }, data: { content } });
// 2. Purge only the article's page.rsc and index.html
// Leaves the parent /blog layout and global navigation cache untouched
await revalidatePage(`/blog/${slug}`);
return { success: true };
}// src/plugins/cms-webhook.ts
import type { DinouPlugin } from "dinou";
import { revalidatePage } from "dinou/server";
export const cmsWebhookPlugin: DinouPlugin = {
name: "cms-webhook",
async onRequest(request) {
const url = new URL(request.url);
if (url.pathname === "/api/webhooks/cms" && request.method === "POST") {
const payload = await request.json();
const slug = payload?.entry?.slug;
if (slug) {
// Invalidate strictly the updated page without rebuilding shared layouts
await revalidatePage(`/posts/${slug}`);
return Response.json({ revalidated: true, path: `/posts/${slug}` });
}
return Response.json({ error: "Missing slug" }, { status: 400 });
}
},
};revalidatePath()
Server FunctionTarget: Route & LayoutAsynchronously invalidates and regenerates the targeted page and its segment layout (layout.rsc). When invoked with { cascade: true } on a route that defines a layout, it recursively invalidates all child subroutes beneath that route segment.
Parameters & Cascade Hierarchy
Options accepted by revalidatePath(path, options?).
| Parameter | Type | Default | Description |
|---|---|---|---|
| path | string | Required | The target route path to revalidate (e.g. "/docs" or "/products"). |
| options.cascade | boolean | false | When true and the targeted route defines a layout (layout.tsx), Dinou revalidates that layout and automatically cascades down to all child subroutes inheriting it (e.g. revalidating /docs with cascade also purges /docs/routing, /docs/rendering, etc.). If no layout exists at that segment, it revalidates only the page and skips cascading. |
Cascade Invalidation Requires a Segment Layout
cascade: true option only activates if the specified route segment defines an active layout.tsx. Cascading in Dinou is fundamentally tied to invalidating a shared ancestor layout payload. If a targeted path does not contain a layout, Dinou revalidates only that single page and skips traversing child subroutes.// src/actions/store-actions.ts
"use server";
import { revalidatePath } from "dinou/server";
import { db } from "@/lib/db";
export async function updateStoreBanner(bannerText: string) {
// 1. Update promotional banner in database
await db.settings.set("store_banner", bannerText);
// 2. Revalidate /store page AND /store layout (layout.rsc)
// Essential because the promotional banner lives in src/app/store/layout.tsx
await revalidatePath("/store");
return { success: true };
}// src/actions/docs-actions.ts
"use server";
import { revalidatePath } from "dinou/server";
import { cms } from "@/lib/cms";
export async function publishDocSection(sectionId: string) {
// 1. Publish new section in CMS
await cms.navigation.publish(sectionId);
// 2. Invalidate /docs layout AND cascade down to all child routes
// Purges /docs/routing, /docs/rendering, /docs/api-reference, etc.
// Guarantees all nested pages display the updated sidebar with zero hydration mismatch
await revalidatePath("/docs", { cascade: true });
return { success: true };
}revalidateTag()
Server FunctionMulti-Target Global RevalidationReturns: Promise<void>Purges and marks for on-demand background regeneration all route pages, segment layouts, and independent <DinouCacheSlot> fragments matching the specified cache tag across the application.
Parameters & Tagged Cascade
Options accepted by revalidateTag(tag, options?).
| Parameter | Type | Default | Description |
|---|---|---|---|
| tag | string | Required | The cache tag identifier to invalidate (e.g. "products", "site-nav", or dynamic entity tags like "product-42"). Matches against tags declared on pages, layouts, and slot fragments. |
| options.cascade | boolean | false | When true, if any matched artifact is a route layout (layout.tsx), Dinou initiates a cascading revalidation across all downstream child subroutes inheriting that layout. If false, matched layouts are revalidated individually without traversing child routes. |
Unified Cross-Tier Invalidation
revalidateTag() traverses storage artifacts regardless of where they reside in the route tree:- Micro-ISR Slots: Direct cache slot records matching the tag are immediately purged from storage so the next incoming request resolves fresh server JSX.
- Route Pages: HTML and RSC page payloads declaring the tag in
page_functions = { tags: [...] }(including parameterized routes such as/products/[id]) are marked stale and regenerated. - Layouts: Layouts declaring the tag in
layout_functions = { tags: [...] }are updated, with optional downward cascade whencascade: trueis passed.
// src/app/catalog/page.tsx
import { DinouCacheSlot } from "dinou/server";
import { ProductGrid } from "@/components/product-grid";
import { RealtimeStock } from "@/components/realtime-stock";
// 1. Tag the entire route page
export const page_functions = {
tags: ["catalog", "products"],
revalidate: 3600, // Background fallback
};
export default async function CatalogPage() {
return (
<main className="container mx-auto py-8">
<h1>Product Catalog</h1>
{/* Static product listings */}
<ProductGrid />
{/* 2. Micro-ISR cache slot with granular tags */}
<DinouCacheSlot
id="inventory-feed"
tags={["inventory", "products"]}
revalidate={60}
>
<RealtimeStock />
</DinouCacheSlot>
</main>
);
}// src/actions/inventory-actions.ts
"use server";
import { revalidateTag } from "dinou/server";
import { db } from "@/lib/db";
export async function restockItem(productId: string, quantity: number) {
// 1. Mutate database records
await db.stock.increment(productId, quantity);
// 2. Targeted Invalidation:
// Purges only <DinouCacheSlot> components tagged with "inventory",
// keeping the heavy parent catalog page cached and untouched.
await revalidateTag("inventory");
return { success: true };
}
export async function bulkPriceUpdate(categoryId: string) {
await db.pricing.applyDiscount(categoryId, 0.15);
// Broad Invalidation:
// Purges all catalog pages, search feeds, and inventory slots tagged with "products".
await revalidateTag("products");
return { success: true };
}| Function | Target Scope | Layout Affected? | Cascade to Children? | Best Used For |
|---|---|---|---|---|
| revalidatePage(path) | Single route page artifact (index.html, page.rsc) | No | No | Specific page mutations, content edits, single-record updates where shared layouts remain untouched. |
| revalidatePath(path, options?) | Page component and its segment layout (layout.rsc) | Yes | Optional ({ cascade: true }, requires layout) | Segment-level updates, navigation breadcrumbs, sidebar menus, and branch-wide refreshes. |
| revalidateTag(tag, options?) | All pages, layouts, and <DinouCacheSlot> fragments tagged with matching key | If tagged | Optional ({ cascade: true } for matched layouts) | Cross-cutting domain entity mutations (e.g. inventory stock, product catalog, global banners) affecting multiple disjoint routes and slots simultaneously. |
<DinouCacheSlot>
Server PrimitiveReturns: Promise<ReactNode>The foundational primitive for Micro-ISR (Vertical 2D Segmentation) in React Server Components. Allows isolating, caching, and revalidating independent component subtrees within an active page without re-evaluating the parent page, re-fetching unchanged database queries, or resetting client component state.
Props & Micro-ISR Reference
Configuring TTL revalidation, cache tags, and targeted slot identifiers.
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | Optional | Unique identifier for targeted client-side slot refreshing via refreshSlot(id) or router.refreshSlot(id). If omitted, Dinou automatically falls back to using the first provided tag as the slot identifier. |
| revalidate | number | false (Forever) | Time-based TTL in seconds. Automatically refreshes and persists the slot subtree in the background once the timer expires. |
| tag / tags | string | string[] | undefined | Cache tag or array of tags for server-side on-demand invalidation via revalidateTag(tag). |
| children | ReactNode | (() => Promise<ReactNode> | ReactNode) | Required | The Server Component subtree to isolate, render, and cache independently. Accepts standard JSX elements or a deferred asynchronous render function (() => Promise<ReactNode>). |
Dinou executes the slot's child components once, caches their rendered output in your storage backend (Redis, Memory, or FileSystem), and serves that cached payload to subsequent visitors. Database queries and server logic inside the slot never re-run until its TTL expires or an invalidation is triggered.
When a slot refreshes (either via time-based micro-ISR or refreshSlot), only that specific subtree updates in the browser DOM. Outer forms, input focus, scroll position, and client React state remain completely undisturbed.
Production Slot Patterns
Explore real-world patterns for independent TTLs, tag invalidations, and financial tickers.
// src/components/crypto-ticker-slot.tsx (Server Component)
import { DinouCacheSlot } from "dinou/server";
import { fetchCryptoPrices } from "@/lib/crypto";
export function CryptoTickerSection() {
return (
<div className="border rounded-lg p-4 bg-card">
<h2 className="text-sm font-semibold mb-2">Live Market Watch</h2>
{/* Caches ticker independently for 15 seconds */}
<DinouCacheSlot id="crypto-ticker" revalidate={15} tag="crypto">
<LiveCryptoPrices />
</DinouCacheSlot>
</div>
);
}
async function LiveCryptoPrices() {
const prices = await fetchCryptoPrices();
return (
<div className="flex gap-4 font-mono text-xs">
<span>BTC: ${prices.btc.toLocaleString()}</span>
<span>ETH: ${prices.eth.toLocaleString()}</span>
<span>SOL: ${prices.sol.toLocaleString()}</span>
</div>
);
}// src/components/cart-badge-slot.tsx (Server Component)
import { DinouCacheSlot } from "dinou/server";
import { getContext } from "dinou";
import db from "@/lib/db";
export function CartBadgeSlot() {
const ctx = getContext();
const userId = ctx?.req.cookies.user_id || "guest";
return (
// Cached until a mutation calls revalidateTag(`cart-${userId}`)
<DinouCacheSlot
id="header-cart"
tag={[`cart-${userId}`, "cart"]}
>
<CartCounter userId={userId} />
</DinouCacheSlot>
);
}
async function CartCounter({ userId }: { userId: string }) {
const count = await db.cart.getItemCount(userId);
return <span className="badge font-bold">{count} items</span>;
}// src/app/dashboard/page.tsx (Server Component)
import { DinouCacheSlot } from "dinou/server";
import { UserMetrics } from "@/components/metrics";
import { GlobalAnnouncements } from "@/components/announcements";
import { ActivityFeed } from "@/components/activity";
export default function DashboardPage() {
return (
<div className="space-y-6">
<header>
<h1 className="text-2xl font-bold">Workspace Overview</h1>
</header>
{/* Slot A: Global announcements cached for 1 hour */}
<DinouCacheSlot id="announcements" revalidate={3600} tag="announcements">
<GlobalAnnouncements />
</DinouCacheSlot>
<div className="grid grid-cols-3 gap-6">
{/* Main Content: High-velocity user metrics cached for 30s */}
<div className="col-span-2">
<DinouCacheSlot id="metrics" revalidate={30} tag="metrics">
<UserMetrics />
</DinouCacheSlot>
</div>
{/* Sidebar: Activity feed revalidated on-demand */}
<div>
<DinouCacheSlot id="activity" tags={["activity-feed"]}>
<ActivityFeed />
</DinouCacheSlot>
</div>
</div>
</div>
);
}6. Storage Adapters (dinou/server)Storage Abstraction & Persistence
Dinou completely decouples ISR and Micro-ISR caching logic from physical storage media. All pre-rendered HTML, RSC route payloads, segment layouts, and slot fragments are read and written through a unified StorageAdapter interface.
Switch between local disk, distributed memory caches, edge KV stores, or custom object storage without modifying application components or data loaders.
Every cached entry preserves associated metadata, including generation timestamps (generatedAt), assigned cache tags, and ISR revalidation windows.
In multi-instance setups (Kubernetes, Cloud Run, Edge workers), adapters like RedisStorage or CloudflareKVStorage share generated artifacts globally across all nodes.
Built-In Adapters Reference
Production storage engines available directly from dinou/server.
| Adapter Class | Constructor Signature | Metadata Strategy | Target Environment |
|---|---|---|---|
| FileSystemStorage | new FileSystemStorage(baseDir?: string) | Writes companion metadata.json, layout.metadata.json, and slot.metadata.json files alongside cached payloads. | Local development, single-node Docker containers, VPS servers (default: .dinou/dist2). |
| RedisStorage | new RedisStorage(redisClient: any, prefix?: string) | Serializes content and metadata together as JSON strings under configurable key prefix (default: "dinou:cache:"). | Multi-container Kubernetes clusters, AWS ECS, Google Cloud Run, distributed Node.js servers. |
| CloudflareKVStorage | new CloudflareKVStorage(kvNamespace: any) | Leverages Cloudflare KV native metadata attributes via getWithMetadata() and put(..., { metadata }). | Cloudflare Workers & Pages at global edge scale. |
| DenoKVStorage | new DenoKVStorage(kvInstance?: any) | Stores atomic key-value records with automatic 16KB payload chunking for large HTML/RSC artifacts. | Deno Deploy & distributed edge runtimes. |
| MemoryStorage | new MemoryStorage() | Maintains an in-memory JavaScript Map storing { content, metadata } tuples in memory. | Unit testing, ephemeral CI/CD runners, preview sandboxes. |
Dinou exports getStorageAdapter() to access the active storage instance anywhere on the server, and setStorageAdapter(adapter) to dynamically swap the storage engine during initialization or test suites.
StorageAdapter Contract & Methods
Abstract class methods required when implementing or interacting with storage engines.
| Method | Signature | Required? | Description |
|---|---|---|---|
| get(key) | (key: string) => Promise<{ content: string; metadata?: any } | null> | Yes | Retrieves the cached content string and companion metadata. Returns null on cache miss. |
| set(key, content, metadata?) | (key: string, content: string, metadata?: any) => Promise<void> | Yes | Persists the rendered HTML, RSC stream, or slot JSON alongside its companion metadata object. |
| has(key) | (key: string) => Promise<boolean> | Yes | Fast existence check to verify if a valid cache artifact exists without deserializing full content. |
| delete(key) | (key: string) => Promise<void> | Yes | Removes the targeted cached artifact and associated metadata files or keys. |
| keys(prefix?) | (prefix?: string) => Promise<string[]> | string[] | Optional | Enumerates stored artifact keys. Required for revalidateTag() scanning and revalidatePath({ cascade: true }) child discovery in Edge runtime environments. |
Production Configuration & Custom Implementation
Configure distributed caching with built-in adapters like RedisStorage or extend StorageAdapter to implement a custom engine.
// dinou.config.mjs
import { defineConfig } from "dinou/config";
import { RedisStorage } from "dinou/server";
import Redis from "ioredis";
// Connect to shared Redis cache (AWS ElastiCache, Upstash, or Redis cluster)
const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379");
export default defineConfig({
// Share ISR pages and <DinouCacheSlot> fragments across all Docker / K8s replicas
storage: new RedisStorage(redis, "dinou:cache:"),
});// src/lib/storage/sqlite-storage.ts
import { StorageAdapter } from "dinou/server";
import Database from "better-sqlite3";
export class SQLiteStorage extends StorageAdapter {
private db: Database.Database;
constructor(dbPath: string = ".dinou/cache.db") {
super();
this.db = new Database(dbPath);
this.db.exec(`
CREATE TABLE IF NOT EXISTS dinou_cache (
key TEXT PRIMARY KEY,
content TEXT NOT NULL,
metadata TEXT
);
`);
}
async get(key: string) {
const row = this.db
.prepare("SELECT content, metadata FROM dinou_cache WHERE key = ?")
.get(key) as { content: string; metadata: string | null } | undefined;
if (!row) return null;
return {
content: row.content,
metadata: row.metadata ? JSON.parse(row.metadata) : null,
};
}
async set(key: string, content: string, metadata: any = null): Promise<void> {
const metaStr = metadata ? JSON.stringify(metadata) : null;
this.db
.prepare(
"INSERT INTO dinou_cache (key, content, metadata) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET content = excluded.content, metadata = excluded.metadata"
)
.run(key, content, metaStr);
}
async has(key: string): Promise<boolean> {
const row = this.db
.prepare("SELECT 1 FROM dinou_cache WHERE key = ?")
.get(key);
return Boolean(row);
}
async delete(key: string): Promise<void> {
this.db.prepare("DELETE FROM dinou_cache WHERE key = ?").run(key);
}
async keys(prefix: string = ""): Promise<string[]> {
const rows = this.db
.prepare("SELECT key FROM dinou_cache WHERE key LIKE ?")
.all(`${prefix}%`) as { key: string }[];
return rows.map((r) => r.key);
}
}// dinou.config.mjs
import { defineConfig } from "dinou/config";
import { SQLiteStorage } from "./src/lib/storage/sqlite-storage.js";
export default defineConfig({
// Register custom storage engine declaratively
storage: new SQLiteStorage(".dinou/production-cache.db"),
});// server-bootstrap.ts
import { setStorageAdapter, getStorageAdapter } from "dinou/server";
import { SQLiteStorage } from "./src/lib/storage/sqlite-storage";
// Dynamically override or configure storage during server boot
const customStorage = new SQLiteStorage(".dinou/production-cache.db");
setStorageAdapter(customStorage);
// Inspect active storage adapter
const activeStorage = getStorageAdapter();
console.log("Dinou active storage adapter:", activeStorage.constructor.name);What is the Redis Key Prefix?
The prefix parameter in new RedisStorage(redis, prefix) defines the namespace added to the front of every key stored in Redis:
- Concrete Example: When Dinou caches the page
/catalog, the actual key stored in your Redis database isdinou:cache:catalog/index.html. - Avoid Collisions: If your Redis server is shared with user sessions, BullMQ queues, or database caches, this prefix guarantees that Dinou will never overwrite other application data.
- It is Optional: It defaults to
"dinou:cache:", so writingnew RedisStorage(redis)is usually sufficient. You only need to change it if you share Redis across environments (e.g."dinou:prod:"vs"dinou:staging:").
7. Route Lifecycles & Configpage_functions.ts & layout_functions.ts
Dinou strictly decouples UI components from server-side lifecycle contracts. Route lifecycles are declared in parallel page_functions.ts (or layout_functions.ts) files.
getProps()
Route Lifecycle HookServer Data LoaderReturns: Promise<TProps> | TPropsThe primary server-side data loading contract for routes and layouts. Executes exclusively on the server during SSG build, on-demand ISG generation, or runtime request handling. The object returned by getProps is automatically injected as props into the companion Page or Layout component.
Parameters
Arguments supplied to getProps(params).
| Parameter | Type | Default | Description |
|---|---|---|---|
| params | Record<string, string | string[]> | {} | Parsed dynamic URL parameters defined by route folder naming (e.g. { id: "42" } for [id] or { slug: ["a", "b"] } for catch-all [...slug]). In layouts, receives all segment parameters up to that layout level. Empty on static routes. |
Accessing Request Headers, Cookies & Query
getProps receives route parameters as its first argument, you can access incoming HTTP request headers, cookies, query parameters, or platform environment variables by calling getContext() directly inside the function.// src/app/products/[id]/page_functions.ts
import type { PageFunctions } from "dinou";
import { getContext } from "dinou";
import { db } from "@/lib/db";
export interface ProductPageProps {
product: {
id: string;
title: string;
price: number;
description: string;
};
preferredCurrency: string;
}
// Pass the route pattern directly: PageFunctions automatically infers params
export const getProps: PageFunctions<
"/products/[id]",
ProductPageProps
>["getProps"] = async (params) => {
// 1. Fetch data on the server using route parameters (params.id is typed as string)
const product = await db.products.findById(params.id);
// 2. Read cookies or headers from request context
const ctx = getContext();
const preferredCurrency = ctx.req.cookies["user_currency"] || "USD";
// 3. Return props passed directly to the companion Page component
return {
product,
preferredCurrency,
};
};// src/app/products/[id]/page.tsx
import type { PageProps } from "dinou";
import type { ProductPageProps } from "./page_functions";
// In Dinou, Page components receive route params plus props returned by getProps()
type Props = PageProps<"/products/[id]"> & ProductPageProps;
export default function ProductPage({
params,
product,
preferredCurrency,
}: Props) {
return (
<main className="container mx-auto py-8">
<h1 className="text-3xl font-bold">{product.title}</h1>
<p className="text-2xl font-semibold mt-2 text-primary">
{preferredCurrency} {product.price.toFixed(2)}
</p>
<p className="mt-4 text-muted-foreground leading-relaxed">
{product.description}
</p>
</main>
);
}// src/app/[organization]/layout_functions.ts
import type { LayoutFunctions } from "dinou";
import { getContext, redirect } from "dinou";
import { db } from "@/lib/db";
export interface OrgLayoutProps {
organization: {
id: string;
name: string;
tier: string;
};
unreadCount: number;
}
// Layouts receive route parameters defined at or above this segment level
export const getProps: LayoutFunctions<
"/[organization]",
OrgLayoutProps
>["getProps"] = async (params) => {
const org = await db.organizations.findBySlug(params.organization);
if (!org) {
redirect("/select-organization");
}
const ctx = getContext();
const unreadCount = await db.notifications.countUnread(org.id, ctx.req.cookies["user_id"]);
// Props are injected directly into src/app/[organization]/layout.tsx
return {
organization: org,
unreadCount,
};
};// src/app/[organization]/layout.tsx
import type { LayoutProps } from "dinou";
import type { OrgLayoutProps } from "./layout_functions";
// Layout components receive children, segment params, and props returned by getProps()
type Props = LayoutProps<"/[organization]"> & OrgLayoutProps;
export default function OrgLayout({
children,
params,
organization,
unreadCount,
}: Props) {
return (
<div className="min-h-screen flex flex-col">
<header className="border-b px-6 py-4 flex items-center justify-between">
<span className="font-bold">{organization.name}</span>
<span className="text-xs bg-muted px-2 py-1 rounded">
{unreadCount} unread
</span>
</header>
<main className="flex-1 p-6">{children}</main>
</div>
);
}revalidate
Rendering Strategynumber | false | FunctionDefault: false (Static SSG)The single source of truth for route caching and rendering modes. In page_functions.ts, it defines the rendering strategy of that specific page route. In layout_functions.ts, it establishes a baseline caching policy inherited and reconciled across all descendant routes rendered within that layout branch.
| Value | Rendering Strategy | Lifecycle & Caching Behavior |
|---|---|---|
0 (or < 1) | Dynamic SSR | Bypasses the server static storage cache entirely. Dinou renders the component tree fresh on every incoming HTTP request and sends Cache-Control: no-store, no-cache, must-revalidate headers to downstream proxies and browsers. |
>= 1 (e.g. 60) | ISR (Incremental Static Regeneration) | Cached in the configured StorageAdapter for the specified duration in seconds. While valid, requests receive instantaneous cache hits. The first visitor request after TTL expiration receives the stale cache item while Dinou triggers background revalidation without blocking the response. |
false (or omitted) | Static (SSG) | Pre-rendered during build time (or on first demand via ISG) and persisted indefinitely in storage. The static document is served forever until purged explicitly via revalidatePath(), revalidatePage(), or revalidateTag(). |
Function (sync or async) | Dynamic Strategy Evaluation | An executable function returning number | false. Dinou executes this function on the server at runtime, enabling conditional rendering strategies based on environment variables, CMS flags, or runtime configuration. |
Hierarchical Segment Reconciliation (Math.min)
layout_functions.ts and a nested page_functions.ts) export revalidate, Dinou reconciles the values using Math.min(). The segment specifying the shortest revalidation interval takes precedence. If any layout or page in the active branch sets revalidate: 0, the entire route becomes Dynamic SSR.// src/app/products/page_functions.ts
import type { PageFunctions } from "dinou";
// Time-based ISR: Cache rendered HTML for 60 seconds
// Next request after 60s triggers asynchronous background revalidation
export const revalidate: PageFunctions["revalidate"] = 60;// src/app/cart/page_functions.ts
import type { PageFunctions } from "dinou";
// Dynamic SSR: Bypasses static cache completely
// Freshly renders on every request with Cache-Control: no-store headers
export const revalidate: PageFunctions["revalidate"] = 0;// src/app/pricing/page_functions.ts
import type { PageFunctions } from "dinou";
// Dynamic evaluation: Switch caching strategy based on runtime environment
export const revalidate: PageFunctions["revalidate"] = async () => {
// Disable caching during local development or staging audits
if (process.env.NODE_ENV !== "production") {
return 0; // Dynamic SSR
}
// Cache for 10 minutes in production
return 600; // ISR 10m
};// src/app/dashboard/layout_functions.ts
import type { LayoutFunctions } from "dinou";
// Sets a baseline 5-minute cache for all routes within /dashboard
// Child pages can shorten this interval (e.g. 60s) or force SSR (0) via Math.min()
export const revalidate: LayoutFunctions["revalidate"] = 300;ppr
Partial PrerenderingPPRConfigDefault: false (Inherited)Configures Partial Prerendering (PPR) for a route segment, layout subtree, or global application. PPR combines the instantaneous response time of static delivery with the flexibility of server-rendered dynamic data. Dinou immediately transmits a pre-rendered static HTML shell and streams dynamic <Suspense> holes concurrently over the same HTTP response stream as they resolve.
| Scope | Declaration Location | Behavior & Precedence |
|---|---|---|
| 1. Global Config | dinou.config.mjs | Sets application-wide default via defineConfig({ ppr: true }). Serves as the fallback baseline for all routes when no segment defines an override. |
| 2. Layout Subtree | layout_functions.ts | Cascades downward to all nested layouts and child pages in that directory subtree. Enables activating PPR across entire feature sections such as /dashboard/*. |
| 3. Page Override | page_functions.ts | Provides granular control. Explicitly opts into PPR (true) or opts out (false), taking final precedence over parent layouts and global settings. |
Critical Architectural Boundary (getProps vs React Suspense)
getProps() executes on the server before React rendering commences, operating entirely outside of React <Suspense> boundaries. Accessing dynamic request parameters, headers, or cookies directly inside getProps() prevents the generation of a static shell, causing Dinou to bail out into Dynamic SSR. To maximize PPR performance, return only deterministic or static shell data from getProps(), and isolate dynamic personalized data inside asynchronous Server Components wrapped with <Suspense fallback={<Skeleton />}>.// src/app/dashboard/page_functions.ts
import type { PageFunctions } from "dinou";
// 1. Literal boolean activation
export const ppr: PageFunctions["ppr"] = true;
// 2. Programmatic evaluation (sync or async function)
// export const ppr: PageFunctions["ppr"] = async () => {
// return process.env.ENABLE_PPR === "true";
// };// src/app/dashboard/page.tsx
import { Suspense } from "react";
import { UserGreeting, UserStatsSkeleton } from "@/components/skeletons";
import { RealtimeMetrics } from "@/components/realtime-metrics";
export default function DashboardPage() {
return (
<div className="p-6 space-y-6">
{/* 1. Static Shell: Rendered at build time and served in 0ms */}
<header className="border-b pb-4">
<h1 className="text-2xl font-bold">Analytics Dashboard</h1>
<p className="text-muted-foreground text-sm">
Overview of workspace activities and live metrics.
</p>
</header>
{/* 2. Dynamic Streaming Hole: Fallback sent in shell, resolved asynchronously */}
<Suspense fallback={<UserStatsSkeleton />}>
<RealtimeMetrics />
</Suspense>
</div>
);
}// src/app/admin/layout_functions.ts
import type { LayoutFunctions } from "dinou";
// Branch-level enablement
// Automatically enables PPR for all nested pages under /admin/*
export const ppr: LayoutFunctions["ppr"] = true;// src/app/admin/audit/page_functions.ts
import type { PageFunctions } from "dinou";
// Explicit opt-out
// Overrides the parent /admin layout and disables PPR for this specific route
export const ppr: PageFunctions["ppr"] = false;
// Force fresh Dynamic SSR rendering on every request
export const revalidate: PageFunctions["revalidate"] = 0;getStaticPaths
Static Generation (SSG)StaticPathsResultpage_functions.ts & layout_functions.tsDefines the parameter list for dynamic routes (e.g. [slug], [[category]], [category]/[id], or [...catchall]) to be pre-rendered during build time (SSG). Dinou iterates over the returned paths, executes the component rendering pipeline, and saves the generated HTML and RSC payloads into storage for instant edge delivery.
| Route Segment Type | Valid Return Formats | Example Value |
|---|---|---|
Single Dynamic Segment ([slug] or [id]) | string[], number[], or Record<string, string>[] | ["v7-release", "migration"] or [{ slug: "v7-release" }] |
Optional Single Segment ([[category]]) | (string | number | undefined)[] or Record<string, string | number | undefined>[] | [undefined, "electronics"] (undefined pre-renders the root path) |
Multi-Segment Hierarchy ([category]/[id]) | Record<string, string | number>[] | [{ category: "tech", id: "42" }] |
Catch-all Dynamic Route ([...slug]) | string[][] or { slug: string[] }[] | [["docs", "getting-started"]] |
Optional Catch-all ([[...slug]]) | (string[] | undefined)[] or { slug: string[] }[] | [[], ["features"]] (empty array represents root index) |
Perimeter Coordination with allowISG
getStaticPaths works in direct coordination with allowISG. When allowISG: true (the default), paths defined in getStaticPaths are pre-generated during build time, while unlisted dynamic paths requested by visitors are generated on-demand via Incremental Static Generation (ISG). When allowISG: false, any request for a path not pre-rendered by getStaticPaths immediately triggers a 404 response without executing server rendering.No-Gap Rule and Runtime Parameter Guarantees
[[format]] or [[...tags]]) can be set to undefined only if all subsequent segments along the URL path are also undefined. Specifying an undefined segment followed by a defined segment (e.g. [undefined, "something"]) creates an illegal URL gap and causes Dinou to skip the invalid route during build generation.[[category]]). When visiting the root path, params.category is strictly undefined rather than an empty string or null. This allows JavaScript ES6 default destructuring to apply cleanly in page.tsx.[[...slug]]). params.slug is guaranteed to always be an array (string[]). For the root path, Dinou supplies an empty array [] instead of undefined, allowing safe execution of array operations such as params.slug.join("/") without defensive guards. In getStaticPaths, pre-render the root route by passing [], never [undefined].Pass-Through Segments. When intermediate dynamic folders lack their own page.tsx, the terminal leaf page_functions.ts supplies parameter objects mapping all dynamic keys at once.
Chain of Responsibility. When both parent and child folders contain a page.tsx, the parent acts as a gatekeeper. If the parent returns an empty array [], traversal stops and child pages are not built. When both return paths, Dinou automatically pre-renders their Cartesian product.
Downward Route Propagation. Parameter combinations generated by getStaticPaths automatically cascade to nested static subpages (such as /blog/[slug]/details) without requiring duplicated exports.
Terminal Catch-all Constraint. Because catch-all segments ([...slug] and [[...slug]]) greedily consume all remaining URL path tokens, they must always be declared as the final terminal segment of a directory path.
Architectural Roles of getStaticPaths in layout_functions.ts
getStaticPaths in layout_functions.ts is essential whenever a layout resides in a dynamic directory segment (such as /[lang]/layout.tsx, /docs/[version]/layout.tsx, or /[category]/layout.tsx). It fulfills three critical engine functions.getStaticPaths, Dinou automatically injects those parameter combinations into all nested child pages (such as /[lang]/page.tsx, /[lang]/about/page.tsx, and /[lang]/contact/page.tsx). Child pages do not need to duplicate getStaticPaths exports to inherit layout parameters.getStaticPaths in layout_functions.ts instructs the build pipeline to pre-render every variant (such as es/layout.rsc, en/layout.rsc, and fr/layout.rsc) into the storage adapter. During client-side navigation, the browser fetches the pre-rendered layout stream instantly without invoking server execution.allowISG is set to false in a layout, Dinou compiles the layout's getStaticPaths into a strict perimeter set. Incoming requests containing unknown layout parameter values (such as an uncompiled language /de/pricing) are blocked immediately with a 404 response at the layout perimeter before child pages or database operations are executed.The Golden Rule: Where to Define getStaticPaths
getStaticPaths at the exact directory level where the dynamic bracket segment ([param]) is introduced in the filesystem./blog/[slug]/page.tsx where /blog/layout.tsx is static), declare getStaticPaths in page_functions.ts. The layout remains shared across all articles, and the parameter maps directly to individual posts./[lang]/layout.tsx with /about and /contact), declare getStaticPaths in layout_functions.ts. Dinou automatically cascades the defined languages to all nested routes without duplicating exports./[category]/layout.tsx and /[category]/[product]/page.tsx), declare getStaticPaths in both locations. The layout declares valid categories and pre-renders their shells, while the page defines specific product items within those categories.| Route Structure | Declaration Location | Returned Parameter Scope |
|---|---|---|
| /blog/[slug] | [slug]/page_functions.ts | { slug: string } |
| /[lang]/about | [lang]/layout_functions.ts | { lang: string } (cascades to /about) |
| /[category]/[item] | Both layout_functions and page_functions | Layout { category } and Page { category, item } |
PageFunctions<Route> and LayoutFunctions<Route>) enforce parameter shapes automatically and raise compile-time type errors if an unexpected parameter is declared at the wrong hierarchy level.// src/app/blog/[slug]/page_functions.ts
import type { PageFunctions } from "dinou";
import { cmsClient } from "@/lib/cms";
// Asynchronous loader fetching dynamic slugs from an external CMS
export const getStaticPaths: PageFunctions<"/blog/[slug]">["getStaticPaths"] = async () => {
const articles = await cmsClient.getAllArticleSlugs();
// Return an array of strings or parameter objects
return articles.map((article) => ({
slug: article.slug,
}));
};// src/app/products/[[category]]/page_functions.ts
import type { PageFunctions } from "dinou";
// Optional single segment: pre-renders root /products and category variations
export const getStaticPaths: PageFunctions<"/products/[[category]]">["getStaticPaths"] = [
undefined, // Pre-renders /products (category is undefined)
"electronics", // Pre-renders /products/electronics
"fashion", // Pre-renders /products/fashion
];// src/app/shop/[category]/[id]/page_functions.ts
import type { PageFunctions } from "dinou";
// Parameter objects mapping multiple dynamic segments
export const getStaticPaths: PageFunctions<"/shop/[category]/[id]">["getStaticPaths"] = [
{ category: "electronics", id: "pro-laptop" },
{ category: "electronics", id: "wireless-mouse" },
{ category: "apparel", id: "hoodie" },
];// src/app/docs/[...slug]/page_functions.ts
import type { PageFunctions } from "dinou";
import { getAllDocPaths } from "@/lib/content";
// Catch-all segment returning nested string arrays
export const getStaticPaths: PageFunctions<"/docs/[...slug]">["getStaticPaths"] = async () => {
const docRoutes = await getAllDocPaths();
// Example: [["installation"], ["routing", "pages"], ["api", "reference"]]
return docRoutes.map((route) => route.split("/"));
};// src/app/org/[tenant]/layout_functions.ts
import type { LayoutFunctions } from "dinou";
import { db } from "@/lib/database";
// Pre-render static paths at the layout level for multi-tenant segments
export const getStaticPaths: LayoutFunctions<"/org/[tenant]">["getStaticPaths"] = async () => {
const tenants = await db.tenants.findMany({ select: { slug: true } });
return tenants.map((t) => ({ tenant: t.slug }));
};allowISG
Static Generation Controlboolean | (() => boolean | Promise<boolean>)Default: truelayout_functions.ts & page_functions.tsControls whether dynamic route segments not pre-rendered during build time by getStaticPaths can be generated on-demand at runtime via Incremental Static Generation (ISG).
allowISG: false restricts accessible dynamic URLs strictly to the finite collection declared in getStaticPaths. Any request for a path outside the pre-rendered set immediately returns 404 Not Found without invoking server renderers.Perimeter Coordination with getStaticPaths
allowISG: false, ensure that getStaticPaths exports all valid paths. Incoming requests for dynamic paths absent from the static paths collection will immediately return 404 Not Found.allowISG: false protects server infrastructure against denial-of-service attempts that generate infinite random parameter paths to exhaust build CPU or storage capacity.Branch Enclave and Denial-of-Service Protection in layout_functions.ts
layout_functions.ts, allowISG: false confines accessible layout segments strictly to the collection declared in that layout's getStaticPaths. Requests containing unlisted layout parameters (such as an unknown version in /docs/[version]/guide) trigger an immediate 404 response before child pages are inspected.allowISG: false at the layout boundary, preventing ungenerated layout shells from compiling on demand.// src/app/blog/[slug]/page_functions.ts
import type { PageFunctions } from "dinou";
import { getPublishedSlugs } from "@/lib/content";
// 1. Pre-render all published articles during the build phase
export const getStaticPaths: PageFunctions<"/blog/[slug]">["getStaticPaths"] = async () => {
const slugs = await getPublishedSlugs();
return slugs.map((slug) => ({ slug }));
};
// 2. Strict static enclave: unlisted slugs return 404 immediately
export const allowISG: PageFunctions["allowISG"] = false;
export const getProps: PageFunctions<"/blog/[slug]">["getProps"] = async (params) => {
return { slug: params.slug };
};// src/app/products/[id]/page_functions.ts
import type { PageFunctions } from "dinou";
import { getTopProductIds } from "@/lib/catalog";
// 1. Pre-render the top 100 most popular products at build time
export const getStaticPaths: PageFunctions<"/products/[id]">["getStaticPaths"] = async () => {
const topIds = await getTopProductIds(100);
return topIds.map((id) => ({ id }));
};
// 2. Allow on-demand generation for long-tail products on first visitor request
export const allowISG: PageFunctions["allowISG"] = true;
export const getProps: PageFunctions<"/products/[id]">["getProps"] = async (params) => {
return { id: params.id };
};// src/app/catalog/[category]/page_functions.ts
import type { PageFunctions } from "dinou";
// Programmatic ISG flag toggled via environment configuration
export const allowISG: PageFunctions["allowISG"] = () => {
// Disable runtime generation in test or high-load maintenance windows
return process.env.ENABLE_RUNTIME_ISG !== "false";
};
export const getStaticPaths: PageFunctions<"/catalog/[category]">["getStaticPaths"] = () => [
{ category: "electronics" },
{ category: "apparel" },
{ category: "books" },
];// src/app/docs/[version]/layout_functions.ts
import type { LayoutFunctions } from "dinou";
// Define the exact supported documentation versions at build time
export const getStaticPaths: LayoutFunctions<"/docs/[version]">["getStaticPaths"] = () => [
{ version: "v1" },
{ version: "v2" },
{ version: "v3" },
];
// Enforce a closed enclave across all child documentation pages
export const allowISG: LayoutFunctions["allowISG"] = false;validateParams()
Perimeter Guard(params?: TParams) => boolean | Promise<boolean>layout_functions.ts & page_functions.tsRoute perimeter guard that evaluates incoming dynamic route parameters before pipeline execution. Returning false immediately terminates the request with a clean 404 Not Found HTTP response without invoking getProps or evaluating downstream components.
getProps executes. Prevents expensive database queries, external API calls, or server load when incoming requests contain malformed, illegal, or malicious parameter inputs.validateParams in layout_functions.ts validates segment parameters (such as organization slugs or project identifiers) and guards the entire subtree, halting requests before any child layout or nested page executes.Authoritative 404 Response and Delivery Veto
validateParams returns false or resolves to false, Dinou halts route resolution immediately and responds with a clean 404 Not Found HTTP response.false acts as an authoritative veto that blocks delivery and returns a 404 response.Subtree Perimeter Guard in layout_functions.ts
layout_functions.ts, validateParams receives exclusively the dynamic parameters of that specific layout segment (for example, params.organization in /[organization]/layout.tsx) rather than downstream child page parameters.false or resolves to false, Dinou halts execution immediately with a 404 response, preventing child layouts, child parameter validations, getProps loaders, and page renderers from running./dashboard, /billing, /settings, and /members) with a single validation hook at the branch root, eliminating redundant validation logic across individual page_functions.ts files.// src/app/users/[id]/page_functions.ts
import type { PageFunctions } from "dinou";
// Synchronous perimeter guard rejecting non-numeric parameters
export const validateParams: PageFunctions<"/users/[id]">["validateParams"] = (params) => {
return /^[0-9]+$/.test(params.id);
};
export const getProps: PageFunctions<"/users/[id]">["getProps"] = async (params) => {
// params.id is guaranteed to be a valid numeric string here
return { userId: Number(params.id) };
};// src/app/documents/[uuid]/page_functions.ts
import type { PageFunctions } from "dinou";
const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
// Rejects malformed UUID tokens before invoking backend loaders
export const validateParams: PageFunctions<"/documents/[uuid]">["validateParams"] = (params) => {
return UUID_V4_REGEX.test(params.uuid);
};// src/app/[organization]/layout_functions.ts
import type { LayoutFunctions } from "dinou";
import { db } from "@/lib/database";
// Asynchronous perimeter guard protecting an entire organization branch
export const validateParams: LayoutFunctions<"/[organization]">["validateParams"] = async (params) => {
const tenant = await db.organizations.findUnique({
where: { slug: params.organization },
select: { active: true },
});
return Boolean(tenant?.active);
};// src/app/shop/[category]/[slug]/page_functions.ts
import type { PageFunctions } from "dinou";
const ALLOWED_CATEGORIES = new Set(["electronics", "clothing", "books", "home"]);
// Multi-segment parameter validation ensuring valid taxonomy paths
export const validateParams: PageFunctions<"/shop/[category]/[slug]">["validateParams"] = (params) => {
if (!ALLOWED_CATEGORIES.has(params.category)) {
return false;
}
return /^[a-z0-9-]+$/.test(params.slug);
};