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

Page titles and SEO

Beginner9 min

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.

In search resultsdemostore.salla.sa › leather-armchairLeather armchair with wooden legs | ثيم رائدA deep leather armchair on solid oak legs. Delivered in 2 to 4 days, with free returns for 14 days.
When the link is shared
demostore.salla.saLeather armchair with wooden legs | ثيم رائدA deep leather armchair on solid oak legs. Delivered in 2 to 4 days, with free returns fo…
head(descriptor): {…} 4 keys
meta: Array(6)
0: {…} 1 keys
title: "Leather armchair with wooden legs | ثيم رائد"
1: {…} 2 keys
name: "description"
content: "A deep leather armchair on solid oak legs. Delivered in 2 to 4 days, with free returns for 14 days."
2: {…} 2 keys
property: "og:type"
content: "product"
3: {…} 2 keys
property: "og:title"
content: "Leather armchair with wooden legs | ثيم رائد"
4: {…} 2 keys
property: "og:description"
content: "A deep leather armchair on solid oak legs. Delivered in 2 to 4 days, with free returns for 14 days."
5: {…} 2 keys
property: "og:image"
content: "https://cdn.salla.network/salla.com/logo-wide-1.svg"
links: []
styles: []
scripts: []

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.
Storefront canvas · en · LTR
Runs in the browser…
Controls
extend: add the store name to the title
the loader returned data
What a theme writes
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,
});
Adding the store name to product titles
// 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} />;
}
A page without loader 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
  • head from @salla.sa/twilight-theme-engine/tanstack is the TanStack head adapter: title becomes a { title } meta entry, description, keywords and robots named meta tags, openGraph og:* properties, canonical and alternateLanguages links, jsonLd a JSON-LD script (src/tanstack/head.ts).
  • withHead(route, extend?) returns {} when the match has no loaderData, so a route without a loader gets no tags from it: use a plain head: () => head({...}) there. A SettingsError thrown 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.
  • HeadDescriptor is exported from @salla.sa/twilight-theme-engine/utils/head.
  • Reference: withHead, head, HeadDescriptor, Route modules.
Check yourself

Why set a product's title in the route's head rather than with document.title in an effect?