Support for React Server Components (RSC), Server-Side Rendering (SSR), Static Generation (SSG), Incremental Static Generation (ISG), and Incremental Static Regeneration (ISR).
- React Server Components: Built on the React 19 core, leveraging Suspense and Streaming for optimal performance without client-side waterfalls.
- Server Functions: Type-safe RPC actions. Execute server-side logic directly from your components without creating manual API endpoints.
- Automatic Static Bailout: Static by default (SSG). Automatically switches to Dynamic Rendering (SSR) when request-specific data (cookies, headers, search params) is detected.
- Full-Featured Router: Client-side soft navigation including
push,replace,back,forward, andrefresh(soft reload). - Generation Strategies: Comprehensive support for Incremental Static Regeneration (ISR) and Incremental Static Generation (ISG).
- Bundler Agnostic: The only framework that lets you choose your build engine. Switch between Webpack, Rollup, or Esbuild via configuration.
- Smart Navigation:
<Link>component with automatic prefetching and opt-infreshdata fetching for volatile states. - File-System Routing: Intuitive routing based on
page.{jsx,tsx}files located within thesrcdirectory structure.
- Getting Started
- Routing
- Layouts & Hierarchical Rendering
- Data Fetching & Rendering
- Server Functions (
"use server") & Smart Suspense - Advanced Patterns: The "Dinou Pattern"
- Page Configuration (
page_functions.ts) - π API Reference
- π¨ Favicons
- π Environment Variables (
.env) - π Styles (Tailwind, CSS Modules, & Global CSS)
- πΌοΈ Assets & Media
- π Import Aliases (
@/) - β‘ Bundlers & Running the App
βοΈ Eject Dinou- π Deployment
- π¦ Changelog
- π License
The fastest way to scaffold a new application is using the CLI generator:
npx create-dinou@latest my-app
cd my-app
npm run devAlternatively, you can set up a project manually:
-
Install dependencies:
npm install react react-dom dinou
-
Create the structure: Create a
srcdirectory in the root of your project and add an entrypage.jsxfile:// src/page.jsx export default function Page() { return <h1>Hello, Dinou!</h1>; }
-
Start the server:
npx dinou dev
Dinou uses a file-system based router. Files named page.{jsx,tsx,js,ts} inside the src directory automatically become routes.
| Pattern | File Path | URL Example | Params (params) |
|---|---|---|---|
| Static | src/page.jsx |
/ |
{} |
| Dynamic | src/blog/[slug]/page.jsx |
/blog/hello |
{ slug: "hello" } |
| Optional Dynamic | src/blog/[[slug]]/page.jsx |
/blog |
{ slug: undefined } |
| Catch-all | src/blog/[...slug]/page.jsx |
/blog/a/b/c |
{ slug: ["a", "b", "c"] } |
| Optional Catch-all | src/blog/[[...slug]]/page.jsx |
/blog |
{ slug: [] } |
Note: To access query parameters (e.g.
?q=hello), use theuseSearchParams()hook. They are NOT passed as props.
Dinou supports deep nesting of optional segments ([[...]] or [[slug]]), but it enforces a strict No-Gap Rule.
The Rule: You cannot skip an intermediate optional segment. You can only omit parameters if they are at the end of the URL.
You can leave optional segments undefined, but only if they are the last ones in the structure.
- Structure:
src/inventory/[[warehouse]]/[[aisle]]/page.tsx
| URL | Result (params) |
Status |
|---|---|---|
/inventory/main/a1 |
{ warehouse: "main", aisle: "a1" } |
β Full |
/inventory/main |
{ warehouse: "main", aisle: undefined } |
β Valid (Last one omitted) |
/inventory |
{ warehouse: undefined, aisle: undefined } |
β Valid (All omitted) |
It is not possible to define a later segment while skipping an earlier one.
- Structure:
src/inventory/[[warehouse]]/[[aisle]]/page.tsx
| Goal | Result |
|---|---|
Skip warehouse but define aisle |
β Forbidden: You cannot provide an aisle without providing a warehouse first. |
Due to their "greedy" nature (consuming the rest of the URL), Catch-all segments ([...] and [[...]]) must always be the terminal (last) segment of a route definition. You cannot place other static or dynamic folders inside a Catch-all folder.
Folders wrapped in parentheses are omitted from the URL path. This is useful for organizational purposes.
src/(auth)/login/page.jsxβ/loginsrc/(marketing)/about/page.jsxβ/aboutsrc/(marketing)/(nested)/about/page.jsxβ/about
Why use them? Route Groups allow you to keep your project structure logical without affecting the public URL structure. For example, grouping all authentication-related routes together.
You can define slots (e.g., @sidebar, @header) to render multiple pages in the same layout simultaneously.
src/dashboard/@sidebar/page.jsxsrc/dashboard/(group-a)/@bottom/page.jsxsrc/dashboard/layout.jsxβ Receivessidebarandbottomas props.
Note: Slots must be located in the same logical folder as the layout they serve.
Why use them? Parallel routes allow independent UI sections and Error Containment:
- Server Component with
error.jsx: If the slot fails, only that specific slot renders the error UI. - Server Component without
error.jsx: If the slot fails, it rendersnullsafely. - Client Component: Without an explicit React Error Boundary, an unhandled error here will crash the entire page.
The <Link> component provides optimized client-side transitions with automatic prefetching.
"use client";
import { Link } from "dinou";
export default function Menu() {
return (
<nav>
{/* Prefetches data automatically on hover/viewport */}
<Link href="/about">About Us</Link>
{/* Opt-in for fresh data (bypasses cache) */}
<Link href="/dashboard" fresh>
Dashboard
</Link>
</nav>
);
}Note: Standard HTML
<a>tags also trigger client-side soft navigation via global event delegation in Dinou, but they lack the smart features (prefetching,freshprop) provided by the<Link>component.
Use the useRouter hook inside Client Components ("use client").
"use client";
import { useRouter } from "dinou";
export default function Controls() {
const router = useRouter();
return (
<div>
<button onClick={() => router.push("/home")}>Push</button>
<button onClick={() => router.replace("/home")}>Replace</button>
<button onClick={() => router.back()}>Go Back</button>
<button onClick={() => router.forward()}>Go Forward</button>
{/* Soft Reload: Refetches server data without a browser refresh */}
<button onClick={() => router.refresh()}>Refresh Data</button>
</div>
);
}Dinou uses a nested routing system. Layouts, Error pages, and Not Found pages cascade down the directory hierarchy.
Layouts wrap pages and child layouts. They persist across navigation, preserving state and preventing unnecessary re-renders.
A layout component receives a children prop, params (route parameters), and any parallel slot (e.g., sidebar) defined in the same folder scope.
Note:
searchParamsare NOT passed to layouts.
// src/dashboard/layout.jsx
export default async function Layout({ children, params, sidebar }) {
return (
<div className="dashboard-grid">
<aside>{sidebar}</aside>
<main>
<h1>Dashboard for {params.teamId}</h1>
{children}
</main>
</div>
);
}Layouts are nested by default. A page at src/dashboard/settings/page.jsx will be wrapped by:
src/layout.jsx(Root Layout)src/dashboard/layout.jsx(Dashboard Layout)src/dashboard/settings/page.jsx(The Page)
Fetching data in Layouts:
The Root Layout that applies to a specific page can receive additional props by defining a getProps function in a page_functions.ts file located alongside the page:
// src/foo/bar/page_functions.ts
export async function getProps({ params }) {
// fetch data using route params
const data = await fetchData(params.id);
// 'layout' props are injected into the Root Layout for this page
return { page: { data }, layout: { title: data.name } };
}Create an error.jsx file to define an error page for a route segment. If a page throws an error (Server or Client not controlled by an Error Boundary), Dinou looks for the closest error.jsx in the directory hierarchy (bubbling up).
error.jsx receives error (object) and params as props.
// Error pages can be Client Components or Server Components
"use client";
export default function Page({ error, params }) {
return (
<div>
<h2>Something went wrong in {params.slug}!</h2>
<p>{`${error.name}: ${error.message}`}</p>
{/* error.stack is only defined in development, not in production */}
{error.stack && (
<pre style={{ background: "#eee", padding: "1rem" }}>{error.stack}</pre>
)}
</div>
);
}Create a not_found.jsx file to customize the 404 UI. Like errors, Dinou renders the closest not_found.jsx found traversing up from the requested URL.
not_found.jsx receives params as a prop. To access search parameters, use useSearchParams().
Sometimes you need to break out of the standard nested hierarchy (e.g., a Landing Page that shouldn't share the App Layout). Dinou uses "Flag Files" (empty files with no extension) to control this behavior.
Place these files in the same directory as your component to activate the behavior.
| Flag File | Applies To | Description |
|---|---|---|
reset_layout |
layout.jsx |
Resets the layout tree. This layout becomes the new Root, ignoring all parent layouts. Perfect for separating Marketing pages from App pages. |
no_layout |
page.jsx, error.jsx, not_found.jsx |
Prevents the component from being wrapped by any layout in the hierarchy. |
no_layout_error |
error.jsx |
Specifically prevents layouts only for the Error page. |
no_layout_not_found |
not_found.jsx |
Specifically prevents layouts only for the Not Found page. |
If you have a marketing page at src/marketing/page.jsx and you don't want it to inherit the Root Layout:
- Create
src/marketing/layout.jsx(Your marketing layout). - Create an empty file named
reset_layoutinsidesrc/marketing/. - Result:
src/marketing/page.jsxwill only use the marketing layout, ignoring the global root layout.
Dinou leverages React 19 Server Components to allow direct data access on the server without sending that logic to the client.
You can define a React Server Component by using an async function.
// src/blog/page.jsx
import db from "@/lib/db";
export default async function Page() {
const posts = await db.query("SELECT * FROM posts");
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}Dinou employs a Zero-Config Hybrid Model. You do not need to configure pages as "Static" or "Dynamic" manually. The framework decides at runtime:
- Static (SSG): Pages are pre-rendered at start time by default.
- Dynamic (SSR): If a page utilizes request-specific APIs (Cookies, Headers, Search Params), it automatically opts out of static generation and renders on demand.
import { getContext } from "dinou";
export default async function Profile() {
const ctx = getContext();
if (!ctx) return null;
// accessing cookies automatically switches this page to Dynamic Rendering (SSR)
const token = ctx.req.cookies.session_token;
const user = await fetchUser(token);
return <h1>Hello, {user.name}</h1>;
}You can enable ISR to update static pages in the background without rebuilding the entire site. Export a revalidate function from your page_functions.ts.
// src/blog/page_functions.ts
// This page will regenerate at most once every 60 seconds
export function revalidate() {
return 60000; // time in milliseconds
}To add interactivity (useState, useEffect, event listeners), place the "use client" directive at the top of your file.
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}Dinou supports Server Functions, allowing you to call server-side logic directly from your Client Components like a Remote Procedure Call (RPC). A unique feature of Dinou is that Server Functions can return rendered Components (both Server or Client Components), not just JSON data.
- Define a function with
"use server"at the top of the file. - Import and call it from any component.
// src/server-functions/get-post.jsx
"use server";
import db from "./db";
import Post from "@/components/post.jsx";
export async function getPost(postId) {
const data = await db.query("SELECT * FROM posts WHERE id = ?", [postId]);
return <Post post={data} />;
}When used in a Client Component, you can use react-enhanced-suspense to automatically re-fetch the Server Function when a key changes.
// src/[id]/page.jsx
"use client";
import { getPost } from "@/server-functions/get-post";
import Suspense from "react-enhanced-suspense";
export default function Page({ params: { id } }) {
return (
<Suspense fallback="Loading post..." resourceId={`get-post-${id}`}>
{() => getPost(id)}
</Suspense>
);
}How react-enhanced-suspense works:
- Standard Mode: If only
children(as React Nodes) andfallbackare passed, it behaves exactly like React's native<Suspense>. - Enhanced Mode: If you provide a
resourceIdprop andchildrenis a function, the promise returned by that function is automatically re-evaluated wheneverresourceIdchanges. This is perfect for handling data dependencies withoutuseEffect.
In Server Components, you can simply wrap the async call to stream the result to the client as soon as it is ready.
// src/[id]/page.jsx
import { getPost } from "@/server-functions/get-post";
import Suspense from "react-enhanced-suspense";
export default async function Page({ params: { id } }) {
return (
<div>
{/* Behaves like native Suspense (Streaming) */}
<Suspense fallback="Loading post...">{getPost(id)}</Suspense>
</div>
);
}Dinou introduces a powerful pattern for handling mutations and list updates without full page reloads. By combining Server Functions, Global State (e.g., jotai-wrapper), and react-enhanced-suspense, you can achieve granular reactivity.
In Dinou, Server Functions can return Client Components. We leverage this to return a "Headless State Updater" component after a mutation. This component renders nothing but updates a global resourceId atom (key), triggering a re-fetch of specific data.
Define an atom to hold the resourceId key.
// src/atoms.js
import { atom } from "jotai";
import getAPIFromAtoms from "jotai-wrapper";
export const { useAtom, useSetAtom, useAtomValue, getAtom, selectAtom } =
getAPIFromAtoms({
tasksListKey: atom(0),
isAddTask: atom(false),
// rest of atoms...
});A tiny component whose only job is to update the atoms when it mounts.
"use client";
import { useEffect } from "react";
import { useSetAtom } from "@/atoms";
export default function AddTaskUpdater() {
const setTasksListKey = useSetAtom("tasksListKey");
const setIsAddTask = useSetAtom("isAddTask");
useEffect(() => {
// Update the key to force a re-fetch
setTasksListKey((k) => k + 1);
setIsAddTask(false);
}, [setTasksListKey, setIsAddTask]);
return null; // It renders nothing visually
}Performs the database operation and returns the Client Component.
"use server";
import AddTaskUpdater from "../components/add-task-updater";
import { tasks } from "./db";
export async function addTask(text) {
// Perform DB mutation
tasks.push(text);
// πͺ Magic: Return the updater to run logic on the client
return <AddTaskUpdater />;
}Use react-enhanced-suspense with the resourceId prop. When the resourceId (in this case tasksListKey) changes, the Server Function (tasksList) is re-evaluated.
"use client";
import Suspense from "react-enhanced-suspense";
import { useAtomValue, useAtom } from "@/atoms";
import { addTask } from "./server-functions/add-task";
import { tasksList } from "./server-functions/tasks-list";
import { useState } from "react";
export default function Page() {
const tasksListKey = useAtomValue("tasksListKey");
const [isAddTask, setIsAddTask] = useAtom("isAddTask");
const [text, setText] = useState("");
return (
<div>
{/* The Mutation Form */}
<input type="text" onChange={(e) => setText(e.target.value)} />
<button onClick={() => setIsAddTask(true)}>Add Task</button>
{/* Conditionally render the mutation suspense */}
{isAddTask && (
<Suspense fallback="Adding task..." resourceId="add-task">
{() => addTask(text)}
</Suspense>
)}
{/* The Reactive List */}
{/* Changing resourceId forces Suspense to re-fetch tasksList */}
<Suspense
fallback={<div>Loading tasks...</div>}
resourceId={`tasks-list-${tasksListKey}`}
>
{() => tasksList()}
</Suspense>
</div>
);
}The Server Function responsible for fetching data:
// src/server-functions/tasks-list.jsx
"use server";
import { tasks } from "./db";
import TasksListDisplay from "../components/tasks-list-display";
export async function tasksList() {
return <TasksListDisplay tasks={tasks} />;
}The Client Component responsible for rendering the list:
// src/components/tasks-list-display.jsx
"use client";
export default function TasksListDisplay({ tasks }) {
return (
<div>
{tasks.map((t, index) => (
<div key={index}>{t}</div>
))}
</div>
);
}For advanced control over rendering behavior, data fetching, and static generation, you can create a page_functions.ts (or .js) file next to your page.jsx.
Use this function to fetch data based on the route parameters and inject it into your Page and Layout.
Design Note:
getPropsonly receivesparams. To use request-specific data likesearchParamsorcookies, fetch data directly inside your Server Components usingSuspenseor Hooks to avoid blocking the initial HTML render.
- Arguments:
{ params }(The dynamic route parameters). - Returns: An object with
pageandlayoutkeys containing the props.
// src/blog/[slug]/page_functions.ts
export async function getProps({ params }) {
// 1. Fetch data based on the URL path (e.g., /blog/my-post)
const post = await db.getPost(params.slug);
// 2. Return data.
// 'page' props go to page.jsx
// 'layout' props go to layout.jsx (useful for setting document titles dynamically)
return {
page: { post },
layout: { title: post.title },
};
}Defines which dynamic paths should be pre-rendered at server start (SSG).
- ISG (Incremental Static Generation): Paths not returned here will be generated on-demand when requested for the first time.
Return Format:
Dinou is flexible with the return format depending on the complexity of your route:
| Route Type | Best Format | Example Return |
|---|---|---|
Simple ([id]) |
Array<string> |
["1", "2"] |
Catch-all ([...slug]) |
Array<Array<string>> |
[["a", "b"], ["c"]] |
| Nested / Complex | Array<Object> |
[{ id: "1", category: "tech" }] |
// src/blog/[id]/page_functions.ts
export function getStaticPaths() {
return ["1", "2", "hello"];
// Generates: /blog/1, /blog/2, /blog/hello
}// src/docs/[...slug]/page_functions.ts
export function getStaticPaths() {
return [
["intro"], // /docs/intro
["api", "v1", "auth"], // /docs/api/v1/auth
];
}One of Dinou's most powerful features is that static parameters propagate downwards. If you define values for a segment, Dinou will automatically generate all static sub-pages nested within that segment.
Example Structure:
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 generates 4 pages:
/blog/post-a/blog/post-a/details/blog/post-b/blog/post-b/details
When nesting routes, dependency flows downwards. If an intermediate segment (whether static or dynamic) contains a page.tsx, it becomes a required step in the generation chain.
If a parent page fails to define its own paths (e.g., returns an empty array), the generator stops there. It will never reach the child pages, regardless of whether the children have valid getStaticPaths defined.
Scenario:
src/case3/[slug]/page.tsx(Parent Page)src/case3/[slug]/[id]/page.tsx(Child Page)
In this structure, [id] depends physically on [slug] existing first.
β Broken Chain:
If src/case3/[slug]/page_functions.ts returns [] (no paths):
- Dinou tries to build
/case3/[slug]. - No paths are returned. No folders are created.
- Result: The build process never attempts to generate
[id], because the parent directory/case3/foo/was never created.
β Functional Chain: The parent must resolve its own level for the children to run.
// src/case3/[slug]/page_functions.ts
export function getStaticPaths() {
// 1. Defines the parent folders
return ["foo", "bar"];
}// src/case3/[slug]/[id]/page_functions.ts
export function getStaticPaths() {
// 2. Now runs inside /case3/foo/ and /case3/bar/
return ["100", "200"];
}Rule of Thumb: Every
page.tsxin the hierarchy is responsible for "opening the door" to its children.
When you have multiple dynamic segments in a path without intermediate pages, you must return an Object to map values to all parameter names involved.
// Structure: src/shop/[category]/[...specs]/[[brand]]/page_functions.ts
// (Assuming [category] and [...specs] do NOT have their own page.tsx)
export function getStaticPaths() {
return [
{
category: "electronics",
specs: ["m3", "16gb"],
brand: "apple",
},
{
category: "clothing",
specs: ["cotton", "white"],
brand: undefined, // Valid: optional and at the end of the route
},
];
}Reminder: According to the No-Gap Rule, you can use
undefinedfor an intermediate optional segment only if all subsequent segments are alsoundefined. You cannot leave a "gap" (an undefined segment followed by a defined one).
Dinou ensures that params are consistent between SSG and SSR:
- Catch-all segments will always be an
Array(e.g.,undefinedbecomes[],"val"becomes["val"]). - Optional Single segments remain
undefinedif omitted.
Enables Incremental Static Regeneration. Defines the cache lifetime of a static page in milliseconds.
- Returns:
number(milliseconds). - If it returns
0(or is not defined), the page remains static indefinitely (unless rebuilt).
// src/dashboard/page_functions.ts
export function revalidate() {
return 60000; // Regenerate at most once every 60 seconds
}Forces a page to be rendered dynamically (Server-Side Rendering) on every request, bypassing static generation.
- Returns:
boolean.
// src/profile/page_functions.ts
export function dynamic() {
return true; // Always render on demand (SSR)
}The primary way to navigate between pages. Enables client-side navigation (SPA transition) without full page reloads.
- Props:
href(string): The target path. Supports both absolute and relative paths.prefetch(boolean): Iftrue, preloads the code and data for the destination route when the user hovers over the link. Defaults totrue.fresh(boolean): Iftrue, bypasses the client-side router cache and forces a fetch of the latest data from the server. Defaults tofalse....props: Standard HTML anchor attributes (className,id,target, etc.).
Path Resolution: Dinou resolves paths similar to a file system:
| Type | Syntax | Example | Description |
|---|---|---|---|
| Absolute | Starts with / |
/about |
Navigates from the root of the app. |
| Relative (Child) | No slash or ./ |
team |
Appends to the current path (e.g., /about β /about/team). |
| Relative (Sibling) | Starts with ../ |
../contact |
Go up one level, then down (e.g., /about/team β /about/contact). |
import { Link } from "dinou";
// Absolute path
<Link href="/dashboard">Home</Link>
// Relative path (Go deeper)
<Link href="./settings">Settings</Link>
// Relative path (Go up/Sibling)
<Link href="../profile">Profile</Link>
// With options
<Link href="/volatile-data" fresh={true} prefetch={false}>
Live Status
</Link>A utility component that triggers an immediate client-side navigation when rendered.
- Props:
to(string) - The destination URL. - Usage: While you can use this directly, it is recommended to use the
redirect()helper function instead for better server-side handling.
import { ClientRedirect } from "dinou";
// Forces navigation to home
return <ClientRedirect to="/" />;Functions available in both Server and Client environments.
Stops execution and redirects the user.
- Server: Sets HTTP 307 header (if headers not sent).
- Client: Renders
<ClientRedirect />. - Usage: Use with
returnto halt rendering.
import { redirect } from "dinou";
if (!user) return redirect("/login");Returns a standard URLSearchParams object to read the query string.
import { useSearchParams } from "dinou";
export default function SearchPage() {
const searchParams = useSearchParams();
const query = searchParams.get("q");
return <div>Result: {query}</div>;
}Behavior & Static Generation (Bailout):
| Component Type | Behavior during Build | Result |
|---|---|---|
| Server Component | Accessing this hook triggers a Static Bailout. | The page opts out of SSG and switches to Dynamic Rendering (SSR) on demand. |
| Client Component | Does NOT trigger a bailout. | The page remains Static (SSG). The initial HTML renders with empty params, and the browser updates values upon hydration. |
β οΈ Client Component Warning: If used in a Client Component on a static page, be aware of Hydration Mismatches. The server renders with empty params (since they don't exist at build time), but the browser renders with the real URL. Recommendation: If the initial UI depends heavily on params, pass them as props from a Server Component to force Dynamic Rendering.
Common Methods:
.get(name): Returns the first value..getAll(name): Returns all values (useful for?id=1&id=2)..has(name): Checks existence..toString(): Returns the query string.
Returns the current URL pathname as a string (e.g., /blog/post-1).
Provides programmatic navigation methods.
- Methods:
.push(href),.replace(href),.back(),.forward(),.refresh(). - Note: Only works inside Client Components (
"use client").
Returns a boolean indicating if a client-side navigation is in progress.
Retrieves the request/response context. Server Components Only.
- Returns:
{ req, res }. - req:
headers,cookies,query,path,method. - res:
status(),setHeader(),redirect(),cookie(),clearCookie().
While getContext() technically works during the Server-Side Rendering (SSR) phase of Client Components, using it directly inside a Client Component is strongly discouraged.
"use client";
import { getContext } from "dinou";
// β DANGEROUS PATTERN
export default function UserProfile() {
const ctx = getContext(); // Runs on server during SSR
return <div>{ctx.req.headers["authorization"]}</div>;
// β οΈ The sensitive header is now baked into the public HTML source code!
}Risks:
- Data Leak: Any data read from
getContextduring SSR is serialized into the initial HTML. If you mistakenly render sensitive data (like tokens or internal headers), it will be visible in the page source (View Source), even if React hydration fails later. - Hydration Mismatch: The browser execution will fail because
getContextis not available in the browser, causing the UI to break or flicker.
β Correct Pattern: Fetch sensitive data in a Server Component and pass only the necessary, safe fields as props.
// src/profile/page.tsx (Server Component)
import { getContext } from "dinou";
import ClientProfile from "./client-profile";
export default function Page() {
const ctx = getContext();
// Extract ONLY what is safe for the client
const safeUser = { name: ctx.req.cookies.username };
return <ClientProfile user={safeUser} />;
}Export these functions from page_functions.{ts,js} to configure the associated page.tsx.
Defines paths for Static Site Generation (SSG).
- Returns:
Array<string | string[] | Object>.
export function getStaticPaths() {
return [{ slug: "foo", id: "bar" }];
}Async function to fetch data on the server and pass it as props to the Page component and to the Root Layout (if exists).
- Receives: Object with resolved
params. - Returns: Object with the props.
export async function getProps({ params }) {
const data = await db.getItem(params.id);
return { page: { item: data }, layout: { title: data.title } }; // Available as props in page.tsx and Root Layout of this particular page.
}Sets the Incremental Static Regeneration (ISR) time in milliseconds.
- Returns: Number (ms).
0means no revalidate (always the same static page).
export function revalidate() {
return 60000;
} // Regenerate static page every 1 minuteWhether to bypass static generation or not (e.g. use dynamic rendering).
- Returns:
true,false.
export function dynamic() {
return true;
}Dinou recognizes specific filenames to build the routing hierarchy.
| Filename | Environment | Description |
|---|---|---|
page.{jsx,tsx,js,ts} |
Server or Client | The unique UI for a route. |
layout.{jsx,tsx,js,ts} |
Server or Client | Wraps the page and children segments. |
error.{jsx,tsx,js,ts} |
Server or Client | UI for 500 errors within the segment. |
not_found.{jsx,tsx,js,ts} |
Server or Client | UI for 404 not found pages within the segment. |
Create these empty files to alter how layouts apply to a specific route directory.
| Filename | Effect |
|---|---|
no_layout |
The page.tsx in this folder will NOT be applied any layout. |
reset_layout |
Resets the layout hierarchy. The first layout found from within this folder becomes the Root Layout. |
no_layout_error |
The error.tsx in this folder will render without any layout. |
no_layout_not_found |
The not-found.tsx in this folder will render without any layout. |
To add a favicon to your application:
- Generate your assets using a tool like favicon.io.
- Unzip the downloaded folder.
- Rename the folder to
faviconsand place it in the root of your project. - Update your root
layout.tsx(orpage.tsx) to include the references in the<head>tag:
"use client";
import type { ReactNode } from "react";
export default function Layout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<head>
<title>Dinou App</title>
<link rel="icon" type="image/png" href="/favicon.ico" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest" />
</head>
<body>{children}</body>
</html>
);
}Dinou automatically loads environment variables for server-side code (Server Components, Server Functions, and getProps).
- Create a
.envfile in the root of your project. - Add
.envto your.gitignoreto prevent exposing sensitive keys.
# .env
API_SECRET=my_secret_value
DB_HOST=localhostDinou supports Tailwind CSS, CSS Modules (.module.css), and standard Global CSS (.css) out of the box.
Dinou bundles all your styles into a single file served at /styles.css. You must manually link this file in your root layout or page.
Add the link tag to the <head> of your root component:
<link href="/styles.css" rel="stylesheet" />src/layout.tsx (Client Component example):
"use client";
import type { ReactNode } from "react";
import "./globals.css"; // Import global styles here
export default function Layout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<head>
<title>Dinou App</title>
{/* Favicons omitted for brevity */}
<link href="/styles.css" rel="stylesheet" />
</head>
<body>{children}</body>
</html>
);
}src/globals.css (Tailwind setup):
@import "tailwindcss";
.custom-bg {
background-color: purple;
}src/page.tsx (Using CSS Modules):
"use client";
import styles from "./page.module.css";
export default function Page() {
return (
<div className={`text-red-500 custom-bg ${styles.underlined}`}>
Hello World!
</div>
);
}src/page.module.css:
.underlined {
text-decoration: underline;
}src/css.d.ts (TypeScript support for modules):
declare module "*.module.css" {
const classes: { [key: string]: string };
export default classes;
}You can import media files directly into your components. Dinou supports a wide range of extensions:
- Images:
.png,.jpg,.jpeg,.gif,.svg,.webp,.avif,.ico,.mjpeg,.mjpg - Audio/Video:
.mp4,.webm,.ogg,.mov,.avi,.mkv,.mp3,.wav,.flac,.m4a,.aac
Usage:
// src/component.tsx
"use client";
import logo from "./logo.png";
export default function Component() {
return <img src={logo} alt="Logo" />;
}TypeScript Configuration:
To avoid type errors, create a declaration file (e.g., src/assets.d.ts):
declare module "*.png" {
const value: string;
export default value;
}
// Repeat for other extensions used (jpg, svg, mp4, etc.)Customization: If you need to support additional extensions, you can eject Dinou and modify
dinou/core/asset-extensions.js.
Dinou supports import aliases (e.g., import Button from "@/components/Button"). Configure paths in your tsconfig.json.
tsconfig.json (TypeScript):
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"allowJs": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true
},
"include": ["src"]
}Dinou is flexible and integrates with three major bundlers: esbuild (default), Rollup, and Webpack.
Starts the development server with hot reloading. Files are emitted to the public folder.
| Command | Bundler | Description |
|---|---|---|
npm run dev |
esbuild | Default. Fastest startup. |
npm run dev:esbuild |
esbuild | Explicit esbuild command. |
npm run dev:rollup |
Rollup | Uses Rollup for bundling. |
npm run dev:webpack |
Webpack | Uses Webpack for bundling. |
Compiles the application for production. Files are emitted to the dist3 folder.
| Command | Bundler |
|---|---|
npm run build |
esbuild (Default) |
npm run build:esbuild |
esbuild (Explicit) |
npm run build:rollup |
Rollup |
npm run build:webpack |
Webpack |
Runs the built application from the dist3 folder.
| Command | Description |
|---|---|
npm start |
Same as npm run start:esbuild. |
npm run start:esbuild |
Use it after building with esbuild. |
npm run start:rollup |
Use it after building with Rollup. |
npm run start:webpack |
Use it after building with Webpack. |
If you need full control over the configuration or internal logic, you can "eject" the framework.
npm run eject
# or
npx dinou ejectThis will copy the entire Dinou core into a dinou/ folder in your project root, allowing you to modify build scripts, server logic, and configuration files directly.
Dinou apps can be deployed to any platform supporting Node.js, provided you can pass custom flags.
Dinou works seamlessly on DigitalOcean. It allows full control over the runtime command, essential for the required --conditions react-server flag.
Netlify is currently incompatible because it does not support passing custom Node.js flags (--conditions react-server) during runtime.
Ensure your platform allows customization of the start command. Your start command should look like:
node --conditions react-server ...(Or use npm start if your package.json scripts are configured correctly).
For a detailed list of changes, enhancements, and bug fixes across versions, see the CHANGELOG.md.
Dinou is licensed under the MIT License.