generateMetadata
Build a page's or layout's head metadata per request, from its params, query or the page's props.
app/posts/[id]/page.tsx
import { notFound, type GenerateMetadata, type GetServerSideProps, type InferPageProps } from '@gio.js/core';
import { db, type Post } from '../../../lib/db.server.ts';
export const getServerSideProps: GetServerSideProps<{ post: Post }, '/posts/:id'> = async (ctx) => {
const post = await db.posts.find(ctx.params.id);
if (post === null) notFound();
return { props: { post } };
};
// Reuses the post getServerSideProps already loaded - no second query.
export const generateMetadata: GenerateMetadata<'/posts/:id'> = (ctx, { props }) => {
const { post } = props as InferPageProps<typeof getServerSideProps>;
return {
title: post.title,
description: post.excerpt,
openGraph: { images: post.coverUrl },
};
};Reference
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
ctx (required) | MetadataContext<Route> | - | The same context getServerSideProps receives - params, query, headers, cookies, locale, ip, ... - with the same tracking of personal reads. |
extras.props | Record<string, unknown> | undefined | - | Pages only: the props the page renders with (what getServerSideProps returned, or { params, searchParams }). undefined in a layout. |
Returns
A Metadata object, or a promise of one. It is merged over the module's static metadata export field by field, then into the other segments by the usual rules. undefined or null leaves the static metadata alone. Anything else (a string, a number) is a render error: generateMetadata must return an object, got string.
Behavior
- Where. Read from
layout.tsx,page.tsx,not-found.tsxanderror.tsx. - When. On every render, after
getServerSidePropsand before the page renders, so the tags are in the<head>of the first bytes sent (and of a PPR shell). The functions of the page and of every layout above it run concurrently. A cache hit runs none of them. - Errors. A thrown
notFound()answers404, a thrownredirect()redirects, and any other error answers500with the nearesterror.tsx. On a not-found or error page, a failinggenerateMetadatanever stops the page rendering: it renders with the staticmetadataexports only, and the failure is logged. - Caching. Reading
ctx.cookies,ctx.ip,ctx.host,ctx.schemeor a credential header makes the render personal, so a page withrevalidateis not cached and a warning names the route. On ashell = 'cache'page the head is part of the cached shell, so usingpropsfrom agetServerSidePropsthat read credentials costs the shell its cache as well. Derive metadata fromparamsandqueryon cached pages. - Layouts. A layout's
generateMetadatasees the params of the page being rendered, and on a not-found or error page the params of the route that was not found or failed (none for a URL no route matches).
Types
GenerateMetadata<Route> types the whole function, with ctx.params typed by Route (a pattern of your app such as '/posts/:id', or a params shape). MetadataContext<Route> and MetadataExtras type the arguments alone.
Examples
Title from the URL
Without getServerSideProps, read ctx.params directly:
app/tags/[tag]/page.tsx
import type { GenerateMetadata, PageProps } from '@gio.js/core';
export const generateMetadata: GenerateMetadata<'/tags/:tag'> = (ctx) => ({
title: `Posts tagged ${ctx.params.tag}`,
alternates: { canonical: `/tags/${ctx.params.tag}` },
});
export default function TagPage({ params }: PageProps<'/tags/:tag'>) {
return <h1>Posts tagged {params.tag}</h1>;
}Keep search results out of the index
app/search/page.tsx
import type { GenerateMetadata, PageProps } from '@gio.js/core';
export const generateMetadata: GenerateMetadata = (ctx) => ({
title: ctx.query.q ? `Results for ${ctx.query.q}` : 'Search',
robots: { index: false },
});
export default function Search({ searchParams }: PageProps) {
return <h1>{searchParams.q ? `Results for ${searchParams.q}` : 'Search'}</h1>;
}A layout for a section
app/shop/[category]/layout.tsx
import type { GenerateMetadata, LayoutProps } from '@gio.js/core';
import { db } from '../../../lib/db.server.ts';
// A params shape, not a pattern: the layout also wraps pages below /shop/:category.
export const generateMetadata: GenerateMetadata<{ category: string }> = async (ctx) => {
const category = await db.categories.find(ctx.params.category);
return { title: { default: category.name, template: `%s - ${category.name}` } };
};
export default function CategoryLayout({ children }: LayoutProps) {
return <section>{children}</section>;
}Good to know
- Use
propsinstead of loading the same data twice: the page'sgetServerSidePropshas already run whengenerateMetadatadoes. propsis read lazily: agenerateMetadatathat never touches it does not tie the head to the page's data.- Under
gio exportit runs at build time, once per exported page, likegetServerSideProps. - Like every export but the default, it stays out of the browser bundle. The browser gets the resolved tags in the page's hydration data.
Related
- Metadata & SEO - the guide
metadata- the static form, and every fieldgetServerSideProps- Personalized pages are never shared
Version history
| Version | Changes |
|---|---|
v0.1.0-beta.8 | Introduced. |