How to link alternative language versions in TanStack Start v1

Link language alternatives for search engines

Problem

When a website offers the same content in multiple languages, search engines treat each language version as a separate page by default. Without explicit signals connecting these versions, search engines cannot understand that /en/about and /fr/about are translations of the same content rather than competing duplicates. This fragmentation splits ranking authority across language versions and creates serving problems: a French-speaking user might see the English version ranked higher in search results, even though a French translation exists. Search engines need explicit metadata to understand the relationship between language variants and serve the appropriate version based on user language preferences and location.

Solution

Add hreflang link elements to the document head that declare all available language versions of each page. These links tell search engines which URLs contain the same content in different languages, allowing them to consolidate ranking signals and serve the correct version to users. Each page lists all its language alternatives, including a reference to itself, creating a bidirectional relationship that search engines use to understand the translation structure. This metadata is added per-route using the framework's head management system, which has access to the current route parameters needed to construct URLs for all language variants.

Steps

1. Create a helper to build language alternate URLs

The router's buildLocation method constructs full URLs from route parameters, which you'll use to generate URLs for each language version.

import { AnyRouter } from "@tanstack/react-router";

export function buildLanguageAlternates(
  router: AnyRouter,
  currentPath: string,
  currentLang: string,
  availableLanguages: string[],
) {
  return availableLanguages.map((lang) => {
    const location = router.buildLocation({
      to: currentPath,
      params: { lang },
    });
    return {
      lang,
      href: `${location.pathname}${location.search}${location.hash}`,
    };
  });
}

This function takes the current route path and generates alternate URLs by substituting each language code into the route parameters.

2. Define your available languages

Create a configuration file that lists all languages your application supports.

export const AVAILABLE_LANGUAGES = ["en", "fr", "de", "es"];

export const DEFAULT_LANGUAGE = "en";

This centralized list will be used to generate hreflang links for every page.

The head function receives context including matches, params, and loaderData, which provides access to the current language parameter and router instance.

import { createFileRoute } from "@tanstack/react-router";
import { buildLanguageAlternates, AVAILABLE_LANGUAGES } from "../i18n-config";

export const Route = createFileRoute("/$lang/about")({
  head: ({ params }) => {
    const alternates = buildLanguageAlternates(
      Route.router,
      "/$lang/about",
      params.lang,
      AVAILABLE_LANGUAGES,
    );

    return {
      links: alternates.map((alt) => ({
        rel: "alternate",
        hreflang: alt.lang,
        href: alt.href,
      })),
    };
  },
  component: AboutPage,
});

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

The hreflang attribute uses ISO 639-1 language codes, and each link points to the same page in a different language.

4. Add an x-default hreflang for fallback behavior

The x-default hreflang attribute signals the default page when no language matches.

export const Route = createFileRoute("/$lang/about")({
  head: ({ params }) => {
    const alternates = buildLanguageAlternates(
      Route.router,
      "/$lang/about",
      params.lang,
      AVAILABLE_LANGUAGES,
    );

    const defaultUrl = alternates.find((alt) => alt.lang === "en");

    return {
      links: [
        ...alternates.map((alt) => ({
          rel: "alternate",
          hreflang: alt.lang,
          href: alt.href,
        })),
        {
          rel: "alternate",
          hreflang: "x-default",
          href: defaultUrl?.href || alternates[0].href,
        },
      ],
    };
  },
  component: AboutPage,
});

The x-default link provides a fallback URL for users whose language preferences don't match any declared alternate.

5. Apply to dynamic routes with parameters

For routes with additional dynamic segments beyond language, include those parameters when building alternates.

export const Route = createFileRoute("/$lang/posts/$postId")({
  head: ({ params }) => {
    const alternates = AVAILABLE_LANGUAGES.map((lang) => {
      const location = Route.router.buildLocation({
        to: "/$lang/posts/$postId",
        params: { lang, postId: params.postId },
      });
      return {
        lang,
        href: `${location.pathname}${location.search}${location.hash}`,
      };
    });

    return {
      links: [
        ...alternates.map((alt) => ({
          rel: "alternate",
          hreflang: alt.lang,
          href: alt.href,
        })),
        {
          rel: "alternate",
          hreflang: "x-default",
          href:
            alternates.find((a) => a.lang === "en")?.href || alternates[0].href,
        },
      ],
    };
  },
});

Each page must reference all its language versions, including itself, ensuring search engines understand the complete set of translations for that content.

6. Verify hreflang implementation

The head function returns link elements that are rendered by the HeadContent component. Inspect the rendered HTML to confirm the links appear in the document head.

<link rel="alternate" hreflang="en" href="/en/about" />
<link rel="alternate" hreflang="fr" href="/fr/about" />
<link rel="alternate" hreflang="de" href="/de/about" />
<link rel="alternate" hreflang="es" href="/es/about" />
<link rel="alternate" hreflang="x-default" href="/en/about" />

Bidirectional links between pages ensure search engines understand the relationship between localized versions, allowing them to serve the most appropriate version to each user.