Next.js 15 Fullstack (App Router + Server Actions) · Leçon

Métadonnées localisées, plans de site et hreflang

Générez pour chaque langue des titres, des descriptions et des variantes hreflang afin d’assurer un SEO international correct.

Leçon 3 sur 413 étapes

Métadonnées localisées, plans de site et hreflang est une leçon Next.js 15 Fullstack (App Router + Server Actions) gratuite sur CoddyKit. Ceci est la leçon 3 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage Next.js 15 Fullstack (App Router + Server Actions), et ta progression se synchronise sur le web et l'application CoddyKit. Le cours Next.js 15 Fullstack (App Router + Server Actions) comprend 4 leçons au total.

Certaines parties de cette leçon n'ont pas encore été traduites et s'affichent en anglais.

Why Localized Metadata Matters

When your Next.js 15 app serves multiple languages, search engines need two things to rank you correctly: per-locale metadata (titles and descriptions in the right language) and hreflang links that tell Google which URL serves which language/region.

Without these, Google may index only your default locale, show the wrong language in search results, or penalise you for duplicate content across /en/about, /fr/about, and /de/about.

  • Localized title/description — improves click-through rate in each market
  • hreflang alternates — prevents duplicate-content penalties and routes users to their locale
  • Canonical URL — confirms the preferred URL for each locale page

Next.js 15 App Router gives you a first-class generateMetadata API that makes all of this straightforward.

Project Setup: Locale Config and Dictionary Types

Before generating metadata, define your supported locales and a typed dictionary shape. A central i18n.ts config keeps everything consistent across metadata, routing, and sitemaps.

Conventions used in this lesson:

  • locales — array of BCP 47 tags (en, fr, de)
  • defaultLocale — the fallback locale
  • getDictionary — async loader that returns typed strings per locale
// lib/i18n.ts
export const locales = ['en', 'fr', 'de'] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = 'en';

export function isValidLocale(value: string): value is Locale {
  return (locales as readonly string[]).includes(value);
}

// Typed shape for SEO-related strings
export interface SeoStrings {
  title: string;
  description: string;
}

export interface Dictionary {
  home: SeoStrings;
  about: SeoStrings;
}

Building a Dictionary Loader

The dictionary loader dynamically imports a JSON file for each locale. Using dynamic import() keeps bundle sizes small — each locale's strings are only loaded when needed.

Store dictionaries in messages/ at the project root. The loader is called inside generateMetadata and inside Server Components, so it must be async.

// lib/getDictionary.ts
import type { Locale, Dictionary } from './i18n';

// Next.js caches the result of dynamic imports automatically in the
// App Router, so repeated calls in the same request are free.
const dictionaries: Record<string, () => Promise<Dictionary>> = {
  en: () => import('../messages/en.json').then((m) => m.default as Dictionary),
  fr: () => import('../messages/fr.json').then((m) => m.default as Dictionary),
  de: () => import('../messages/de.json').then((m) => m.default as Dictionary),
};

export async function getDictionary(locale: Locale): Promise<Dictionary> {
  const loader = dictionaries[locale];
  if (!loader) throw new Error(`No dictionary for locale: ${locale}`);
  return loader();
}

Sample Message Files

Each locale needs a JSON file with SEO strings. These are the raw strings that generateMetadata will consume. Keep them short and keyword-rich — they appear directly in <title> and <meta name="description"> tags.

// messages/en.json
{
  "home": {
    "title": "CoddyKit — Learn to Code",
    "description": "Master programming with interactive lessons on CoddyKit."
  },
  "about": {
    "title": "About CoddyKit",
    "description": "Learn who we are and our mission to make coding accessible."
  }
}

// messages/fr.json
{
  "home": {
    "title": "CoddyKit — Apprendre la Programmation",
    "description": "Maîtrisez la programmation avec des leçons interactives sur CoddyKit."
  },
  "about": {
    "title": "À propos de CoddyKit",
    "description": "Découvrez qui nous sommes et notre mission pour rendre le code accessible."
  }
}

generateMetadata with Per-Locale Title and Description

generateMetadata is an async function exported from a page.tsx or layout.tsx. Next.js 15 calls it at request time (or at build time for static routes) and injects the result into the <head>.

The params argument gives you the dynamic segment — in an [locale] route, params.locale contains the BCP 47 tag. Load the right dictionary, then return a Metadata object.

// app/[locale]/page.tsx
import type { Metadata } from 'next';
import { getDictionary } from '@/lib/getDictionary';
import { isValidLocale, defaultLocale, type Locale } from '@/lib/i18n';

type Props = {
  params: Promise<{ locale: string }>;
};

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { locale: rawLocale } = await params;
  const locale: Locale = isValidLocale(rawLocale) ? rawLocale : defaultLocale;
  const dict = await getDictionary(locale);

  return {
    title: dict.home.title,
    description: dict.home.description,
  };
}

export default async function HomePage({ params }: Props) {
  const { locale: rawLocale } = await params;
  const locale: Locale = isValidLocale(rawLocale) ? rawLocale : defaultLocale;
  const dict = await getDictionary(locale);
  return <h1>{dict.home.title}</h1>;
}

Adding hreflang Alternates to Metadata

The alternates key in Metadata maps directly to <link rel="alternate" hreflang="..."> tags. You must include:

  • One entry per locale with the full absolute URL
  • An x-default entry pointing at your default-locale URL (or a language selector page)

Next.js renders these as <link> tags inside <head> automatically — no manual template editing needed.

// app/[locale]/page.tsx  (generateMetadata, extended)
import type { Metadata } from 'next';
import { getDictionary } from '@/lib/getDictionary';
import { locales, isValidLocale, defaultLocale, type Locale } from '@/lib/i18n';

const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL ?? 'https://www.coddykit.com';

type Props = { params: Promise<{ locale: string }> };

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { locale: rawLocale } = await params;
  const locale: Locale = isValidLocale(rawLocale) ? rawLocale : defaultLocale;
  const dict = await getDictionary(locale);

  // Build one alternate entry per supported locale
  const languages: Record<string, string> = {};
  for (const loc of locales) {
    languages[loc] = `${BASE_URL}/${loc}`;
  }
  languages['x-default'] = `${BASE_URL}/${defaultLocale}`;

  return {
    title: dict.home.title,
    description: dict.home.description,
    alternates: {
      canonical: `${BASE_URL}/${locale}`,
      languages,
    },
  };
}

Sharing Alternate Logic Across Pages

Repeating the alternate-building logic in every generateMetadata function is error-prone. Extract it into a small helper that accepts the current path segment and returns the alternates object.

This helper can live in lib/metadata.ts and be imported anywhere in the app.

// lib/metadata.ts
import { locales, defaultLocale, type Locale } from './i18n';

const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL ?? 'https://www.coddykit.com';

/**
 * Builds the `alternates` block for Next.js Metadata.
 * @param path - path after the locale segment, e.g. '' for home, '/about' for about
 */
export function buildAlternates(path = '') {
  const languages: Record<string, string> = {};

  for (const locale of locales) {
    languages[locale] = `${BASE_URL}/${locale}${path}`;
  }
  languages['x-default'] = `${BASE_URL}/${defaultLocale}${path}`;

  return { languages };
}

// Usage in app/[locale]/about/page.tsx:
// alternates: {
//   canonical: `${BASE_URL}/${locale}/about`,
//   ...buildAlternates('/about'),
// }

Root Layout: Default Metadata and OpenGraph Locale

The root layout.tsx under app/[locale]/ is the right place for default metadata that child pages can override. It is also where you set the <html lang> attribute and the OpenGraph locale field.

  • openGraph.locale uses underscore-separated BCP 47 tags (e.g. fr_FR)
  • openGraph.alternateLocale lists the other supported locales
// app/[locale]/layout.tsx
import type { Metadata } from 'next';
import { locales, isValidLocale, defaultLocale, type Locale } from '@/lib/i18n';
import { buildAlternates } from '@/lib/metadata';

const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL ?? 'https://www.coddykit.com';

type Props = {
  children: React.ReactNode;
  params: Promise<{ locale: string }>;
};

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { locale: rawLocale } = await params;
  const locale: Locale = isValidLocale(rawLocale) ? rawLocale : defaultLocale;
  const ogLocale = locale.replace('-', '_'); // 'fr' → 'fr', 'pt-BR' → 'pt_BR'
  const alternateLocales = locales.filter((l) => l !== locale).map((l) => l.replace('-', '_'));

  return {
    metadataBase: new URL(BASE_URL),
    alternates: {
      canonical: `${BASE_URL}/${locale}`,
      ...buildAlternates(),
    },
    openGraph: {
      locale: ogLocale,
      alternateLocale: alternateLocales,
    },
  };
}

export default async function LocaleLayout({ children, params }: Props) {
  const { locale: rawLocale } = await params;
  const locale: Locale = isValidLocale(rawLocale) ? rawLocale : defaultLocale;
  return (
    <html lang={locale}>
      <body>{children}</body>
    </html>
  );
}

generateStaticParams for All Locale Pages

For static or ISR pages, export generateStaticParams so Next.js pre-renders a version for every locale at build time. Without this, a fully-static export would only render the default locale.

This function pairs with generateMetadata — Next.js calls both for every combination produced by generateStaticParams.

// app/[locale]/about/page.tsx
import type { Metadata } from 'next';
import { locales, isValidLocale, defaultLocale, type Locale } from '@/lib/i18n';
import { getDictionary } from '@/lib/getDictionary';
import { buildAlternates } from '@/lib/metadata';

const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL ?? 'https://www.coddykit.com';

// Pre-render /en/about, /fr/about, /de/about at build time
export function generateStaticParams() {
  return locales.map((locale) => ({ locale }));
}

type Props = { params: Promise<{ locale: string }> };

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { locale: rawLocale } = await params;
  const locale: Locale = isValidLocale(rawLocale) ? rawLocale : defaultLocale;
  const dict = await getDictionary(locale);

  return {
    title: dict.about.title,
    description: dict.about.description,
    alternates: {
      canonical: `${BASE_URL}/${locale}/about`,
      ...buildAlternates('/about'),
    },
  };
}

export default async function AboutPage({ params }: Props) {
  const { locale: rawLocale } = await params;
  const locale: Locale = isValidLocale(rawLocale) ? rawLocale : defaultLocale;
  const dict = await getDictionary(locale);
  return <main><h1>{dict.about.title}</h1></main>;
}

Building a Localized Sitemap

Next.js 15 supports a special app/sitemap.ts file that returns an array of MetadataRoute.Sitemap entries. For an internationalized app, each page appears once per locale, and each entry carries an alternates.languages map — effectively embedding hreflang data inside the sitemap.

Google's Sitemap hreflang spec and the <link rel="alternate"> head tag approach are redundant — you only need one, but using both is safe and recommended for large sites.

// app/sitemap.ts
import type { MetadataRoute } from 'next';
import { locales, defaultLocale } from '@/lib/i18n';

const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL ?? 'https://www.coddykit.com';

// Static paths (add dynamic paths from your DB as needed)
const staticPaths = ['', '/about', '/courses'];

export default function sitemap(): MetadataRoute.Sitemap {
  const entries: MetadataRoute.Sitemap = [];

  for (const path of staticPaths) {
    for (const locale of locales) {
      const url = `${BASE_URL}/${locale}${path}`;

      // Build hreflang alternates for this path
      const languages: Record<string, string> = {};
      for (const loc of locales) {
        languages[loc] = `${BASE_URL}/${loc}${path}`;
      }
      languages['x-default'] = `${BASE_URL}/${defaultLocale}${path}`;

      entries.push({
        url,
        lastModified: new Date(),
        changeFrequency: 'weekly',
        priority: path === '' ? 1 : 0.8,
        alternates: { languages },
      });
    }
  }

  return entries;
}

Dynamic Sitemap Entries from a Database

Most production sites have dynamic pages — blog posts, course pages, product listings. Fetch those slugs inside sitemap.ts (it is a Server-only file) and generate localized entries for each one.

Keep the sitemap fast: fetch only IDs and slugs, not full content. If you have thousands of pages, use Next.js's sitemap splitting feature by exporting multiple sitemap-N.ts files or using generateSitemaps.

// app/sitemap.ts  (extended with dynamic course pages)
import type { MetadataRoute } from 'next';
import { locales, defaultLocale } from '@/lib/i18n';

const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL ?? 'https://www.coddykit.com';

async function getCourseSlugs(): Promise<string[]> {
  // Replace with your actual data-fetch (Supabase, fetch, ORM, etc.)
  const res = await fetch(`${BASE_URL}/api/courses/slugs`, {
    next: { revalidate: 3600 }, // revalidate every hour
  });
  const data: { slug: string }[] = await res.json();
  return data.map((c) => c.slug);
}

function buildEntry(path: string): MetadataRoute.Sitemap[number] {
  const languages: Record<string, string> = {};
  for (const loc of locales) {
    languages[loc] = `${BASE_URL}/${loc}${path}`;
  }
  languages['x-default'] = `${BASE_URL}/${defaultLocale}${path}`;
  return { url: `${BASE_URL}/${defaultLocale}${path}`, lastModified: new Date(),
           changeFrequency: 'weekly', priority: 0.7, alternates: { languages } };
}

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const courseSlugs = await getCourseSlugs();
  const coursePaths = courseSlugs.flatMap((slug) =>
    locales.map((locale) => ({ locale, path: `/courses/${slug}` }))
  );
  return [
    buildEntry(''),
    buildEntry('/about'),
    ...coursePaths.map(({ path }) => buildEntry(path)),
  ];
}

Which alternates field is correct for Next.js 15 hreflang?

Your Next.js 15 app supports English, French, and German. You want Google to show the French version at /fr/about when a French user searches. Which generateMetadata return value correctly implements hreflang for the French About page?

Recap: Localized Metadata, Sitemaps, and hreflang

In this lesson you built a complete internationalized SEO layer for a Next.js 15 App Router project. Here is what to remember:

  • Dictionary loader — dynamic imports per locale, typed with a shared Dictionary interface, cached automatically by Next.js
  • generateMetadata — async, receives params.locale, loads the right dictionary, returns title, description, alternates.canonical, and alternates.languages
  • hreflang alternates — always absolute URLs; include every supported locale plus x-default pointing at the default locale URL
  • OpenGraph locale — set in the root layout with underscore format (fr_FR) and list alternateLocale for other languages
  • generateStaticParams — pair with generateMetadata so every locale is pre-rendered at build time
  • sitemap.ts — use MetadataRoute.Sitemap with alternates.languages per entry; fetch dynamic slugs inside the file (it runs server-side)
  • buildAlternates helper — extract the languages-map logic into a shared utility to keep page files clean

With these pieces in place, each locale page carries correct metadata and cross-links, Google can index every language variant independently, and users land on the right locale every time.

Gratuit pour commencer

Apprends TypeScript avec un tuteur IA — gratuit

Écris et exécute du vrai code dans ton navigateur, obtiens de l'aide instantanée d'un tuteur IA disponible 24h/24, et reprends là où tu t'es arrêté sur le web ou dans l'app.

Cours
22
Leçons
88

Questions Fréquemment Posées

La leçon « Métadonnées localisées, plans de site et hreflang » est-elle gratuite ?

Oui — le texte complet de « Métadonnées localisées, plans de site et hreflang » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours Next.js 15 Fullstack (App Router + Server Actions), passe à CoddyKit PRO. Le cours Next.js 15 Fullstack (App Router + Server Actions) comprend 4 leçons au total.

Qu'est-ce que j'apprendrai dans « Métadonnées localisées, plans de site et hreflang » ?

Générez pour chaque langue des titres, des descriptions et des variantes hreflang afin d’assurer un SEO international correct. Tu pratiques Next.js 15 Fullstack (App Router + Server Actions) avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.

Dois-je avoir de l'expérience pour commencer Next.js 15 Fullstack (App Router + Server Actions) ?

Aucune expérience préalable n'est requise. Next.js 15 Fullstack (App Router + Server Actions) sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 3 sur 4.

Combien de temps prend la leçon « Métadonnées localisées, plans de site et hreflang » ?

La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.

Peux-tu écrire et exécuter du code dans cette leçon Next.js 15 Fullstack (App Router + Server Actions) ?

Oui. Chaque leçon Next.js 15 Fullstack (App Router + Server Actions) inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.

Toutes les leçons de ce cours

  1. Détection de la langue et routage par sous-chemin avec le middleware
  2. Chargement des traductions côté serveur et catalogues de messages
  3. Métadonnées localisées, plans de site et hreflang
  4. Formater les dates, les nombres et les pluriels selon la langue
← Retour à Next.js 15 Fullstack (App Router + Server Actions)