/ โ† Hreflang Fixes / Next.js and Nuxt
In this guide
  1. Start from one locale map
  2. Next.js App Router: the metadata API
  3. Next.js Pages Router: i18n config and next/head
  4. Nuxt with @nuxtjs/i18n
  5. Middleware, redirects and crawlers
  6. Sitemaps, Link headers and libraries: one source only
  7. The fix, step by step
  8. Keeping it right as the project grows
  9. Questions and answers

Start from one locale map

Both frameworks already hold a list of locales somewhere: in next.config.js, in a library such as next-intl, or in the i18n block of nuxt.config.ts. Hreflang errors appear when the annotations are built from something else, such as a hand-typed list in a layout. Put the facts hreflang needs in one module and import it everywhere:

// i18n/locales.ts
export const SITE = "https://example.com";
export const LOCALES = {
  "en-gb": { hreflang: "en-GB", prefix: "" },     // default, no prefix
  "en-ie": { hreflang: "en-IE", prefix: "/ie" },
  "fr":    { hreflang: "fr",    prefix: "/fr" },
} as const;
export const DEFAULT_LOCALE = "en-gb";
export type Locale = keyof typeof LOCALES;

Keep the routing segment and the hreflang value separate. The route segment can be anything convenient, like ie; the hreflang value must be a valid language code with an optional region, like en-IE. Many Next.js sites use the route segment directly as the hreflang value, which works only if the segments happen to be valid codes.

If pages have translated slugs, such as /fr/tarifs for /pricing, the map also needs a path for each page in each locale. Store that with the page content, or in a routes file, and look it up rather than assuming the path is the same in every language.

Next.js App Router: the metadata API

The App Router has no built-in i18n routing. Most sites put a [locale] segment at the top of the app directory and use middleware or a library to choose it. Hreflang goes in the alternates field of the metadata you return from generateMetadata:

// app/[locale]/pricing/page.tsx
import type { Metadata } from "next";
import { LOCALES, DEFAULT_LOCALE, type Locale } from "@/i18n/locales";
import { pathFor } from "@/i18n/paths"; // returns the localised path for a page key

export async function generateMetadata(
  { params }: { params: Promise<{ locale: Locale }> }
): Promise<Metadata> {
  const { locale } = await params;
  const languages: Record<string, string> = {};
  for (const [key, cfg] of Object.entries(LOCALES)) {
    languages[cfg.hreflang] = cfg.prefix + pathFor("pricing", key as Locale);
  }
  languages["x-default"] = pathFor("pricing", DEFAULT_LOCALE);
  return {
    alternates: {
      canonical: LOCALES[locale].prefix + pathFor("pricing", locale),
      languages,
    },
  };
}

Next.js renders each entry as a <link rel="alternate" hreflang="โ€ฆ" href="โ€ฆ"> in the head, including the current locale, so the self-reference comes for free as long as your loop includes every locale. In older versions params is a plain object rather than a promise; awaiting it works in both.

Set metadataBase, or the addresses are wrong

The paths above are relative. Next.js turns them into absolute addresses using metadataBase, which you set once in the root layout:

// app/layout.tsx
export const metadata = { metadataBase: new URL("https://example.com") };

Without it, Next.js warns at build time and falls back to a local address during development, and in some deployments to the platform's own hostname. Hreflang pointing at a preview domain is a common result.

Layouts and pages do not merge alternates

Metadata from layouts and pages is merged shallowly. If the layout sets alternates and the page also sets alternates, the page's object replaces the layout's entirely. A page that sets only a canonical therefore wipes out the layout's hreflang. Generate the whole alternates object in one place for each page, using a shared helper, rather than splitting it between levels.

Next.js Pages Router: i18n config and next/head

The Pages Router has i18n routing built in through next.config.js, with sub-path or domain routing. It sets the lang attribute and handles locale prefixes, but it does not print hreflang. You add it in a shared component with next/head:

// components/Hreflang.tsx
import Head from "next/head";
import { useRouter } from "next/router";

const SITE = "https://example.com";
const CODES = { "en-GB": "en-GB", fr: "fr", de: "de" };

export default function Hreflang() {
  const { asPath, locales = [], defaultLocale } = useRouter();
  const path = asPath.split(/[?#]/)[0];
  const href = (l: string) => SITE + (l === defaultLocale ? "" : "/" + l) + path;
  return (
    <Head>
      {locales.map((l) => (
        <link key={l} rel="alternate" hrefLang={CODES[l]} href={href(l)} />
      ))}
      <link rel="alternate" hrefLang="x-default" href={href(defaultLocale!)} />
    </Head>
  );
}

Two details matter. The query string and fragment are stripped from asPath, so tracking parameters never leak into the tags. And this version assumes the same path in every locale; if your slugs are translated, look the paths up instead. With domain routing, build each address from the locale's domain rather than SITE.

Note that the Pages Router's i18n routing does not work with static export (output: "export"). Statically exported multilingual sites generate each locale's pages themselves and must add hreflang in the same way.

Nuxt with @nuxtjs/i18n

The official Nuxt i18n module can generate hreflang, canonical and og:locale tags from its own configuration. It needs two things to get them right: a baseUrl, so addresses are absolute, and a valid language code on each locale.

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ["@nuxtjs/i18n"],
  i18n: {
    baseUrl: "https://example.com",
    strategy: "prefix_except_default",
    defaultLocale: "en",
    locales: [
      { code: "en", language: "en-GB" },
      { code: "ie", language: "en-IE" },
      { code: "fr", language: "fr-FR" },
    ],
  },
});

Older versions of the module call the language field iso rather than language. Then add the generated tags in app.vue or your default layout with the useLocaleHead composable, passing it to useHead. The option that switches on the hreflang and canonical output has been renamed between major versions, so check the documentation for the version in your lock file and confirm the output in the rendered HTML.

Middleware, redirects and crawlers

Locale detection is where framework sites most often hide pages from Google. A common Next.js middleware reads the Accept-Language header and redirects to a locale prefix. Google's documentation says Googlebot usually crawls without an Accept-Language header and mostly from United States addresses, so this logic sends Google to the default locale, or loops it, however many locales you have.

You can reproduce what Google receives with curl, which sends no language header by default:

curl -sI https://example.com/fr/tarifs | head -1     # expect 200, not 307 or 308
curl -sI https://example.com/ | grep -i "^location"  # see where the root sends visitors

Sitemaps, Link headers and libraries: one source only

Framework projects can end up declaring hreflang in three places without anyone deciding to. The App Router's app/sitemap.ts accepts an alternates.languages object per entry in recent versions, which writes xhtml:link entries. The next-intl library's middleware can send hreflang as an HTTP Link response header, controlled by its alternateLinks option. And your metadata writes head tags. If these are built from different data, they disagree.

Pick one. For most projects the head tags from the metadata API or the Nuxt head helper are easiest to test. If you use a library that adds a Link header, either switch that off or remove your own tags, and make sure any sitemap alternates are generated from the same locale map. The sitemap mismatch guide covers choosing between sources.

๐Ÿ’ก The Link header is invisible in View Source. Check it with curl -sI on a locale page before assuming your head tags are the only hreflang on the site.

The fix, step by step

Step 1

Write down the locale map the site really uses

List every locale the router serves, its path prefix or domain, the valid hreflang code for it, and the default locale. Compare that with next.config.js, your i18n library settings or the Nuxt i18n block, and correct any route codes that are not valid language codes. Put the result in one module that routing, the language switcher and hreflang all import, so there is only one list to change when you add a market.

Step 2

Build one helper that returns every alternate

Write a function that takes a page key and returns the canonical path for each locale, including translated slugs, plus the x-default. It should include the current locale, strip query strings and apply one trailing slash rule that matches your trailingSlash setting. Leave out locales where the page is not published. Use the same function for the canonical, the hreflang tags and any sitemap alternates, so they cannot drift apart.

Step 3

Output the tags through the framework head system

In the App Router, return the full alternates object with canonical and languages from each page's generateMetadata, and set metadataBase in the root layout. In the Pages Router, render the links with next/head in a shared component. In Nuxt, set baseUrl and valid language codes, then pass useLocaleHead output to useHead. Remove any hand-written hreflang left in layouts or custom documents.

Step 4

Stop middleware redirecting crawlers

Review middleware and i18n settings for automatic language detection. Limit redirects to requests with no locale in the path, never redirect a request that already names a locale, and disable browser-language detection where it redirects the root for crawlers. Request each locale page with curl, sending no cookies and no language header, and confirm it returns 200 with its own content rather than a 307 or 308 to another locale.

Step 5

Keep sitemaps and headers from competing

Check whether hreflang is also being produced by app/sitemap.ts, a sitemap module or an i18n library's HTTP Link header. Decide which single source you will keep and switch off the others, or feed every one from the same helper so they are identical. Check the response headers of a locale page with curl -sI, because a Link header will not appear when you view the page source.

Step 6

Test the production build output

Run a production build locally or on a staging deployment with the live base URL and fetch the raw HTML of one page per locale. Confirm every hreflang address is absolute, uses the production host, matches the canonical and includes the page itself and the x-default. Then deploy and run the Hreflang Checker across each locale. Add a test to your pipeline that fails if any hreflang points at a preview host.

Keeping it right as the project grows

For other head tags in the same framework, see fixing meta tags in Next.js.

Questions and answers

1. Does Next.js add hreflang tags automatically?

No. The Pages Router's i18n routing handles locale prefixes, domains and the lang attribute, but it does not print hreflang; you add the links yourself, usually with next/head in a shared component. The App Router has no built-in i18n routing at all, but its metadata API renders hreflang for you from the alternates.languages object you return. In both cases you decide the values, so the accuracy depends on building them from your locale configuration.

2. Why are my Next.js hreflang URLs relative or pointing at localhost?

In the App Router, relative paths in alternates are made absolute using metadataBase, which is normally set in the root layout. If it is missing, Next.js warns at build time and falls back to a local or platform-provided address, so tags can point at localhost or a preview domain. Set metadataBase to your production address, ideally from an environment variable that fails the build if it is empty, and check the rendered HTML of a production build.

3. My page's canonical works but the layout's hreflang disappeared. Why?

Next.js merges metadata from layouts and pages shallowly. When a page returns an alternates object, even one with only a canonical, it replaces the layout's whole alternates object, including languages. The fix is to generate canonical and languages together for each page from one shared helper, rather than setting languages in the layout and canonical in the page. That also guarantees the canonical and the page's own hreflang line use exactly the same address.

4. Is it safe to redirect visitors based on Accept-Language in middleware?

Only with care. Googlebot usually crawls without an Accept-Language header and mostly from the United States, so middleware that redirects based on browser language can send it to the default locale every time, hiding your other versions. Limit automatic redirects to requests with no locale in the path, never redirect a URL that already names a locale, and consider a banner suggesting another language instead. Every locale page should load directly with a 200 status.

5. Which Nuxt i18n settings matter for hreflang?

The two most important are baseUrl, which makes the generated addresses absolute and fixes the host, and a valid language code on each locale, which becomes the hreflang value. Older versions of the module call that field iso and newer ones language. The strategy setting decides the address pattern, and the head composable must be called in app.vue or a layout with SEO output switched on. Option names have changed between major versions, so check the documentation for yours.

6. Should I put hreflang in the Next.js sitemap or in the page head?

Either works, as Google treats them as equivalent, but use only one unless both come from the same helper. Head tags from the metadata API are easy to test by fetching a page. A sitemap from app/sitemap.ts with alternates suits large sites where head blocks get long. Also check whether an i18n library adds hreflang as an HTTP Link header, because that counts as a third source and can quietly disagree with the other two.

7. Can I use the route segment as the hreflang value?

Only if the segment is already a valid language code, optionally with a region. Segments like fr or de-at are fine. Segments like uk for a British store, ie for Ireland, or eu for a European store are not, because uk is the code for Ukrainian, ie is not a language code, and eu is Basque. Keep a separate hreflang value for each locale in your configuration and map from the route segment to it when you build the tags.

Test your framework's hreflang output

The Hreflang Checker follows each cluster of locale pages and validates return tags, language codes, x-default, sitemap consistency, target reachability and canonical alignment, so you can see what your Next.js or Nuxt build actually sends to Google.

Open Hreflang Fixes โ†’
๐Ÿ’ฌ Got a problem?
Related Guides: All Hreflang Fixes  ·  Hreflang and Canonical Conflicts  ·  Meta Tags in Next.js  ·  Hreflang Guide  ·  Learning Hub

About aiwebpageseo

aiwebpageseo.com is a data-driven SEO and AEO (Answer Engine Optimisation) platform providing a free suite of technical SEO tools. Rather than relying on AI-theorised assumptions, the platform analyses live URL performance, delivering objective diagnostics, page speed metrics, CLS debugging, and site crawl data alongside actionable technical tutorials.