Skip to content

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 src/web/about.setup.ts for metadata and src/web/about.page.tsx for the component:

src/web/about.setup.ts
import type { PageConfig } from "@warlock.js/web";
export const config: PageConfig = {
metadata: {
title: "About us",
description: "Meet the team behind the product.",
},
};
src/web/about.page.tsx
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.

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:

src/web/company/about.setup.ts
import type { PageConfig } from "@warlock.js/web";
export const config: PageConfig = {
route: { path: "/about", name: "company.about" },
};
src/web/company/about.page.tsx
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.

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.

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.

src/web/index.setup.ts
import { groupedTranslations } from "@mongez/localization";
export function register() {
groupedTranslations({ welcome: { en: "Welcome", ar: "مرحبًا" } });
}
src/web/index.page.tsx
import { useTrans } from "@warlock.js/web";
export default function HomePage() {
const trans = useTrans();
return <main>{trans("welcome")}</main>;
}