Page & Layout Functions
Dinou v7 introduces 2D Horizontal Layered Segmentation. Route configuration and server lifecycle functions are cleanly decoupled into page_functions.ts (for page.tsx) and layout_functions.ts (for layout.tsx).
Overview & Horizontal Segmentation
In Dinou, every route segment can independently configure its data fetching, static pre-rendering, cache lifecycles, and validation without cluttering component UI code.
Placed alongside page.tsx. Configures data fetching (getProps), static paths, ISR, PPR, and validation specifically for that page.
Placed alongside layout.tsx. Supplies data directly to the layout component and manages segment-level cache strategy independently from child pages.
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" (dinou/index.d.ts):
/**
* 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 use | Type to import | Purpose |
|---|---|---|
| src/.../page_functions.ts | PageFunctions | Types data loading (getProps), static paths, and cache for the page. |
| src/.../layout_functions.ts | LayoutFunctions | Types data loading (getProps) and independent cache for the layout shell. |
| Plugins & Shared Utils | RouteSegmentConfig | Base "abstract" interface for generic utilities that process arbitrary route segments. |
import type { PageFunctions } in a page file immediately clarifies intent and self-documents that the functions belong to the page tier.1. getProps (Direct Component Props)
Use getProps to load data on the server based on route params and pass it directly to your component.
v7 Direct Return Contract
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">
<BlogSidebar categories={categories} />
<main>{children}</main>
</div>
);
}Deep-Dive Architecture: Why getProps Only Receives params & The Dynamic Bailout Mechanism
Learn why Dinou separates route parameters (params) from dynamic request data (cookies, headers, and query params), how accessing them in getProps triggers an automatic switch to dynamic rendering (bailout), and how to stream cookie- or auth-dependent components with React <Suspense> without delaying the initial HTML delivery.
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.
Dinou features an automatic bailout mechanism that is essential for cache integrity: it guarantees that personalized or per-request data is never accidentally baked into public static HTML or shared caches. Calling getContext() itself does NOT trigger a bailout; the switch to Dynamic SSR happens only when your code actually reads request-specific properties:
| Property Accessed | Behavior | Why? |
|---|---|---|
| req.cookies | Bailout to Dynamic | Session tokens and user identity vary per request; cannot be cached statically. |
| req.headers | Bailout to Dynamic | Request headers (e.g. authorization, user-agent) are unique per connection. |
| req.query | Bailout to Dynamic | URL query parameters (?q=...) are unconstrained runtime inputs. |
| req.path, req.method | Preserves Static | The normalized segment path is deterministic and safe during SSG/ISR generation. |
Inside Server Components, Partial Prerendering (PPR) can isolate dynamic accesses inside <Suspense> boundaries while preserving the static shell. However, getProps executes outside React before component rendering begins. Because getProps is never wrapped by a <Suspense> boundary, reading cookies or query inside getProps forces the entire route to bail out from static generation directly into Dynamic SSR.
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 (in Server Components with <Suspense> or in Client Components):
// 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"
/>
);
}Handled in getProps. Pure and cacheable for all visitors (SSG/ISR).
Handled in Server Components inside <Suspense> to stream without bailing out the whole page.
Works in Server Components (streamed with <Suspense>) and in Client Components (for instant UI state).
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):
Ideal for fixed sets of routes, locales, or documentation sections without extra function execution overhead:
// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
// Best Practice: Type-safe constant array
export const getStaticPaths: PageFunctions<RouteParams<"/blog/[slug]">]["getStaticPaths"] = [
"foo",
"bar",
];Ideal for fetching IDs or slugs from external APIs or databases at build time:
// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db } from "@/lib/db";
// Best Practice: Type-safe async function loader
export const getStaticPaths: PageFunctions<RouteParams<"/blog/[slug]">]["getStaticPaths"] = async () => {
const ids = await db.getTopProductIds();
return ids;
};2.2. Return Formats
Dinou accommodates simple, catch-all, and multi-segment architectures with flexible item formats:
| Route Type | Accepted Format | Example Return |
|---|---|---|
Simple ([id] or [slug]) | string[] | number[] | ["foo", "bar"] or [1, 2, 3] |
Optional Simple ([[id]] or [[slug]]) | (string | number | undefined)[] | [undefined, "foo", "bar"] |
Catch-all ([...slug]) | string[][] | [["a", "b"], ["c"]] |
Optional Catch-all ([[...slug]])* Use [] (empty array) to pre-render the root path | (string[] | undefined)[] | [[], ["a"], ["a", "b"]] |
Nested / Multi-parameter ([category]/[id]) | Record<string, string | number>[] | [{ category: "tech", id: "1" }] |
Complex Nested + Catch-all ([lang]/[category]/[...slug]) | Record<string, string | string[]>[] | [{ lang: "en", category: "api", slug: ["v2", "auth"] }] |
All-Inclusive Matrix (shop/[category]/[id]/[[format]]/[[...tags]])* Respects Dinou's No-Gap Rule (optionals must be trailing) | Record<string, string | number | string[] | undefined>[] | { category: "shoes", id: 42, format: undefined, tags: undefined }→ Pre-renders: /shop/shoes/42{ category: "shoes", id: 42, format: "pdf", tags: ["sale", "pro"] }→ Pre-renders: /shop/shoes/42/pdf/sale/pro |
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
import type { PageFunctions, RouteParams } from "dinou";
// Best Practice: Type-safe constant array (string[] or number[])
export const getStaticPaths: PageFunctions<RouteParams<"/blog/[id]">]["getStaticPaths"] = [
"1",
"2",
"3",
];
// Generates /blog/1, /blog/2, /blog/3// src/shop/[[category]]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
// Pass undefined to pre-render the base route without the segment
export const getStaticPaths: PageFunctions<
RouteParams<"/shop/[[category]]">
>["getStaticPaths"] = [
undefined, // Generates /shop (root path, params.category will be undefined)
"electronics", // Generates /shop/electronics
"clothing", // Generates /shop/clothing
];Optional Simple: Root Path & ES6 Default Values
getStaticPaths: Return undefined to pre-render the base route without the optional segment (e.g. /shop).page.tsx receives: When visiting the root route, params.category is strictly undefined (never an empty string "" or null). This allows native JavaScript ES6 default destructuring to kick in seamlessly: export default function Page({ params: { category = "all" } }: PageProps).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
import type { PageFunctions, RouteParams } from "dinou";
// Best Practice: Type-safe catch-all segments (string[][])
// Each entry represents an array of path segments
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
import type { PageFunctions, RouteParams } from "dinou";
// Pass an empty array [] to pre-render the root route (/docs)
export const getStaticPaths: PageFunctions<RouteParams<"/docs/[[...slug]]">>["getStaticPaths"] = [
[], // Generates /docs (root path)
["intro"], // Generates /docs/intro
["api", "v1", "auth"], // Generates /docs/api/v1/auth
];Optional Catch-all: Root Path & Param Guarantee
getStaticPaths: To pre-render the root page without any segments (e.g. /docs), return an empty array [] (or undefined). Never return [undefined] or [[undefined]].page.tsx receives: In Dinou, params.slug is guaranteed to always be an array (string[]). For the root page, it receives an empty array [] (length 0). It is never undefined, never [undefined], and never a bare string. You can safely call params.slug.join("/") or params.slug.length without defensive Array.isArray() checks.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_functions.ts)src/blog/[slug]/details/page.tsx (Nested static page)If getStaticPaths in blog/[slug] returns ["post-a", "post-b"], Dinou automatically generates 4 static pages:
/blog/post-a/blog/post-a/details/blog/post-b/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":
/shop/[category]/[id]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 loader)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:
If src/shop/[category]/page_functions.ts returns an empty array []:
- Dinou attempts to build parent category routes (
/shop/[category]). - Since
[]is returned, no parent category folders or HTML files are created. - Result: Dinou halts at the parent level. It never invokes the child
[id]/page_functions.ts, so no child product pages are generated.
The parent resolves its own categories first, unlocking 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/200Rule of Thumb
page.tsx is responsible for opening the door to its descendants. (If an intermediate segment does not have a page.tsx, see Section 2.7 for pass-through segments).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:
// Structure: src/shop/[category]/[brand]/[...specs]/page_functions.ts
// ([category] and [brand] do NOT have their own page.tsx; [...specs] is terminal)
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", "air"],
// → Pre-renders: /shop/clothing/nike/running/shoes/air
},
];// Structure: src/shop/[category]/[id]/[[format]]/page_functions.ts
// ([category] and [id] do NOT have their own page.tsx; [[format]] is terminal)
import type { PageFunctions, RouteParams } from "dinou";
export const getStaticPaths: PageFunctions<
RouteParams<"/shop/[category]/[id]/[[format]]">
>["getStaticPaths"] = [
{
category: "electronics",
id: "macbook-pro",
format: "json",
// → Pre-renders: /shop/electronics/macbook-pro/json
},
{
category: "clothing",
id: "hoodie",
format: undefined, // Valid: trailing optional segment without trailing gap
// → Pre-renders: /shop/clothing/hoodie
},
];Terminal Catch-all & No-Gap Rules
[...slug]) greedily absorbs all remaining path segments to the end of the URL, so it must always be the final folder of a route.[[format]]) can be set to undefined only if all subsequent segments are also undefined. You cannot have an undefined segment followed by a defined one.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";
// Best Practice: Type-safe async generator querying a database or CMS
export const getStaticPaths: PageFunctions<RouteParams<"/products/[id]">]["getStaticPaths"] = async () => {
// Only pre-generate the top 50 products at build time
const featured = await db.getFeaturedProducts({ limit: 50 });
return featured.map((p) => p.id);
// Any product ID outside the top 50 will be generated
// on-demand when first requested and cached statically (ISG).
};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>(Returnfalseto abort rendering and return 404)
// src/posts/[id]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
// Best Practice: Type-safe route parameter validation
export const validateParams: PageFunctions<
RouteParams<"/posts/[id]">
>["validateParams"] = (params) => {
// Only allow positive integer IDs; reject malformed requests with 404
return typeof params?.id === "string" && /^\d+$/.test(params.id);
};validateParams is the first line of defense executed by the Dinou server engine on every incoming request. It has ultimate veto power regardless of whether the route is pure static (SSG), on-demand (ISG), or dynamic (SSR):
Rejects malformed or invalid IDs before on-demand generation starts. Prevents wasteful database queries and stops bots from polluting disk/KV cache with junk pages.
Ultimate veto: even if a static HTML file was pre-rendered by getStaticPaths at build time, returning false immediately blocks delivery with 404, preventing obsolete or revoked pages from being served.
Protects your live server from malicious parameter inputs. Rendering halts and returns 404 before getProps or your page component runs.
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 and cached. - Strict SSG (false): Returning
falsedisables on-demand generation. Any request to a path not included ingetStaticPathsreturns 404 immediately. - Async Kill-Switch: Returning a
Promise<boolean>dynamically evaluates runtime flags or system load before permitting on-demand generation.
Disables on-demand generation permanently. Any path not returned by getStaticPaths returns 404 immediately:
// src/blog/[slug]/page_functions.ts
import type { PageFunctions } from "dinou";
// Strictly serve build-time pre-rendered paths
export const allowISG: PageFunctions["allowISG"] = false;Dynamically toggles ISG via remote config or feature flags (e.g. Redis, LaunchDarkly) to protect upstream databases:
// src/blog/[slug]/page_functions.ts
import type { PageFunctions } from "dinou";
import { getFeatureFlag } from "@/lib/flags";
// Protect origin database during maintenance or traffic spikes
export const allowISG: PageFunctions["allowISG"] = async () => {
const isgAllowed = await getFeatureFlag("allow_ondemand_rendering");
return isgAllowed;
};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).
Dynamic SSR: Escapes static generation (which is the default). The page is rendered dynamically on every incoming request with live database reads.
Incremental Static Regeneration (ISR): The pre-rendered HTML/RSC payload is served from static cache and re-evaluated in the background at most once every N seconds (stale-while-revalidate).
Pure Static (SSG): Pre-rendered at build time and cached indefinitely until explicit on-demand invalidation via revalidatePath() or revalidateTag().
Type-Safe Examples: Constant or Function Declaration
// src/posts/[id]/page_functions.ts
import type { PageFunctions } from "dinou";
import { getArticleStatus } from "@/lib/articles";
// 1. Force Dynamic SSR on every request (< 1 or 0, replaces legacy 'dynamic' export)
export const revalidate: PageFunctions["revalidate"] = 0;
// 2. Incremental Static Regeneration (re-evaluates every 60 seconds)
// export const revalidate: PageFunctions["revalidate"] = 60;
// 3. Pure Static (SSG): Cached indefinitely until on-demand invalidation
// export const revalidate: PageFunctions["revalidate"] = false;
// 4. Type-safe async function: adjust TTL dynamically based on content volatility
// export const revalidate: PageFunctions["revalidate"] = async () => {
// const status = await getArticleStatus();
// return status.isBreakingNews ? 15 : 3600; // 15s for breaking news, 1h for standard stories
// };Migration from Legacy dynamic Export
export function dynamic(). In Dinou v7, dynamic has been completely removed. Setting export const revalidate: PageFunctions["revalidate"] = 0; (or any value < 1) is the official, unified way to escape static generation.7. ppr (Partial Prerendering)
New in v7Partial 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.
PPR eliminates the traditional trade-off between static and dynamic rendering by dividing the page into two phases over a single HTTP connection:
All static UI (layouts, headers, navigation) along with the skeleton placeholders defined in <Suspense fallback={...}> are delivered immediately to the browser with zero server wait time.
Async Server Components wrapped in <Suspense> execute concurrently on the server. As each component resolves its database or API queries, its HTML/RSC chunk is streamed down to replace the skeleton in-place.
- Boolean Flag:
export const ppr: PageFunctions["ppr"] = true;(orfalseto opt out). - Sync Function:
() => boolean(e.g. evaluate environment variables or server conditions). - Async Function:
() => Promise<boolean>(e.g. await remote feature flags or rollout gates).
// src/dashboard/page_functions.ts
import type { PageFunctions } from "dinou";
import { getFeatureFlag } from "@/lib/flags";
// 1. Static Boolean (Standard): Enable PPR for this route
export const ppr: PageFunctions["ppr"] = true;
// 2. Dynamic Async Function: Evaluate remote rollout flags or A/B testing
// export const ppr: PageFunctions["ppr"] = async () => {
// const isPPREnabled = await getFeatureFlag("enable_ppr_dashboard");
// return isPPREnabled;
// };// src/dashboard/page.tsx
import { Suspense } from "react";
import { UserStatsSkeleton, UserStats } from "./components";
export default function DashboardPage() {
return (
<div>
{/* 1. Static shell rendered and delivered instantly */}
<header className="p-6 border-b">
<h1>Dashboard Overview</h1>
</header>
{/* 2. Dynamic component streamed over HTTP once server data resolves */}
<Suspense fallback={<UserStatsSkeleton />}>
<UserStats />
</Suspense>
</div>
);
}8. End-to-End Type-Safe Example (page_functions.ts)
To achieve complete type safety, autocompletion, and compile-time validation in your routes, import PageFunctions (or LayoutFunctions) from "dinou" and pass your parameter and props types:
PageFunctions<TParams, TProps>: Defines the contract for all functions inpage_functions.ts.- Indexed Access Typing: Annotating exports with
PageFunctions<TParams, TProps>["getProps"]ensuresparamsis typed automatically asTParamsand the returned object is validated againstTProps. - Unified Interface: Both
page_functions.tsandlayout_functions.tsshare the exact same structural types.
1. Defining src/blog/[slug]/page_functions.ts
// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
// 1. Route parameters automatically inferred from the route string pattern
type BlogParams = RouteParams<"/blog/[slug]">; // Inferred as: { slug: string }
// 2. Define page props types
export interface BlogPost {
title: string;
slug: string;
content: string;
author: string;
publishedAt: string;
}
export interface BlogPageProps {
post: BlogPost;
}
// 3. Static Paths (constant array or function)
export const getStaticPaths: PageFunctions<BlogParams>["getStaticPaths"] = [
"getting-started",
"building-with-dinou-v7",
];
// 4. Props loader: loads static article content for the 0ms shell
export const getProps: PageFunctions<BlogParams, BlogPageProps>["getProps"] = async (params) => {
// params?.slug is strongly typed as string | undefined
const post = await db.posts.findUnique({ where: { slug: params?.slug } });
if (!post) throw new Error("Post not found");
// Directly returns BlogPageProps (type error if shape doesn't match!)
return { post };
};
// 5. Caching Strategy & PPR configuration
export const revalidate: PageFunctions["revalidate"] = 60; // ISR 60s
export const ppr: PageFunctions["ppr"] = true; // PPR: Static shell + Suspense streaming
// 6. Parameter validation hook (returns false to abort with 404)
export const validateParams: PageFunctions<BlogParams>["validateParams"] = (params) => {
return typeof params?.slug === "string" && params.slug.length >= 3;
};
// 7. Cache tags for on-demand revalidation via revalidateTag("blog")
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
PageFunctions<BlogParams, BlogPageProps> (for pages) or LayoutFunctions<any, BlogLayoutData> (for layouts) and export its properties directly:// 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: layoutRevalidate, ppr: layoutPpr, getProps: layoutGetProps } = layoutConfig;