Page titles and SEO
Set the title, description and share tags in a page's head, with a route's head function and withHead.
Every page has a <head>: tags the shopper never sees on the page, but that decide how it appears elsewhere.
- The title names the browser tab and is the blue line in search results.
- The description is often the grey text under it.
- Open Graph tags (
og:title,og:image…) make the preview card when a link is shared in WhatsApp or X. - canonical and alternate links tell search engines which address is the real one, and where the other languages are.
Describe the page, the engine writes the tags
You describe a page's head as one plain object, a HeadDescriptor. The engine's head() function turns it into the tag lists the router puts in the page. Edit the fields and watch both previews and the generated tags.
head(descriptor): {…} 4 keys
meta: Array(6)
0: {…} 1 keys
1: {…} 2 keys
2: {…} 2 keys
3: {…} 2 keys
4: {…} 2 keys
5: {…} 2 keys
This browser tab's title right now: …
Where the head comes from
Each route has a head option next to its loader and component. For engine pages it is withHead(Module): it hands the module's head function the data the loader returned (the product, the category), so the tags describe the thing on the page. The second argument changes the result before the tags are written.
withHead(Offer) turns a route module's head into the function TanStack Router calls. Here it runs with this page's real context.Try this: turn off "the loader returned data": the result is {}, no tags at all. Then add the store name through extend.import { createFileRoute } from '@tanstack/react-router';
import { withHead } from '@salla.sa/twilight-theme-engine/tanstack';
import { Offer } from '../route-modules/offer';
export const Route = createFileRoute('/{-$locale}/offer')({
loader: () => Offer.loader(),
head: withHead(Offer),
component: OfferPage,
});
// app/routes/product-custom.tsx, listed in app/routes.ts as route('/$slug/p{$id}', 'product-custom.tsx')
import { createFileRoute } from '@tanstack/react-router';
import { Product, type ProductPageProps } from '@salla.sa/twilight-theme-engine/routes/product';
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 }),
// The engine's product tags, with the store name added to the title.
head: withHead(Product, (result, ctx) => ({
...result,
title: `${result.title} | ${ctx.settings.store?.name ?? ''}`,
})),
component: ProductRoute,
});
function ProductRoute() {
const data: ProductPageProps = Route.useLoaderData();
return <Product.Component {...data} />;
}
// app/routes/gift-cards.tsx: a page with no loader data
import { createFileRoute } from '@tanstack/react-router';
import { head } from '@salla.sa/twilight-theme-engine/tanstack';
export const Route = createFileRoute('/{-$locale}/gift-cards')({
head: () =>
head({
title: 'Gift cards',
description: 'Send a gift card by email in a minute.',
openGraph: { type: 'website', title: 'Gift cards' },
}),
component: () => <h1>Gift cards</h1>,
});
In engine terms
headfrom@salla.sa/twilight-theme-engine/tanstackis the TanStack head adapter:titlebecomes a{ title }meta entry,description,keywordsandrobotsnamed meta tags,openGraphog:*properties,canonicalandalternateLanguageslinks,jsonLda JSON-LD script (src/tanstack/head.ts).withHead(route, extend?)returns{}when the match has noloaderData, so a route without a loader gets no tags from it: use a plainhead: () => head({...})there. ASettingsErrorthrown inside is swallowed; anything else is rethrown.- The root route from
createTwilightRootRoute()adds the store-wide head (the SDK script, fonts, favicon). TanStack merges every matched route's head, and the deepest route's title wins. HeadDescriptoris exported from@salla.sa/twilight-theme-engine/utils/head.- Reference: withHead, head, HeadDescriptor, Route modules.