Head and metadata
A route or layout exports metadata, and Dusk writes the head on the server and on every navigation.
A route or layout module declares its document head with exports. Dusk merges the exports of the matched chain, outermost first, writes the result into <head> on the server, and writes it again on every client navigation. The field names follow the Next.js Metadata and Viewport types, so an object moves over unchanged.
import type { Metadata, Viewport } from "@shadow-js/dusk";
import { Outlet } from "@shadow-js/router";
export const metadata: Metadata = {
metadataBase: "https://example.com",
title: { default: "Example", template: "%s · Example" },
description: "A site built with Dusk.",
alternates: { canonical: "./" },
openGraph: { type: "website", siteName: "Example", images: ["/og.png"] },
robots: { index: true, follow: true },
};
export const viewport: Viewport = { width: "device-width", initialScale: 1, themeColor: "#0b0b0a" };
export default function Layout() {
return <Outlet />;
}
A page adds to it. When the title depends on data, export generateMetadata, which receives the params, the search, the loader data and the layers above as parent.
import type { MetadataArgs } from "@shadow-js/dusk";
export const generateMetadata = ({ params }: MetadataArgs<{ id: string }>) => ({
title: `Post ${params.id}`,
openGraph: { type: "article" as const, publishedTime: "2026-10-09T00:00:00Z" },
});
export default () => <article>Post</article>;
How layers merge#
- A string
titleis wrapped in the nearesttemplateabove it.absoluteskips the template, anddefaultis used only when no layer sets a title. - Every other field replaces the value above it as a whole: a page's
openGraphreplaces the layout's, and the fields are not merged one by one.nullremoves the value above it. - A module exports
metadataorgenerateMetadata, not both. metadataBasemust be an absolute http(s) URL; relative URLs in the metadata resolve against it.
Values are escaped, and a URL that is not http(s) is dropped, so javascript: never reaches an attribute. generateMetadata runs on the server and in the browser like a loader, so keep secrets out of it.
Next: configure the app. Sitemaps, robots, manifests, JSON-LD and social images are in Metadata.