The exports
A module exports metadata or generateMetadata, not both.
| Export | Where | What it does |
|---|---|---|
metadata |
route, layout | A Metadata object (static) |
generateMetadata |
route, layout | (args) => Metadata | Promise<Metadata>; receives params, search, parent and loaderData |
viewport |
route, layout | A Viewport: the viewport meta, theme color and color scheme |
generateViewport |
route, layout | (args) => Viewport | Promise<Viewport> |
links |
route, layout | Array of link descriptors ({ rel, href, as, ... }), or a function returning one |
meta |
route, layout | React Router style descriptors ({ title }, { name, content }, { property, content }, { tagName: "link", ... }) |
headers |
route, layout | Response headers: an object, or (args) => object with parentHeaders |
A module exports metadata or generateMetadata, not both. The same holds for viewport and generateViewport;
a module that does both reports a TypeError and its layer contributes nothing. The head export of
Routing still works, and the metadata tags win over it for the same tag.
src/routes/_layout.tsxtsx
import type { Metadata, Viewport } from "@shadow-js/dusk";
import { Outlet } from "@shadow-js/router";
import type { JSX } from "@shadow-js/web";
export const metadata: Metadata = {
metadataBase: "https://example.com",
title: { default: "Example", template: "%s · Example" },
description: "Storage for agent plans, files and sites.",
alternates: { canonical: "./" },
openGraph: { type: "website", siteName: "Example", images: ["/og.png"] },
robots: { index: true, follow: true, googleBot: { "max-image-preview": "large" } },
};
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
themeColor: "#0b0b0a",
colorScheme: "dark",
};
export default function Layout(): JSX.Element {
return (
<div>
<Outlet />
</div>
);
}
src/routes/posts/[id].tsxtsx
import type { MetadataArgs } from "@shadow-js/dusk";
import type { JSX } from "@shadow-js/web";
type Post = { readonly title: string; readonly body: string };
export const generateMetadata = ({ loaderData, params }: MetadataArgs<{ id: string }, Post>) => ({
title: loaderData?.title ?? `Post ${params.id}`,
description: loaderData?.body.slice(0, 150),
openGraph: { type: "article" as const, publishedTime: "2026-10-09T00:00:00Z" },
});
export default function Post(): JSX.Element {
return <article>Post</article>;
}