# General Translation Knowledge Base: How do I set up SEO for a multilingual Next.js app?
URL: https://generaltranslation.com/en-GB/kb/multilingual-nextjs-seo.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: Give every language its own URL, set the html lang attribute, point each page's canonical URL at itself, add hreflang alternates for every language, translate titles and descriptions, and list every language version in your sitemap.
Category: Multilingual SEO
Last updated: 2026-02-17
Authors: General Translation Team
Published: 2026-02-17

## Video

The video is narrated in English, with English captions.

[How do I set up SEO for a multilingual Next.js app?](https://caxecubb9c64cezv.public.blob.vercel-storage.com/films/kb-multilingual-nextjs-seo.mp4) (50 secs)

General Translation lets you give every language its own URL and tell search engines about them. Move your pages into `app/[locale]` with `gt-next`'s middleware, set the html `lang` attribute with `useLocale`, add canonical and hreflang links with a helper, translate the title with `getGT`, and list every page in every language in a sitemap.

## Why multilingual SEO needs special attention

Most developers who add i18n to their Next.js app focus on translating the UI and consider the job done.
But if search engines can&#39;t find, index and properly associate your language versions,
all that translation work is invisible.

A multilingual site without a proper SEO set-up runs into problems:

* Google may index only one language version and ignore the rest
* Users searching in Spanish are served the English page
* Duplicate content penalties because Google sees `/en/about` and `/fr/about` as the same page
* The wrong language is shown in search result snippets

**The good news:** getting multilingual SEO right in Next.js isn&#39;t complicated.
There are six things you need to get right, and this guide covers all of them using [`gt-next`](/docs/react/nextjs-quickstart).

***

## 1. Locale-based URL routing

The foundation of multilingual SEO is having distinct URLs for each language.
Search engines need separate, crawlable URLs to index each language version independently.

This means **locale-in-the-URL** — not cookies, not query parameters, not `Accept-Language` detection alone.

```text
✅ generaltranslation.com/en/about
✅ generaltranslation.com/fr/about
✅ generaltranslation.com/es/about

❌ generaltranslation.com/about?lang=fr
❌ generaltranslation.com/about (with locale in a cookie)
```

### Setting up locale routing with gt-next

First, nest your pages under a `[locale]` dynamic segment:

```text
app/
└── [locale]/
    ├── layout.tsx
    ├── page.tsx
    └── about/
        └── page.tsx
```

Then create middleware in your project root (`proxy.ts` for Next.js 16+, or `middleware.ts` for Next.js 15 and earlier):

```ts title="proxy.ts"
import { createNextMiddleware } from 'gt-next/middleware';

export default createNextMiddleware();

export const config = {
  matcher: ['/((?!api|static|.*\\..*|_next).*)'],
};
```

This gives you locale-prefixed URLs automatically.
By default, the default locale (e.g. English) doesn&#39;t get a prefix — `/about` stays clean,
while Spanish users see `/es/about` and French users see `/fr/about`.

***

## 2. Setting the HTML `lang` attribute

The `lang` attribute on your `<html>` tag tells browsers and search engines which language the page is in.
It&#39;s one of the simplest and most effective things you can do for accessibility and SEO.

Without it, screen readers have to guess the language (often incorrectly), and search engines are less confident in their language classification.

{/* useLocale works in both client- and server-rendered components, but not in async components */}

gt-next provides the [`useLocale`](/docs/react/reference/hooks/use-locale) hook, which makes this trivial in your root layout:

```tsx title="app/[locale]/layout.tsx"
import { useLocale, GTProvider } from 'gt-next';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  const locale = useLocale();

  return (
    <html lang={locale}>
      <body>
        <GTProvider>
          {children}
        </GTProvider>
      </body>
    </html>
  );
}
```

[`useLocale`](/docs/react/reference/hooks/use-locale) returns the BCP 47 locale code (e.g. `en-US`, `ar`, `zh-Hans`).

***

## 3. Canonical URLs

Canonical tags tell search engines which URL is the &quot;primary&quot; version of a page.
For multilingual sites, each language version should point to itself as the canonical URL:

```html
<!-- On /fr/about -->
<link rel="canonical" href="https://example.com/fr/about" />
```

This prevents search engines from treating your French page as a duplicate of your English page.

In Next.js, you set canonical URLs through the [metadata API](https://nextjs.org/docs/app/api-reference/functions/generate-metadata).
Combine it with gt-next&#39;s [`getLocale`](/docs/react/nextjs/reference/functions/get-locale) to generate the correct canonical URL for each locale:

```tsx title="app/[locale]/about/page.tsx"
import { getLocale } from 'gt-next/server';

const BASE_URL = 'https://example.com';

export async function generateMetadata() {
  const locale = await getLocale();

  return {
    alternates: {
      canonical: `${BASE_URL}/${locale}/about`,
    },
  };
}

export default function AboutPage() {
  return <h1>About Us</h1>;
}
```

For the default locale without a prefix, adjust accordingly:

```tsx
import { getDefaultLocale } from 'gt-next';
import { getLocale } from 'gt-next/server';

export async function generateMetadata() {
  const locale = await getLocale();
  const defaultLocale = getDefaultLocale();
  const path = '/about';

  const prefix = locale === defaultLocale ? '' : `/${locale}`;

  return {
    alternates: {
      canonical: `${BASE_URL}${prefix}${path}`,
    },
  };
}
```

***

## 4. Hreflang tags

Hreflang tags are the most important multilingual SEO signal.
They tell search engines: &quot;this page exists in these other languages, and here are the URLs.&quot;

Without hreflang, Google has to guess which language version to show in search results — and it often gets it wrong.

```html
<link rel="alternate" hreflang="en" href="https://example.com/about" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr/about" />
<link rel="alternate" hreflang="es" href="https://example.com/es/about" />
<link rel="alternate" hreflang="x-default" href="https://example.com/about" />
```

The `x-default` tag tells search engines which URL to show when none of the specified languages match the user.

In Next.js, you can add hreflang through the metadata API&#39;s `alternates.languages` property.
Here&#39;s a reusable helper that works with gt-next:

```tsx title="lib/i18n-metadata.ts"
import { getDefaultLocale } from 'gt-next';
import { getLocale } from 'gt-next/server';

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

export async function getI18NMetadata(path: string) {
  const locale = await getLocale();
  const defaultLocale = getDefaultLocale();

  const getUrl = (loc: string) => {
    const prefix = loc === defaultLocale ? '' : `/${loc}`;
    return `${BASE_URL}${prefix}${path}`;
  };

  const languages: Record<string, string> = {};
  for (const loc of SUPPORTED_LOCALES) {
    languages[loc] = getUrl(loc);
  }
  languages['x-default'] = getUrl(defaultLocale);

  return {
    alternates: {
      canonical: getUrl(locale),
      languages,
    },
  };
}
```

Then use it on any page:

```tsx title="app/[locale]/about/page.tsx"
import { getI18NMetadata } from '@/lib/i18n-metadata';

export async function generateMetadata() {
  return await getI18NMetadata('/about');
}
```

This generates both the canonical URL and all hreflang tags in one call.

***

## 5. Translated metadata

Search engines display your page title and description in their results.
If these are in English on a French page, users are less likely to click through — and Google may demote the result.

Use gt-next&#39;s [`getGT`](/docs/react/nextjs/reference/functions/get-gt) function to translate metadata strings:

```tsx title="app/[locale]/about/page.tsx"
import { getGT } from 'gt-next/server';
import { getI18NMetadata } from '@/lib/i18n-metadata';

export async function generateMetadata() {
  const t = await getGT();
  const i18nMeta = await getI18NMetadata('/about');

  return {
    title: t('About Us'),
    description: t('Learn about our mission and team.'),
    ...i18nMeta,
  };
}
```

This gives you localised titles and descriptions in search results,
which significantly improves click-through rates for non-English queries.

***

## 6. Multilingual sitemaps

A sitemap helps search engines discover all of your pages, including every language version.
For multilingual sites, you should also include hreflang annotations in your sitemap.

Next.js supports [programmatic sitemaps](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap) through a `sitemap.ts` file:

```ts title="app/sitemap.ts"
import { MetadataRoute } from 'next';

const BASE_URL = 'https://example.com';
const LOCALES = ['en', 'fr', 'es'];
const DEFAULT_LOCALE = 'en';
const PAGES = ['/', '/about', '/blog', '/contact'];

export default function sitemap(): MetadataRoute.Sitemap {
  return PAGES.flatMap((path) => {
    const getUrl = (locale: string) => {
      const prefix = locale === DEFAULT_LOCALE ? '' : `/${locale}`;
      return `${BASE_URL}${prefix}${path === '/' ? '' : path}`;
    };

    const languages: Record<string, string> = {};
    for (const locale of LOCALES) {
      languages[locale] = getUrl(locale);
    }

    return LOCALES.map((locale) => ({
      url: getUrl(locale),
      lastModified: new Date(),
      alternates: { languages },
    }));
  });
}
```

This generates a sitemap with one entry per page per locale,
and each entry includes hreflang annotations pointing to all language versions.

***

## Checklist

Here&#39;s a quick summary of everything covered:

| SEO requirement       | Implementation                                                                                                            |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Locale in URL         | [`createNextMiddleware()`](/docs/react/nextjs/reference/functions/create-next-middleware) with `[locale]` dynamic segment |
| HTML `lang` attribute | [`useLocale()`](/docs/react/reference/hooks/use-locale) in root layout                                                    |
| Canonical URLs        | [`getLocale()`](/docs/react/nextjs/reference/functions/get-locale) + Next.js metadata API `alternates.canonical`          |
| Hreflang tags         | Next.js metadata API `alternates.languages` with all supported locales                                                    |
| Translated metadata   | [`getGT()`](/docs/react/nextjs/reference/functions/get-gt) for page titles and descriptions                               |
| Multilingual sitemap  | `sitemap.ts` with per-locale entries and hreflang alternates                                                              |

***

Use the [`gt-next` Quickstart](/docs/react/nextjs-quickstart) to set up the full internationalisation stack. Then configure routing with the [middleware guide](/docs/react/nextjs/app-router-middleware), generate multilingual pages with [static site generation](/docs/react/nextjs/app-router-static-site-generation), and add [right-to-left support](/docs/react/guides/managing-locales).

## Related questions

- [Can I do i18n without translation files?](https://generaltranslation.com/en-GB/kb/i18n-without-translation-files.mdx): Yes. With gt-react or gt-next, content stays inline in your components inside a `<T>` component, and translations are generated at build time as output, so there are no keys or per-locale JSON files to write and keep in sync.
- [What is the best localisation software for developers?](https://generaltranslation.com/en-GB/kb/best-localization-software.mdx): Look for one system that covers the whole pipeline, from the i18n library in your code to translation and delivery, instead of stitching together a separate library, translation management system and translation vendor.
- [Why use General Translation instead of a TMS (Smartling, Lokalise, Phrase, Crowdin, or Transifex)?](https://generaltranslation.com/en-GB/kb/general-translation-vs-tms.mdx): A translation management system (TMS) such as Smartling, Lokalise, Phrase, Crowdin, or Transifex coordinates translators, but engineers still extract strings, send them out, wait for translations, and wire them back into the product. General Translation translates content in your code with its context, ships every language with each release, and runs on open-source libraries with no lock-in.

## Previous and next questions

- Previous question: [How do I translate conditional text in JSX?](https://generaltranslation.com/en-GB/kb/translate-conditional-jsx.mdx)
- Next question: [What is the best localisation software for developers?](https://generaltranslation.com/en-GB/kb/best-localization-software.mdx)

## Sitemap

See the [Knowledge Base sitemap](https://generaltranslation.com/en-GB/kb/sitemap.md) for every article.
