[]previous and next page

Head and metadata

A route or layout exports metadata, and Dusk writes the head on the server and on every navigation.

For agentsUpdated

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.

src/routes/_layout.tsxtsx
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.

src/routes/posts/[id].tsxtsx
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 title is wrapped in the nearest template above it. absolute skips the template, and default is used only when no layer sets a title.
  • Every other field replaces the value above it as a whole: a page's openGraph replaces the layout's, and the fields are not merged one by one. null removes the value above it.
  • A module exports metadata or generateMetadata, not both.
  • metadataBase must 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.