First visit and navigation
- Direct visits: For a Worker-served page, Goribu runs middleware and the GET handler, resolves metadata and streams HTML through the Document.
- Page props: A handler's
res.render(props)supplies props for both the page and its browser hydration.
After hydration, eligible Link transitions request a JSON description of the next page and its data. Goribu loads the page module and its styles, updates metadata and commits the navigation. You keep writing ordinary React components and handlers.
In production, pre-rendered pages receive both HTML and client navigation data from Static Assets without invoking the Worker. They still hydrate normally. See Routes for the conditions that make a page static.
Page metadata
Export meta from a route. Use an object for fixed values:
export const meta = {
title: "About our team",
description: "Meet the people building this app.",
canonical: "https://example.com/about",
robots: "index,follow",
};
// Page content lives inside the shared Document.
export default function AboutPage() {
return <main><h1>About our team</h1></main>;
}
export const meta = {
title: "About our team",
description: "Meet the people building this app.",
canonical: "https://example.com/about",
robots: "index,follow",
};
// Page content lives inside the shared Document.
export default function AboutPage() {
return <main><h1>About our team</h1></main>;
}
Use a synchronous function when metadata depends on page props or the URL. Load data in the handler, then pass it through res.render():
import type { Meta } from "goribu";
type Props = { post: { title: string; summary: string } };
// Add this export to a route that renders a post prop.
export const meta: Meta<Props> = ({ props, url }) => ({
title: props.post.title,
description: props.post.summary,
canonical: new URL(url.pathname, "https://example.com").href,
meta: [{ property: "og:type", content: "article" }],
});
// Add this export to a route that renders a post prop.
export const meta = ({ props, url }) => ({
title: props.post.title,
description: props.post.summary,
canonical: new URL(url.pathname, "https://example.com").href,
meta: [{ property: "og:type", content: "article" }],
});
- Function contract: A
metafunction receives{ props, url }and must be synchronous. Do not make it async. - Validation: Metadata is validated before HTML streaming begins.
- Navigation: Route-owned head tags are replaced on navigation.
- Pre-rendered metadata: Props, URL, and metadata are captured at build time. Client navigation reuses that metadata even when query strings change.
- Shared head tags: Keep shared tags, such as the viewport and favicon, in the Document.
- Metadata fields: Supported fields are
title,description,canonical,robots,meta, andlinks. See the reference. - Additional tags: Use
metafor extra name/property tags andlinksfor additional link tags. - Duplicate tags: Do not repeat a canonical or description in both the dedicated field and the additional tags.
The Document
The starter supplies an optional Document in src/setup/. To customize it, edit src/setup/_document.tsxsrc/setup/_document.jsx:
import { ClientEntry, Stylesheet } from "goribu";
import type { DocumentProps } from "goribu";
// Preserve the framework's head, hydration root, and browser entry.
export default function Document({ children, head, nonce }: DocumentProps) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Stylesheet />
{head}
</head>
<body>
<div id="root">{children}</div>
<ClientEntry src="/src/setup/client-entry.js" nonce={nonce} />
</body>
</html>
);
}
import { ClientEntry, Stylesheet } from "goribu";
// Preserve the framework's head, hydration root, and browser entry.
export default function Document({ children, head, nonce }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Stylesheet />
{head}
</head>
<body>
<div id="root">{children}</div>
<ClientEntry src="/src/setup/client-entry.js" nonce={nonce} />
</body>
</html>
);
}
- Default shell: Goribu uses its default shell when no custom Document is present.
- Required browser entry:
src/setup/client-entry.jsis always required. - Document structure: Keep one
Stylesheet, oneClientEntry, and the fixed#root. - Worker nonce:
nonceis available for your own inline scripts on Worker-rendered responses, including static pages rendered by the Worker in development. - Pre-rendered nonce:
nonceisundefinedon prerendered pages, which receive no CSP header. Setprerender = falseon a page that must have a CSP. - Inline scripts: Pass
nonce={nonce}to your inline scripts. React omits an undefined attribute, so one Document works for Worker-rendered and prerendered pages. - CSP configuration: Send a CSP through
responseHeaders. A<meta>tag built fromnoncewould block a prerendered page's inline scripts. See Configuration. - Entry URL:
ClientEntry'ssrcis the development fallback. Production resolves the hashed asset.
Styling and assets
The starter includes Tailwind by default. Plain CSS works too:
import "goribu/client";
import "../styles/global.css";
- CSS imports: Import CSS from
src/setup/client-entry.js, a page or a component. - Style loading: Goribu includes styles needed for the first render and loads the next page's styles during navigation.
- Fixed asset URLs: Put files in
public/when they need a fixed URL. For examplepublic/logo.svgis available at/logo.svg. - Imported assets: Vite-managed imports are built into the asset graph.
- Reserved paths: Do not create files under
/_goribu/or public HTML that shadows a route.
Server and browser code
Read a browser-only API after hydration so the initial server and browser output match. This component checks the connection once when it mounts:
import { useEffect, useState } from "react";
export default function ConnectionCheck() {
const [online, setOnline] = useState<boolean | null>(null);
// After React has taken over
useEffect(() => {
setOnline(navigator.onLine);
}, []);
let message = "Checking…";
if (online !== null) message = online ? "Online" : "Offline";
return <p>{message}</p>;
}
import { useEffect, useState } from "react";
export default function ConnectionCheck() {
const [online, setOnline] = useState(null);
// After React has taken over
useEffect(() => {
setOnline(navigator.onLine);
}, []);
let message = "Checking…";
if (online !== null) message = online ? "Online" : "Offline";
return <p>{message}</p>;
}
- Execution environments: Page components run on the server and in the browser.
- Browser APIs: Use and read browser-only APIs in effects or event handlers.
- Request-specific work: Keep request-specific work in handlers.
Anything serialized into page props is public. Keep credentials and private SDKs in server-only modules, and return only the fields the UI needs.