How to translate page metadata in TanStack Start v1

Translate metadata for search and social

Problem

Page metadata—titles and descriptions—appears in browser tabs, bookmarks, search results, and social media previews. When metadata language doesn't match page content language, users experience jarring inconsistency before they even view the page. A Spanish page with an English title in search results signals poor localization quality. Search engines may interpret this mismatch as a ranking signal, potentially lowering visibility in language-specific search results.

This disconnect undermines the user experience at the discovery stage. Users searching in their preferred language expect metadata to match, and inconsistency erodes trust before the first click.

Solution

Translate page metadata using the same translation resources as page content. Define a head function in your route configuration that accesses translated strings and returns localized title and description metadata. This ensures consistency between what appears in browser chrome, search results, and the rendered page.

By using react-intl's message formatting within the head function, metadata stays synchronized with your translation workflow and updates automatically when locale changes.

Steps

1. Create a helper to format messages outside React components

The head function runs outside the React component tree where hooks are unavailable. Create a utility that formats messages using createIntl from react-intl.

import { createIntl, createIntlCache } from "react-intl";

const cache = createIntlCache();

export function formatMetadataMessage(
  locale: string,
  messages: Record<string, string>,
  id: string,
  values?: Record<string, string | number>,
): string {
  const intl = createIntl({ locale, messages }, cache);
  return intl.formatMessage({ id }, values);
}

This helper creates an intl instance on demand for use in non-React contexts like the head function.

2. Define metadata translation keys

Add title and description keys to your translation files for each page that needs localized metadata.

export const enMessages = {
  "page.home.title": "Welcome to Our Site",
  "page.home.description": "Discover amazing content in your language",
  "page.about.title": "About Us",
  "page.about.description": "Learn more about our mission and team",
};

export const esMessages = {
  "page.home.title": "Bienvenido a Nuestro Sitio",
  "page.home.description": "Descubre contenido increíble en tu idioma",
  "page.about.title": "Acerca de Nosotros",
  "page.about.description": "Conoce más sobre nuestra misión y equipo",
};

These keys follow the same pattern as your component translations, keeping all localized content in one place.

3. Add a head function to your route

Use the head option in createFileRoute to return translated metadata. Access the current locale from route params or loader data, then format messages using your helper.

import { createFileRoute } from "@tanstack/react-router";
import { formatMetadataMessage } from "../utils/formatMetadataMessage";
import { enMessages, esMessages } from "../i18n/messages";

const messagesByLocale = {
  en: enMessages,
  es: esMessages,
};

export const Route = createFileRoute("/$locale/about")({
  head: ({ params }) => {
    const locale = params.locale || "en";
    const messages = messagesByLocale[locale] || messagesByLocale.en;

    return {
      meta: [
        {
          title: formatMetadataMessage(locale, messages, "page.about.title"),
        },
        {
          name: "description",
          content: formatMetadataMessage(
            locale,
            messages,
            "page.about.description",
          ),
        },
      ],
    };
  },
  component: AboutPage,
});

function AboutPage() {
  return <div>About content</div>;
}

The head function runs during route matching and returns metadata objects that TanStack Start renders into the document head.

4. Use loader data for dynamic metadata

When metadata depends on fetched data, access loaderData in the head function to combine dynamic content with translated templates.

import { createFileRoute } from "@tanstack/react-router";
import { formatMetadataMessage } from "../utils/formatMetadataMessage";
import { enMessages, esMessages } from "../i18n/messages";

const messagesByLocale = {
  en: enMessages,
  es: esMessages,
};

export const Route = createFileRoute("/$locale/posts/$postId")({
  loader: async ({ params }) => {
    const post = await fetchPost(params.postId);
    return { post };
  },
  head: ({ params, loaderData }) => {
    const locale = params.locale || "en";
    const messages = messagesByLocale[locale] || messagesByLocale.en;
    const { post } = loaderData;

    return {
      meta: [
        {
          title: formatMetadataMessage(locale, messages, "page.post.title", {
            title: post.title,
          }),
        },
        {
          name: "description",
          content: post.excerpt,
        },
      ],
    };
  },
  component: PostPage,
});

function PostPage() {
  const { post } = Route.useLoaderData();
  return <article>{post.content}</article>;
}

The loader fetches data before the head function runs, allowing you to interpolate dynamic values into translated metadata templates.