Change page titles and meta tags
Pass an extend function to withHead to adjust a page's title, description and meta tags without rewriting its head.
Goal: change what a page puts in <head>: its title, its description, or a meta tag such as robots, while the engine keeps producing everything else (share tags, canonical link, structured data).
Mechanism: each built-in page's route file sets head: withHead(Module). withHead takes a second, optional argument: an extend function. It receives the tags the engine built for this page (as one object, a HeadDescriptor), the engine's context and the page's loader data, and returns the object to use instead. You adjust that object; withHead turns it into the tags.
1. Try it
A real product from the demo store. Type a title suffix: that is an extend function at work. Switch the view to tags to see what the router receives.
import { createFileRoute } from '@tanstack/react-router';
import { Product } from '@salla.sa/twilight-theme-engine/routes/product';
import type { ProductPageProps } from '@salla.sa/twilight-theme-engine/routes/product';
import { ProductDetailSkeleton } from '@salla.sa/twilight-theme-engine/skeleton';
import { withHead } from '@salla.sa/twilight-theme-engine/tanstack';
export const Route = createFileRoute('/{-$locale}/$slug/p{$id}')({
loader: ({ params }): Promise<ProductPageProps> =>
Product.loader({ params: { id: params.id }, locale: params.locale }),
head: withHead(Product),
pendingComponent: () => <ProductDetailSkeleton />,
component: ProductComponent,
});
function ProductComponent() {
const data: ProductPageProps = Route.useLoaderData();
return <Product.Component {...data} />;
}
2. Take the route file over
The head lives in the page's route file, which the engine generated. Open app/routes/$slug.p$id.tsx and delete its first line, // @auto-generated: from then on the engine leaves the file alone. (Or point the path at a new file, as in Fork a built-in page.)
3. Pass an extend function
import { createFileRoute } from '@tanstack/react-router';
import { Product } from '@salla.sa/twilight-theme-engine/routes/product';
import type { ProductPageProps } from '@salla.sa/twilight-theme-engine/routes/product';
import { ProductDetailSkeleton } from '@salla.sa/twilight-theme-engine/skeleton';
import { withHead } from '@salla.sa/twilight-theme-engine/tanstack';
import { mergeHead } from '@salla.sa/twilight-theme-engine/utils/head';
export const Route = createFileRoute('/{-$locale}/$slug/p{$id}')({
loader: ({ params }): Promise<ProductPageProps> =>
Product.loader({ params: { id: params.id }, locale: params.locale }),
head: withHead(Product, (result, ctx, data) =>
// mergeHead keeps everything the engine set and adds or replaces what you pass.
mergeHead(result, {
title: `${data.product.name} | ${ctx.settings.store?.name ?? ''}`,
meta: [{ property: 'product:availability', content: data.product.is_available ? 'in stock' : 'out of stock' }],
})
),
pendingComponent: () => <ProductDetailSkeleton />,
component: ProductComponent,
});
function ProductComponent() {
const data: ProductPageProps = Route.useLoaderData();
return <Product.Component {...data} />;
}
For one field, spreading is enough. For example, keep search results out of search engines:
// app/routes/search.tsx: only the head line changes
head: withHead(ProductListing, (result) => ({ ...result, robots: 'noindex, follow' })),
In engine terms
withHead(route, extend?) (src/tanstack/head.ts) returns the function TanStack Router calls with the match. It returns {} when the match has no loaderData; otherwise it runs route.head(getTwilightContext(), loaderData), passes the result through extend(result, context, loaderData) and converts the descriptor with tanstackHeadAdapter into meta, links, styles and scripts (JSON-LD becomes a script). mergeHead(base, extension) (src/utils/head.ts) spreads extension over base, appends meta, links, styles and scripts, and merges openGraph and twitter one level deep. The root route adds the store-wide tags (rootHead), including robots: index, follow and a canonical link to the store.
Traps
Go deeper: withHead, mergeHead, HeadDescriptor, route modules, and the lesson Page titles and SEO.