withHead
Wraps a route module's head function into the head option of a TanStack route, with an optional step to change the result.
import { withHead } from '@salla.sa/twilight-theme-engine/tanstack';In plain words
Each built-in page (a route module such as Product or Cart) describes its <head> tags (title, description, social preview, canonical link) in a framework-neutral object. A TanStack route expects its head option in a different shape.
head: withHead(Product) connects the two. When the page loads, it gives the module's head the engine context and the data the page's loader returned, then converts the result. Every generated route file uses it, and any object with a head(ctx, data) function works, including your own.
The optional second argument, extend, changes the result before it is converted, for example to add the store name to a title.
Signature
function withHead<T>(
route: { head: (ctx: TwilightContext, data: T) => HeadDescriptor },
extend?: (result: HeadDescriptor, ctx: TwilightContext, data: T) => HeadDescriptor
): (match: Record<string, unknown>) => Record<string, unknown>
// The returned function reads match.loaderData (the data) and match.matches (for hydration).Try it live
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,
});
Example
// Declared in app/routes.ts: route('/faq', 'faq.tsx')
import { createFileRoute } from '@tanstack/react-router';
import type { HeadDescriptor } from '@salla.sa/twilight-theme-engine/utils/head';
import { withHead, type TwilightContext } from '@salla.sa/twilight-theme-engine/tanstack';
interface FaqData {
questions: Array<{ q: string; a: string }>;
}
// Any object with head(ctx, data) works, not only the engine's route modules.
const Faq = {
head: (ctx: TwilightContext, data: FaqData): HeadDescriptor => ({
title: `FAQ | ${ctx.settings.store?.name ?? ''}`,
description: data.questions[0]?.q,
}),
};
export const Route = createFileRoute('/{-$locale}/faq')({
loader: async (): Promise<FaqData> => ({
questions: [{ q: 'How long is delivery?', a: 'Two to four days.' }],
}),
head: withHead(Faq),
component: FaqPage,
});
function FaqPage() {
const { questions } = Route.useLoaderData();
return <ul>{questions.map((item) => <li key={item.q}>{item.q}</li>)}</ul>;
}
How it behaves
Before calling your
head, it fills the twilight context from the router matches when the context has no settings yet, which can happen while the page hydrates.A
SettingsErrorthrown byheadorextendis caught and the route contributes no tags; any other error is thrown again. A throw during hydration would make React discard the server HTML.extendreceives the finished descriptor, the context and the loader data. Its return value replaces the result, so start from...result.TanStack merges the heads of every matched route: the deepest route's title wins, a
metawith the samenameorpropertyis replaced by the deepest route's, andlinks,stylesandscriptsare appended with only exact duplicates removed. A page'scanonicaltherefore sits next to the root head's own, not in its place.
Gotchas
No loader, no tags: without
loaderDatathe returned function returns{}before callinghead, so a route without a loader shows only the root's title. Give the route a loader, or write a plain TanStackheadfor pages without data.docs/03-routing-system.md shows
ctx.store?.nameinsideextend. The context has nostorefield: readctx.settings.store?.name.
Related
Converts a HeadDescriptor (title, description, Open Graph, canonical, JSON-LD) into the meta, links, styles and scripts arrays a TanStack route returns.
getTwilightContextReads the engine's context outside React: store settings, language, location, route id, auth token and the data cache.
Forking a built-in pageTake over one built-in page: point its path at your own route file, reuse the module's loader and head, and swap or wrap its Component.
Route modulesEvery built-in page is an object with a loader that fetches its data, a head that sets its tags, and a Component that draws it.
SettingsErrorThe error thrown when a request has no store settings; left uncaught at the root it becomes the "Store Unavailable" page.