SEO and head

Set page titles and meta tags, extend the engine's defaults, and redirect old URLs.

Framework: TanStack Start

This page is written for TanStack Start, the default and currently supported framework. See what depends on the framework.

Read this when you want to control how a page appears in search results and share previews.

The head is the part of the page the browser does not show but search engines and social previews read: the title, description, share image and language alternates. The engine fills it for every built-in page.

What you get for free

Each built-in page module has a head function. For the cart it sets the title and description from the page data and adds hreflang links, which tell search engines about the page in other languages. The root route also sets store-wide defaults: title, description, Open Graph and Twitter tags from the store's settings.

Your __root.tsx writes these tags into <head> with ThemeHeadContent from @salla.sa/twilight-theme-engine/tanstack. It is TanStack Router's HeadContent without the modulepreload links, which compete with the page's main image; use it in place of HeadContent, not next to it. See Performance.

In a route file you connect it with withHead (the head line of a route):

app/routes/cart-page.tsx
import { createFileRoute } from '@tanstack/react-router';
import { Cart } from '@salla.sa/twilight-theme-engine/routes/cart';
import { withHead } from '@salla.sa/twilight-theme-engine/tanstack';

export const Route = createFileRoute('/{-$locale}/cart')({
  loader: ({ params }) => Cart.loader({ locale: params.locale }),
  head: withHead(Cart),
  // …
});

Change the engine's head

Pass a second argument to adjust the result. It receives the engine's head, the request context and the loader data:

head: withHead(Product, (result) => ({ ...result, robots: 'noindex, follow' }))

withHead already adds the store name to the title (Page title - Store name). Set appendStoreTitle: false in the result when your title already names the store.

The product listings (/products, /search, a category, a tag, a brand's products) and /reviews have no file in your app/routes/: the engine serves them from its own copy. To change the head of one, copy the engine's file into app/routes/ (for search, node_modules/@salla.sa/twilight-theme-engine/.twilight/search.tsx, written by the first pnpm dev or build), delete its // @auto-generated first line, and change only the head line:

app/routes/search.tsx
head: withHead(ProductListing, (result) => ({ ...result, robots: 'noindex, follow' })),

A file without that line is yours, and the engine uses it instead of its own.

Head for your own page

withHead accepts any object with a head(ctx, data) function that returns a head description. Write one for a page you added:

app/routes/faq.tsx
import { createFileRoute } from '@tanstack/react-router';
import { withHead } from '@salla.sa/twilight-theme-engine/tanstack';
import { buildHreflangAlternates } from '@salla.sa/twilight-theme-engine/utils';

export const Route = createFileRoute('/{-$locale}/faq')({
  loader: () => ({}),
  head: withHead({
    head: (ctx) => ({
      title: 'Frequently asked questions',
      description: 'Answers about shipping and returns.',
      alternateLanguages: buildHreflangAlternates(ctx.settings, ctx.location.pathname),
    }),
  }),
  component: FaqPage,
});

buildHreflangAlternates (from @salla.sa/twilight-theme-engine/utils) returns the language links for a path, the same ones the engine's pages get.

A head description accepts:

FieldBecomes
title, description, keywords, robotsThe matching tags
canonical<link rel="canonical">. Without one, withHead uses the page's own path on the store's domain, without the query
openGraph, twitterShare-card tags (title, images, url, locale …)
alternateLanguageshreflang links
jsonLdA structured-data script
meta, links, styles, scriptsAnything else you need

withHead adds nothing when the route has no loader data, which is why the page above has a loader. Even an empty one is enough.

Redirects

A loader can send the customer somewhere else by throwing a redirect. The starter theme has a route that turns an old brand URL into the new one, using an engine helper:

app/routes/$slug.brand-$id.tsx
import { slugBrandRedirectLoader } from '@salla.sa/twilight-theme-engine/routes/seo-redirects';

export const Route = createFileRoute('/{-$locale}/$slug/brand-{$id}')({
  loader: ({ params }) =>
    slugBrandRedirectLoader({
      params: { slug: params.slug, id: params.id, locale: params.locale },
    }),
});

It sends /{slug}/brand-12 to /{locale}/brands/12 and keeps the language. For your own redirects, throw TanStack Router's redirect from a loader. The engine's other redirect helpers are on SEO redirects.

Next: Salla SDK and events, to change the cart and wishlist in the browser.

On this page