Create your first page
Every *.page.tsx file beneath src/web/ is a page. Its default export is the
React component Warlock renders on the server and hydrates in the browser.
Create a route from the filename
Section titled “Create a route from the filename”Create src/web/about.setup.ts for metadata and src/web/about.page.tsx for the component:
import type { PageConfig } from "@warlock.js/web";
export const config: PageConfig = { metadata: { title: "About us", description: "Meet the team behind the product.", },};export default function AboutPage() { return ( <main> <h1>About us</h1> <p>We build useful things.</p> </main> );}The file is immediately available at /about. index.page.tsx claims its
directory, so src/web/index.page.tsx serves / and
src/web/products/index.page.tsx serves /products.
Declare a stable route name
Section titled “Declare a stable route name”Put a literal route in the page’s config when the public URL should differ
from the file path, or when callers need a stable name:
import type { PageConfig } from "@warlock.js/web";
export const config: PageConfig = { route: { path: "/about", name: "company.about" },};export default function AboutPage() { return <main>About us</main>;}An explicit route always wins over filesystem derivation. Keep it literal; Warlock reads it during discovery without executing application code.
Link between pages
Section titled “Link between pages”Use Link after hydration for navigation without a full document reload:
import { Link } from "@warlock.js/web";
export default function HomePage() { return <Link href="/about">About us</Link>;}The initial request still returns server-rendered HTML. This is SSR with hydration, not React Server Components.
A runtime default export is required
Section titled “A runtime default export is required”config.route is optional; the default component is not. A page with named exports
but no runtime default export fails discovery and names the file. These forms
matter:
export default function Page() {}works.export { Page as default }works.export default interface Page {}does not; the type disappears at runtime.
A parse error is reported separately, so invalid syntax never masquerades as a missing component.
Keep registration outside the component file
Section titled “Keep registration outside the component file”Declare a synchronous, no-argument register() in root.setup.ts,
layout.setup.ts, or the page’s matching .setup.ts file. It runs once per
module namespace in both realms, before middleware or loaders. It must not
return a Promise. The framework projects its browser-safe code to the client;
keep its imports and dependencies safe for both server and browser.
import { groupedTranslations } from "@mongez/localization";
export function register() { groupedTranslations({ welcome: { en: "Welcome", ar: "مرحبًا" } });}import { useTrans } from "@warlock.js/web";
export default function HomePage() { const trans = useTrans(); return <main>{trans("welcome")}</main>;}