Metadata Terlokalisasi, Peta Situs, dan hreflang
Hasilkan judul, deskripsi, dan alternatif hreflang per lokal untuk SEO internasional yang benar.
Metadata Terlokalisasi, Peta Situs, dan hreflang adalah pelajaran Next.js 15 Fullstack (App Router + Server Actions) gratis di CoddyKit. Ini adalah pelajaran 3 dari 4. Kamu bisa membaca pelajaran lengkapnya di bawah secara gratis — lalu praktikkan langsung di browser dengan editor kode bawaan dan tutor AI 24/7. Ini adalah bagian dari jalur belajar Next.js 15 Fullstack (App Router + Server Actions), dan progresmu tersinkronisasi di web dan aplikasi CoddyKit. Kursus Next.js 15 Fullstack (App Router + Server Actions) mencakup 4 pelajaran total.
Bagian dari pelajaran ini belum diterjemahkan dan ditampilkan dalam bahasa Inggris.
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 localegetDictionary— 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-defaultentry 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.localeuses underscore-separated BCP 47 tags (e.g.fr_FR)openGraph.alternateLocalelists 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
Dictionaryinterface, cached automatically by Next.js - generateMetadata — async, receives
params.locale, loads the right dictionary, returnstitle,description,alternates.canonical, andalternates.languages - hreflang alternates — always absolute URLs; include every supported locale plus
x-defaultpointing at the default locale URL - OpenGraph locale — set in the root layout with underscore format (
fr_FR) and listalternateLocalefor other languages - generateStaticParams — pair with
generateMetadataso every locale is pre-rendered at build time - sitemap.ts — use
MetadataRoute.Sitemapwithalternates.languagesper 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.
Belajar TypeScript dengan tutor AI — gratis
Tulis dan jalankan kode asli di browser kamu, dapatkan bantuan instan dari tutor AI 24/7, dan lanjutkan di mana kamu tinggalkan di web atau aplikasi.
- Kursus
- 22
- Pelajaran
- 88
Pertanyaan yang Sering Diajukan
Apakah pelajaran “Metadata Terlokalisasi, Peta Situs, dan hreflang” gratis?
Ya — teks lengkap “Metadata Terlokalisasi, Peta Situs, dan hreflang” gratis dibaca di sini di web. Untuk praktiknya secara interaktif (editor kode bawaan dan tutor AI 24/7) dan buka sisa kursus Next.js 15 Fullstack (App Router + Server Actions), upgrade ke CoddyKit PRO. Kursus Next.js 15 Fullstack (App Router + Server Actions) mencakup 4 pelajaran total.
Apa yang akan aku pelajari di “Metadata Terlokalisasi, Peta Situs, dan hreflang”?
Hasilkan judul, deskripsi, dan alternatif hreflang per lokal untuk SEO internasional yang benar. Kamu berlatih Next.js 15 Fullstack (App Router + Server Actions) dengan kode praktik yang langsung kamu jalankan di browser, dan tutor AI 24/7 menjawab pertanyaanmu saat kamu mengerjakan pelajaran ini.
Apakah aku perlu pengalaman untuk memulai Next.js 15 Fullstack (App Router + Server Actions)?
Tidak diperlukan pengalaman sebelumnya. Next.js 15 Fullstack (App Router + Server Actions) di CoddyKit dirancang untuk pemula hingga pelajar tingkat lanjut, jadi kamu bisa memulai di sini atau dari awal dan belajar sesuai kecepatan kamu sendiri. Ini adalah pelajaran 3 dari 4.
Berapa lama pelajaran “Metadata Terlokalisasi, Peta Situs, dan hreflang” memakan waktu?
Sebagian besar pelajaran CoddyKit memakan waktu sekitar 5–10 menit. Setiap pelajaran ringkas dan interaktif, jadi kamu membuat kemajuan stabil dan melanjutkan dari tempat kamu tinggalkan di web dan aplikasi.
Bisakah aku menulis dan menjalankan kode dalam pelajaran Next.js 15 Fullstack (App Router + Server Actions) ini?
Ya. Setiap pelajaran Next.js 15 Fullstack (App Router + Server Actions) menyertakan editor kode bawaan, jadi kamu menulis dan menjalankan kode nyata langsung di browser dan mendapatkan umpan balik AI instan — tidak diperlukan penyiapan lokal.
Semua pelajaran dalam kursus ini
- Deteksi Lokal dan Perutean Subjalur dengan Middleware
- Pemuatan Terjemahan Sisi Server dan Katalog Pesan
- Metadata Terlokalisasi, Peta Situs, dan hreflang
- Memformat Tanggal, Angka, dan Bentuk Jamak Berdasarkan Lokal