İki dilli bir Next.js App Router sitesi kurmak: i18n, hreflang ve yapılandırılmış veri
İ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:
- 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ı.
- 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ı.
- 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.tsBilinç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:
- 01URL'de zaten locale ön eki varsa ona saygı duy. Seçimi hatırlamak için cookie ayarla.
- 02Önceki ziyaretten cookie varsa, o locale'e yönlendir.
- 03Accept-Language başlığı desteklediğimiz bir locale'e eşleşiyorsa oraya yönlendir.
- 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ı:
- 01Mevcut locale'de başlık ve açıklamayı ayarla.
- 02Bu tam yolu işaret eden canonical URL ayarla.
- 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:
- 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.
- 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.
1 saniye altında bir stüdyo sitesi: bağlı kaldığımız performans bütçesi
Orta düzey telefonda 4G üzerinden 1 saniyenin altında ilk içerik boyaması yapan stüdyo siteleri nasıl teslim ediyoruz — performans bütçesi ve Core Web Vitals için en kritik mimari kararlar.
Creator EconomyBir marka × yaratıcı işbirliğinin anatomisi: işler nasıl kapsamlanıyor, fiyatlanıyor ve ölçülüyor
Bir marka × yaratıcı işbirliğinin gerçekte nasıl yürüdüğüne dair derin bir anlatım — kaynak bulma, fiyatlandırma, sözleşmeler, yapım ve kampanya sonrası ölçüm. Türkiye pazarı dahil.