HeadDescriptor
The plain object a route's head() returns: title, description, Open Graph and Twitter cards, hreflang links, JSON-LD and extra tags.
import { HeadDescriptor, OpenGraphImage, LinkDescriptor, StyleDescriptor, ScriptDescriptor } from '@salla.sa/twilight-theme-engine/utils/head';In plain words
The <head> of a page holds things shoppers barely see but search engines and social networks read: the title in the browser tab, the description in search results, the picture shown when a link is shared. The engine describes all of that with one plain object, a HeadDescriptor, instead of tags.
Each engine route module (the object behind a page) has a head() function that returns one. The engine then turns it into the real <title>, <meta>, <link> and <script> tags.
Signature
interface HeadDescriptor {
title?: string;
description?: string;
keywords?: string | string[];
canonical?: string;
robots?: string;
openGraph?: {
type?: string; siteName?: string; title?: string; description?: string; url?: string;
locale?: string; alternateLocale?: string[];
images?: string | OpenGraphImage | Array<string | OpenGraphImage>;
publishedTime?: string; modifiedTime?: string;
};
twitter?: {
card?: string; title?: string; description?: string;
images?: string | string[]; site?: string; creator?: string; url?: string;
};
alternateLanguages?: Array<{ hreflang: string; href: string }>;
jsonLd?: Record<string, unknown> | Array<Record<string, unknown>>;
importMap?: Array<{ imports: Record<string, string> }>;
meta?: Array<{ name?: string; property?: string; httpEquiv?: string; content: string }>;
links?: LinkDescriptor[];
styles?: StyleDescriptor[];
scripts?: ScriptDescriptor[];
}
interface OpenGraphImage { url: string; width?: number; height?: number; alt?: string }
interface LinkDescriptor {
rel: string; href: string;
id?: string; type?: string; as?: string; hreflang?: string; media?: string; sizes?: string;
crossOrigin?: 'anonymous' | 'use-credentials'; integrity?: string;
fetchPriority?: 'high' | 'low' | 'auto';
[key: string]: string | undefined;
}
interface StyleDescriptor { id?: string; media?: string; children: string }
interface ScriptDescriptor {
id?: string; type?: string; src?: string; async?: boolean; defer?: boolean; nonce?: string;
crossOrigin?: 'anonymous' | 'use-credentials'; children?: string;
[key: string]: unknown;
}Try it live
<title>Summer sale</title> <meta name="description" content="Up to 50% off chairs and lamps."> <meta property="og:type" content="website"> <meta property="og:title" content="Summer sale"> <meta property="og:image" content="https://cdn.salla.sa/mQgZlG/FaWuBveWH22EqE2qUX9gbUVfG3dVO7vzTyNPBaGf.jpg">
import type { HeadDescriptor } from '@salla.sa/twilight-theme-engine/utils/head';
// The head() of a route module: (ctx, loaderData) => HeadDescriptor
export function head(): HeadDescriptor {
return {
title: 'Summer sale',
description: 'Up to 50% off chairs and lamps.',
openGraph: { type: 'website', title: 'Summer sale', images: 'https://cdn.salla.sa/mQgZlG/FaWuBveWH22EqE2qUX9gbUVfG3dVO7vzTyNPBaGf.jpg' },
};
}
Example
import { createFileRoute } from '@tanstack/react-router';
import { withHead, type TwilightContext } from '@salla.sa/twilight-theme-engine/tanstack';
import type { HeadDescriptor } from '@salla.sa/twilight-theme-engine/utils/head';
import { Lookbook } from '../components/Lookbook';
interface LookbookData {
title: string;
cover: string;
}
const LookbookRoute = {
head: (ctx: TwilightContext, data: LookbookData): HeadDescriptor => ({
title: `${data.title} | ${ctx.settings.store?.name}`,
description: 'Our autumn collection, styled.',
openGraph: {
type: 'website',
title: data.title,
images: { url: data.cover, width: 1200, height: 630 },
},
jsonLd: { '@context': 'https://schema.org', '@type': 'CollectionPage', name: data.title },
}),
};
export const Route = createFileRoute('/{-$locale}/lookbook')({
loader: (): LookbookData => ({
title: 'Lookbook',
cover: 'https://cdn.salla.sa/mQgZlG/FaWuBveWH22EqE2qUX9gbUVfG3dVO7vzTyNPBaGf.jpg',
}),
head: withHead(LookbookRoute),
component: Lookbook,
});
How it behaves
Under TanStack Start the descriptor becomes:
title→<title>;description,keywords(an array is joined with ", ") androbots→<meta name>;openGraph.*→og:*properties, withpublishedTime/modifiedTimeasarticle:published_time/article:modified_timeand each image asog:imageplus its width, height and alt;twitter.*→twitter:*names;canonical→<link rel="canonical">; eachalternateLanguagesitem →<link rel="alternate" hrefLang>;jsonLd→ one<script type="application/ld+json">; eachimportMap→<script type="importmap">, before your ownscripts.meta,links,stylesandscriptsare passed through as written, for anything the named fields do not cover.Across nested routes TanStack Router keeps one
<title>and one meta tag pernameorproperty, the deepest route's, but keeps every link, style and script (exact duplicates dropped). So a page'sdescriptionreplaces the store default, while itscanonicalis added next to the root route's (buildTagsFromMatchesin @tanstack/react-router).withHead(routeModule)from@salla.sa/twilight-theme-engine/tanstackcallshead(ctx, loaderData)and converts the result; it returns no tags while the loader data is empty.The same types are exported from
@salla.sa/twilight-theme-engine/utils.
Gotchas
LinkDescriptor.hreflangis copied as written, and React expectshrefLangon a<link>: in development it logs "Invalid DOM property hreflang. Did you mean hrefLang?". For language alternates usealternateLanguages, which the adapter renames.docs/07-data-types.md imports
HeadDescriptorfrom@salla.sa/twilight-theme-engine. The package root does not export it; import it from@salla.sa/twilight-theme-engine/utils/head.
Related
Lays additions over a base HeadDescriptor: tag lists are appended, Open Graph and Twitter merged one level, every other field replaced.
resolveHead, setHeadAdapterConverts a HeadDescriptor into the active framework's head format through a global adapter, which createRouter() installs for TanStack.
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.