How to link alternative language versions in React Router v7

Link language alternatives for search engines

Problem

When a website serves the same content in multiple languages, search engines face a challenge. Without explicit signals, they treat each language version as a separate, unrelated page. A French user searching in French might see the English version ranked higher than the French version, even though both exist. Similarly, an English user might land on a German page. This happens because search engines cannot automatically determine that /en/about and /fr/about are translations of each other rather than competing pages with duplicate content.

This confusion splits ranking authority between language versions and degrades the user experience. Search engines need explicit metadata to understand which pages are language alternatives of the same content so they can serve the appropriate version based on each user's language preference and location.

Solution

Add hreflang link tags to each page that list all available language versions of that content. These tags use the rel="alternate" attribute to signal that the linked pages are translations, not duplicates. Each tag specifies a language code and points to the URL for that language version.

By declaring these relationships in the page metadata, you help search engines understand your site structure and serve the correct language version to each user. This improves search result relevance and prevents duplicate content penalties.

Steps

The hreflang attribute uses ISO 639-1 language codes optionally followed by ISO 3166-1 Alpha 2 region codes. Create a utility that generates link descriptors for all language versions of a page.

type HreflangLink = {
  tagName: "link";
  rel: "alternate";
  hrefLang: string;
  href: string;
};

export function buildHreflangLinks(
  pathname: string,
  locales: string[],
  baseUrl: string,
): HreflangLink[] {
  return locales.map((locale) => ({
    tagName: "link",
    rel: "alternate",
    hrefLang: locale,
    href: `${baseUrl}/${locale}${pathname}`,
  }));
}

This function takes the current pathname, a list of supported locales, and your site's base URL, then returns an array of link descriptors that the meta function can render.

The x-default hreflang value signals a default page when no other page is better suited and doesn't target a specific language or locale. Add this to guide users whose language you don't support.

export function buildHreflangLinks(
  pathname: string,
  locales: string[],
  baseUrl: string,
  defaultLocale: string,
): HreflangLink[] {
  const links = locales.map((locale) => ({
    tagName: "link",
    rel: "alternate",
    hrefLang: locale,
    href: `${baseUrl}/${locale}${pathname}`,
  }));

  links.push({
    tagName: "link",
    rel: "alternate",
    hrefLang: "x-default",
    href: `${baseUrl}/${defaultLocale}${pathname}`,
  });

  return links;
}

The x-default link typically points to your primary language version and serves as a fallback for users whose language preferences don't match any of your specific language versions.

The meta function can set link tags based on data. Use it to return hreflang links for each route.

import type { Route } from "./+types/about";
import { buildHreflangLinks } from "~/utils/hreflang";

const SUPPORTED_LOCALES = ["en", "fr", "de", "es"];
const BASE_URL = "https://example.com";
const DEFAULT_LOCALE = "en";

export function meta({ location }: Route.MetaArgs) {
  const hreflangLinks = buildHreflangLinks(
    location.pathname,
    SUPPORTED_LOCALES,
    BASE_URL,
    DEFAULT_LOCALE,
  );

  return [
    { title: "About Us" },
    { name: "description", content: "Learn about our company" },
    ...hreflangLinks,
  ];
}

The meta function returns an array of descriptors that can include objects with tagName set to "link". React Router renders these as link elements in the document head.

4. Ensure the Meta component is in your root layout

The Meta component renders all meta tags created by the route module's meta export and should be inside the head of your document. Verify your root layout includes it.

import { Links, Meta, Outlet, Scripts } from "react-router";

export default function Root() {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <Scripts />
      </body>
    </html>
  );
}

The Meta component aggregates and renders all meta descriptors from the matched route, including the hreflang link tags you defined in step 3.

5. Adapt the pathname for locale-prefixed routes

If your routes include the locale in the path (like /en/about), strip it before building hreflang links so all language versions point to the same logical page.

export function meta({ location }: Route.MetaArgs) {
  const pathWithoutLocale = location.pathname.replace(/^\/[a-z]{2}(\/|$)/, "/");

  const hreflangLinks = buildHreflangLinks(
    pathWithoutLocale,
    SUPPORTED_LOCALES,
    BASE_URL,
    DEFAULT_LOCALE,
  );

  return [{ title: "About Us" }, ...hreflangLinks];
}

This ensures that /en/about, /fr/about, and /de/about all generate hreflang links pointing to the correct language-specific URLs for the same underlying content.