Skip to content
Twilight React Playground
ثيم رائدaren

HeadDescriptor

interfaceBeginnerserverbrowserlive demo

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

A HeadDescriptor is a plain object describing the page head. The engine turns it into the real title, meta, link and script tags.Try this: switch the image on and off, then add JSON-LD: one field in the object becomes several tags in the head.
Storefront canvas · ar · RTL
resolveHead() turned the descriptor into these tags:
<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">
Controls
openGraph.images
jsonLd
What a theme writes
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

app/routes/lookbook.tsx
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 ", ") and robots<meta name>; openGraph.*og:* properties, with publishedTime/modifiedTime as article:published_time/article:modified_time and each image as og:image plus its width, height and alt; twitter.*twitter:* names; canonical<link rel="canonical">; each alternateLanguages item → <link rel="alternate" hrefLang>; jsonLd → one <script type="application/ld+json">; each importMap<script type="importmap">, before your own scripts.

  • meta, links, styles and scripts are passed through as written, for anything the named fields do not cover.

  • Across nested routes TanStack Router keeps one <title> and one meta tag per name or property, the deepest route's, but keeps every link, style and script (exact duplicates dropped). So a page's description replaces the store default, while its canonical is added next to the root route's (buildTagsFromMatches in @tanstack/react-router).

  • withHead(routeModule) from @salla.sa/twilight-theme-engine/tanstack calls head(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.hreflang is copied as written, and React expects hrefLang on a <link>: in development it logs "Invalid DOM property hreflang. Did you mean hrefLang?". For language alternates use alternateLanguages, which the adapter renames.

  • docs/07-data-types.md imports HeadDescriptor from @salla.sa/twilight-theme-engine. The package root does not export it; import it from @salla.sa/twilight-theme-engine/utils/head.

Related

Source and docs