0Pricing
Next.js 15 Fullstack (App Router + Server Actions) · Lekcja

Zlokalizowane metadane, mapy witryn i hreflang

Generuj tytuły, opisy i alternatywy hreflang dla każdej lokalizacji, aby zapewnić poprawne międzynarodowe SEO.

Zlokalizowane metadane, mapy witryn i hreflang to bezpłatna lekcja Next.js 15 Fullstack (App Router + Server Actions) na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Next.js 15 Fullstack (App Router + Server Actions), a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Next.js 15 Fullstack (App Router + Server Actions) zawiera 4 lekcji w sumie.

Części tej lekcji nie zostały jeszcze przetłumaczone i są wyświetlane po angielsku.

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.

Często zadawane pytania

Czy lekcja „Zlokalizowane metadane, mapy witryn i hreflang” jest bezpłatna?

Tak — pełny tekst „Zlokalizowane metadane, mapy witryn i hreflang” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Next.js 15 Fullstack (App Router + Server Actions), przejdź na CoddyKit PRO. Kurs Next.js 15 Fullstack (App Router + Server Actions) zawiera 4 lekcji w sumie.

Co nauczysz się w „Zlokalizowane metadane, mapy witryn i hreflang”?

Generuj tytuły, opisy i alternatywy hreflang dla każdej lokalizacji, aby zapewnić poprawne międzynarodowe SEO. Ćwiczysz Next.js 15 Fullstack (App Router + Server Actions) z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć Next.js 15 Fullstack (App Router + Server Actions)?

Nie wymagamy żadnego doświadczenia. Next.js 15 Fullstack (App Router + Server Actions) w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.

Ile czasu zajmuje lekcja „Zlokalizowane metadane, mapy witryn i hreflang”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji Next.js 15 Fullstack (App Router + Server Actions)?

Tak. Każda lekcja Next.js 15 Fullstack (App Router + Server Actions) zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Wykrywanie lokalizacji i routing podścieżek z Middleware
  2. Serwerowe ładowanie tłumaczeń i katalogi komunikatów
  3. Zlokalizowane metadane, mapy witryn i hreflang
  4. Formatowanie dat, liczb i liczby mnogiej według lokalizacji
← Powrót do Next.js 15 Fullstack (App Router + Server Actions)