neptaystudio
Projeye başla
Notlar/Engineering

İki dilli bir Next.js App Router sitesi kurmak: i18n, hreflang ve yapılandırılmış veri

9 Haziran 2026·12 dk okuma·Neptay Studio

İki dilli (TR/EN) Next.js App Router siteleri nasıl teslim ettiğimizin pratik bir anlatımı — locale yönlendirme, hreflang alternatifleri, locale başına JSON-LD ve her iki dili de birinci sınıf tutan küçük middleware hileleri.

Çoğu iki dilli site aslında tek dilli bir siteye sonradan yapıştırılmış bir çeviri katmanından ibaret. İngilizce sürüm önce çıkıyor, formları çalışan tek sürüm o oluyor ve arama motorları onu önce öğreniyor. Çevrilmiş baskı, çeviri belleğinden düşmüş bir JSON dosyası ve kimsenin test etmediği bir başlık menüsü.

Bizim kurmak istediğimiz bu değildi. neptay.com, aynı içerik katmanından İngilizce ve Türkçe olarak çalışıyor — locale farkındalıklı yönlendirme, metadata, sitemap, hreflang ve yapılandırılmış veriyle. Türkçe baskı sonradan eklenmiş bir çeviri değil; aynı sitenin başka bir sesi. Bu yazı işin mühendislik tarafını anlatıyor: Next.js App Router'da nasıl kurduğumuzu, middleware'a neyi koyduğumuzu, hreflang'in Google'ın güveneceği şekilde nasıl görünmesi gerektiğini ve ilerde işe yarayan küçük yapısal kararları.

URL alanının şekli

İki dilli bir Next.js sitesinin URL'lerini düzenlemenin temelde üç yolu var:

  1. 01Locale başına alt alan adı (en.example.com, tr.example.com). Güçlü ayrım ama TLS ve analitik yönetimi pahalı, küçük site için görsel olarak parçalayıcı.
  2. 02Locale başına ülke TLD'si (example.com, example.com.tr). Coğrafi hedefleme için mükemmel ama yalnızca gerçekten Türkiye odaklı bir iş yürütüyorsanız haklı.
  3. 03Yol başına locale ön eki (example.com/en/, example.com/tr/). Tek alan adı, tek deploy, tek analitik. Bizim kullandığımız ve çoğu küçük iki dilli sitenin kullanması gereken yapı.

Next.js App Router, locale ön ekini temiz şekilde yönetiyor çünkü yönlendirme dosya sisteminden geliyor. Bütün site app/[locale]/ altında yaşıyor ve her sayfa locale'i bir yol parametresi olarak alıyor. Özel durumlu bir anasayfa yok, 'çevrilmiş' ve 'çevrilmemiş' yollar arasında gizli bir ayrım yok — locale yalnızca bir segment daha.

Kullandığımız klasör düzeni

app/
  [locale]/
    layout.tsx       ← locale başına layout: nav, footer, hreflang metadata
    page.tsx         ← anasayfa
    services/page.tsx
    work/[slug]/page.tsx
    studio/page.tsx
    contact/page.tsx
    insights/[slug]/page.tsx
  layout.tsx         ← kök layout: html lang, global JSON-LD
  sitemap.ts
  robots.ts

Bilinçli olarak iki layout var. Kök layout, <html> elementini sahipleniyor ve Organization + WebSite JSON-LD'yi bir kez enjekte ediyor. Locale layout ise navigasyon, footer ve locale başına metadata'ya sahip. Bu ayrım <html lang="…"> özniteliğini dürüst tutuyor ve yapılandırılmış veriyi olması gerektiği gibi global tutuyor.

Middleware: doğru locale'i tespit etmek ve yönlendirmek

Birisi çıplak alan adına geldiğinde karar vermeniz gerek: nereye gider? Üç sinyal var — URL (açıkça /tr/ yazdılarsa), Accept-Language başlığı (tarayıcının istediği) ve cookie (en son seçtikleri). Kullandığımız kural sıkıcı ama işe yarıyor:

  1. 01URL'de zaten locale ön eki varsa ona saygı duy. Seçimi hatırlamak için cookie ayarla.
  2. 02Önceki ziyaretten cookie varsa, o locale'e yönlendir.
  3. 03Accept-Language başlığı desteklediğimiz bir locale'e eşleşiyorsa oraya yönlendir.
  4. 04Aksi halde varsayılan locale'e (İngilizce) düş.

Bunu middleware.ts'te yapıyoruz, layout'ta değil — daha hızlı, edge'de çalışıyor ve hydration uyumsuzluğunu önlüyor. Middleware ayrıca özel bir başlık (x-locale) ayarlıyor ve kök layout, URL henüz çözülmeden <html lang>'i doğru kurmak için bunu okuyor.

Her sayfa için locale başına metadata

App Router'daki her sayfa generateMetadata fonksiyonu export edebiliyor. İki dilli bir sitede o fonksiyonda üç şey olmalı:

  1. 01Mevcut locale'de başlık ve açıklamayı ayarla.
  2. 02Bu tam yolu işaret eden canonical URL ayarla.
  3. 03Bu sayfanın bulunduğu her locale için hreflang alternatifleri artı bir x-default ayarla.

Next.js bunu alternates.languages nesnesiyle son derece kolaylaştırıyor. İnsanların hreflang'de yaptığı iki yaygın hata:

  • Her alternatif geri işaret etmeli. /en/work, /tr/work'ü alternatif olarak listeliyorsa, /tr/work de /en/work'ü listelemeli. Eksik geri-referanslar Google'a tüm hreflang grubunu yok saydırıyor.
  • x-default bir locale değil. Hiçbir dile uymayan kullanıcılar için olan URL. Biz onu /en/work'e işaret ediyoruz çünkü İngilizce en geniş yedek seçeneğimiz — İngilizce daha önemli olduğu için değil.

Sitemap ve sitemap içinde hreflang

Metadata'daki sayfa başına hreflang işin yarısı. Google ayrıca sitemap'ten de hreflang okuyor ve sitemap daha güvenilir bir sinyal çünkü bütün siteyi tek seferde kapsıyor. App Router'ın yerleşik bir sitemap.ts deseni var; bizimki tekrarlı — bilinçli olarak. Arama motorları açık çiftleri crawl etmekten URL kalıplarını tahmin etmekten daha mutlu.

Yapılandırılmış veri: tek bir temel graf, sayfa özelinde uzantılar

Schema.org JSON-LD, Google'ın çok dilli bir siteyi anlamasının ikinci ayağı. Biz iki katmana bölüyoruz:

  1. 01Kök layout'un bir kez enjekte ettiği temel bir graf (Organization + WebSite). Sitenin kime ait olduğunu, logosunun nerede olduğunu ve hangi dillerde yayınlandığını beyan ediyor.
  2. 02Her sayfanın enjekte ettiği sayfa özelinde şemalar (CreativeWork, BreadcrumbList, Article, FAQPage). Bu küçük JSON-LD blokları o sayfanın ne olduğunu açıklıyor.

İkisinde de inLanguage o anki locale'e ayarlı. Bunu atlamayın — inLanguage olmadan Google dili sayfa metninden çıkarmak zorunda kalıyor; kısa sayfalar için yavaş ve hatalı.

Sözlük deseni

Çeviri metinleri tek bir tipli sözlükte yaşıyor, locale'e göre anahtarlanmış const map olarak export ediliyor. Modern tavsiye, next-intl ya da react-intl gibi bir framework kullanmak. Küçük bir site için tipli sözlük daha hızlı, daha küçük ve eksik çevirileri compile zamanında yakalıyor.

Bir Türkçe çeviri eksikse TypeScript şikayet ediyor. Bir anahtar İngilizce'ye eklenip Türkçe'ye eklenmediyse build başarısız oluyor. Bu, iki dilli bir sitede çok değerli — yayına çıkmanın tek yolu ikisini birlikte yürütmek.

Statik render, edge cache ve bunun SEO için neden önemli olduğu

Yukarıdakilerin hepsi tek sayfa yüklemede performans-nötr. Ölçekte fark yaratıyor. Her sayfayı statik render ederek (çalışma anında veritabanı sorgusu yok, istek başına çeviri çağrısı yok), her locale her sayfa cache'lenebilir, edge'de cache'lenmiş bir HTML dokümanı oluyor. İlk-byte süresi CDN'inizin verdiği değer — gezegenin neresinde olursa olsun genelde 30–80ms.

SEO için iki nedenle önemli: Core Web Vitals TTFB ve LCP'yi ağır puanlıyor ve Google'ın crawler'ının bütçesi var. Statik, edge'de cache'lenmiş bir sayfa crawler'a tek ucuz round-trip'e mal oluyor; sunucu-render edilmiş bir sayfa bunun on katına mal olabiliyor.

İki dilli bir site planlıyor ve mimariye ikinci bir bakış istiyorsanız — ya da bizden kurmamızı istiyorsanız — hello@neptay.com.

Konuşalım

Projenizi anlatın.

Kısa bir mesaj yeterli. 24 saat içinde dürüst bir yanıt veriyoruz — uygun olmasa bile.