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

Yerelleştirilmiş Üst Veriler, Site Haritaları ve hreflang

Doğru uluslararası SEO için her yerel ayara özel başlıklar, açıklamalar ve hreflang alternatifleri oluşturun.

Yerelleştirilmiş Üst Veriler, Site Haritaları ve hreflang, CoddyKit'te ücretsiz bir Next.js 15 Fullstack (App Router + Server Actions) dersidir. Bu, 4 dersinin 3. dersidir. Aşağıdan dersin tamamını ücretsiz okuyabilir, sonra tarayıcıda yerleşik kod editörü ve 7/24 yapay zeka koçu ile uygulamalı olarak pratik yapabilirsin. Bu, Next.js 15 Fullstack (App Router + Server Actions) öğrenme yolunun bir parçasıdır ve ilerlemeniz web ve CoddyKit uygulaması arasında senkronize olur. Next.js 15 Fullstack (App Router + Server Actions) kursu toplamda 4 dersten oluşur.

Bu dersin bazı bölümleri henüz çevrilmemiş olup İngilizce olarak gösterilmektedir.

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.

Sıkça Sorulan Sorular

“Yerelleştirilmiş Üst Veriler, Site Haritaları ve hreflang” dersi ücretsiz mi?

Evet — “Yerelleştirilmiş Üst Veriler, Site Haritaları ve hreflang” dersin tüm metni burada web'de ücretsiz olarak okunabilir. Etkileşimli olarak pratik yapmak (yerleşik kod editörü ve 7/24 yapay zeka koçu) ve Next.js 15 Fullstack (App Router + Server Actions) kursunun geri kalanını açmak için CoddyKit PRO'ya yükselt. Next.js 15 Fullstack (App Router + Server Actions) kursu toplamda 4 dersten oluşur.

“Yerelleştirilmiş Üst Veriler, Site Haritaları ve hreflang” dersinde ne öğreneceğim?

Doğru uluslararası SEO için her yerel ayara özel başlıklar, açıklamalar ve hreflang alternatifleri oluşturun. Next.js 15 Fullstack (App Router + Server Actions) ile uygulamalı kodu tarayıcıda doğrudan çalıştırarak pratik yaparsın ve 7/24 yapay zeka koçu dersi çalışırken sorularını yanıtlar.

Next.js 15 Fullstack (App Router + Server Actions) öğrenmeye başlamak için deneyim gerekli mi?

Önceden deneyim gerekmez. CoddyKit'te Next.js 15 Fullstack (App Router + Server Actions), başlangıçtan ileri seviyeye kadar yapılandırıldığı için buradan başlayabilir veya başından başlayıp kendi hızında ilerleme yapabilirsin. Bu, 4 dersinin 3. dersidir.

“Yerelleştirilmiş Üst Veriler, Site Haritaları ve hreflang” dersi ne kadar sürer?

Çoğu CoddyKit dersi yaklaşık 5–10 dakika sürer. Her biri kısa ve etkileşimli olduğu için sabit ilerleme yaparsın ve web ile uygulama arasında tam olarak bıraktığın yerden devam edebilirsin.

Bu Next.js 15 Fullstack (App Router + Server Actions) dersinde kod yazıp çalıştırabilir miyim?

Evet. Her Next.js 15 Fullstack (App Router + Server Actions) dersi yerleşik bir kod editörü içerir, bu sayede tarayıcıda gerçek kod yazıp çalıştırabilir ve anlık yapay zeka geri bildirimi alırsın — yerel kurulum gerekli değildir.

Bu kursun tüm dersleri

  1. Ara Katmanla Yerel Ayar Algılama ve Alt Yol Yönlendirme
  2. Sunucu Tarafında Çeviri Yükleme ve İleti Katalogları
  3. Yerelleştirilmiş Üst Veriler, Site Haritaları ve hreflang
  4. Tarihler, Sayılar ve Çoğulları Yerel Ayara Göre Biçimlendirme
← Next.js 15 Fullstack (App Router + Server Actions) Sayfasına Dön