GioJSdocs
On this page

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

ParameterTypeDefaultDescription
ctx (required)MetadataContext<Route>-The same context getServerSideProps receives - params, query, headers, cookies, locale, ip, ... - with the same tracking of personal reads.
extras.propsRecord<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.tsx and error.tsx.
  • When. On every render, after getServerSideProps and 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() answers 404, a thrown redirect() redirects, and any other error answers 500 with the nearest error.tsx. On a not-found or error page, a failing generateMetadata never stops the page rendering: it renders with the static metadata exports only, and the failure is logged.
  • Caching. Reading ctx.cookies, ctx.ip, ctx.host, ctx.scheme or a credential header makes the render personal, so a page with revalidate is not cached and a warning names the route. On a shell = 'cache' page the head is part of the cached shell, so using props from a getServerSideProps that read credentials costs the shell its cache as well. Derive metadata from params and query on cached pages.
  • Layouts. A layout's generateMetadata sees 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 props instead of loading the same data twice: the page's getServerSideProps has already run when generateMetadata does.
  • props is read lazily: a generateMetadata that never touches it does not tie the head to the page's data.
  • Under gio export it runs at build time, once per exported page, like getServerSideProps.
  • 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.

Version history

VersionChanges
v0.1.0-beta.8Introduced.