Site kurmak

Bağlantılar ve locale'ler

Bir siteyi birkaç dilde sunmak ve sayfalara adlarıyla bağlantı vererek bağlantıların onları her locale'de izlemesini sağlamak.

Bir collage sitesi her sayfayı birkaç dilde, her birini kendi URL'sinde sunabilir. collage locale'leri yönlendirir (route eder); çeviri yapmaz. Bir kelimenin Türkçede ne olması gerektiği içeriğinizde, o içerik zaten nerede duruyorsa orada yer alır — collage'ın payına düşen, hangi URL'nin hangi dile ait olduğunu bilmek ve bunu bilen bağlantılar kurmaktır.

Locale'leri yapılandırmak

app, err := collage.New(&collage.Config{
	Template: collage.TemplateConfig{Root: "templates"},
	Locale: collage.LocaleConfig{
		Default:   "en",
		Supported: []string{"en", "tr"},
	},
})

Default, locale öneki taşımayan bir URL'nin locale'idir ve varsayılan değeri "en"'dir. Supported sitenin sunduğu her locale'i listeler ve Default'u içermelidir (aksi hâlde collage.ErrLocaleDefaultNotSupported); boş bırakılırsa yalnızca Default'tan oluşur.

Ardından bir sayfa, var olduğu her locale'deki yolunu bildirir:

collage.NewPage("about").
	WithLayout(layout).
	WithContent(about).
	WithPath("en", "/about").
	WithPath("tr", "/hakkinda").
	Build()

Yollar buradaki gibi farklı olabilir ya da her locale'de aynı pattern olabilir. Yalnızca bir locale'de yolu olan bir sayfa yalnızca o locale'de vardır.

Locale'i URL belirler

Locale öneki olmayan bir yol Default locale'dedir. İlk segment'i desteklenen bir locale'i adlandıran bir yol o locale'dedir ve yol eşleştirilmeden önce önek kaldırılır:

URL Locale Eşleştirildiği yer
/about en en yollarındaki /about
/tr/hakkinda tr tr yollarındaki /hakkinda
/tr/about tr tr yollarındaki /about — bulunamaz, 404
/fr/about en /fr/about — fr desteklenmiyor, bu yüzden sıradan bir segment
/en/about — /about'a yönlendirilir
/TR/hakkinda — /tr/hakkinda'ya yönlendirilir

Her sayfanın locale başına tek bir URL'si vardır; bu yüzden bir önekin diğer yazımları kalıcı olarak o URL'ye yönlendirilir: varsayılan locale'in, kendi URL'lerinde bulunmayan öneki ve desteklenen bir locale'in farklı büyük/küçük harfle yazılışı. Yönlendirme GET ya da HEAD için 301, diğer her şey için 308'dir; böylece yanlış yazılışa gönderilen bir form GET'e dönüştürülmek yerine yeniden gönderilir ve query string de beraberinde gider. Bu yönlendirme olmasaydı /en/about, bir arama motoru için /about'un ikinci bir kopyası, önbellekte de ikinci bir girdi olurdu.

Varsayılan dil de dahil her dilin bir önek altında olmasını isteyen bir site PrefixDefault: true ayarlar (v0.14.0'dan itibaren). O zaman /tr/hakkinda'nın yanındaki sayfa /en/about olur ve öneksiz bir URL hiçbir dile ait değildir: önek olmadan istenen bir sayfa — /about ya da kök — varsayılan locale'in adresine, /en/about'a ve /en'e, kalıcı olarak yönlendirilir; adla kurulan her bağlantı da öneki taşır. Document'ler de aynı şekilde önek alır, /tr/sitemap.xml'in yanında /en/sitemap.xml; yalnızca sitenin kendi dosyaları, AtRoot ile kurulanlar, bunun dışındadır: /robots.txt kökte ve yalnızca oradadır (v0.14.1'den itibaren). Dışa aktarma, varsayılan locale'i en/ altına, köke de okuyucuyu /en/'e gönderen bir sayfa yazar.

Bir fragment sonucu rc.Locale olarak okur ve doğru içeriği çekmek için kullanır:

func aboutData(ctx context.Context, rc *collage.RenderContext) (aboutView, []string, error) {
	text, err := cms.Page(ctx, "about", rc.Locale)
	if err != nil {
		return aboutView{}, nil, err
	}
	return aboutView{Body: text}, []string{"page:about:" + rc.Locale}, nil
}

DisablePathLocale: true önekleri tamamen kapatır ve her isteği Default locale'de bırakır — tek dilli bir site ya da dili kendi başına belirleyen bir site için.

Neden yalnızca URL

collage asla Accept-Language header'ından ya da bir çerezden locale seçmez.

İçeriği kimin sorduğuna bağlı olan bir URL, birden çok içeriği olan tek bir URL'dir ve URL saklayan her şey onu yanlış anlar: bir önbellek ilk okuyucunun dilini herkese sunar, bir arama motoru bir sürümü dizine ekler ve diğerini hiç görmez, birinin paylaştığı bir bağlantı da onun göndermediği bir dilde açılır. Daha az belirgin bir şekilde de bozulur — /about'a giden bir bağlantıyı izleyen Türkçe bir tarayıcı, /about'un bulunmadığı Türkçe yollar arasında aranır ve bir 404 alır.

Locale URL'de olduğunda her URL, her okuyucu ve her önbellek için tek bir anlama gelir. Okuyucunun tercihlerini kullanmak isterseniz bunu yine de bilinçli olarak yapabilirsiniz — aşağıya bakın.

Yol olarak yazılmış bir bağlantı, yol değiştiğinde sessizce bozulur ve hangi locale'de olduğunu bilemez. Sayfanın kaydedildiği adla bağlantı verin:

<a href="{{pageURL "about"}}">About</a>
<a href="{{pageURL "blog-post" "slug" .Slug}}">{{.Title}}</a>
<link rel="alternate" type="application/rss+xml" href="{{pageURL "feed"}}">

pageURL önce sayfanın adını, ardından yol parametrelerini ad–değer çiftleri olarak alır. Sayfaların yanı sıra document'lar için de çalışır.

  • Render'ın locale'ini izler. /tr/hakkinda üzerinde {{pageURL "about"}} /tr/hakkinda'dır; /about üzerinde /about'tur. Önek sizin için eklenir.
  • Varsayılan locale'e geri düşer. Geçerli locale'de yolu olmayan bir sayfa için varsayılan locale'deki yola bağlantı verilir; böylece yalnızca İngilizcede var olan bir sayfaya bağlantı veren Türkçe bir sayfa yine de render edilir.
  • Katıdır. Bilinmeyen bir ad, eksik ya da boş bir parametre veya pattern'de karşılığı olan placeholder bulunmayan bir parametre render'ı başarısız kılar. Kurulamayan bir bağlantı, okuyucu için bir 404 değil, geliştirmede bulunacak bir hatadır.
  • Değerler escape edilir; . ya da .. değeri reddedilir.
  • Değerler dizedir. Bir sayıyı printf'ten geçirin: {{pageURL "user" "id" (printf "%d" .ID)}}.
  • TrailingSlash açıkken / ile biter — bir document'in değil, bir sayfanın bağlantısı: /about/, /tr/, ama /feed.xml.

pageURLIn önce locale'i alır ve tam olarak o locale'e bağlantı verir — geri düşme yoktur:

<a href="{{pageURLIn "tr" "about"}}" hreflang="tr">Hakkımızda</a>

O locale'de yolu olmayan bir sayfa render'ı başarısız kılar; sitenin desteklemediği bir locale de öyle.

Dil değiştirici

localeURL, render edilmekte olan sayfanın, aynı yol parametreleriyle, başka bir locale'deki hâlidir. Sayfanın o locale'de yolu yoksa boştur; bu yüzden with, sayfanın çevrilmediği bir dili atlar:

<nav class="languages">
  {{with localeURL "en"}}<a hreflang="en" href="{{.}}">English</a>{{end}}
  {{with localeURL "tr"}}<a hreflang="tr" href="{{.}}">Türkçe</a>{{end}}
</nav>

Onu layout'a koyun; her sayfa yalnızca var olanı sunan bir dil değiştiriciye sahip olur. Hiç desteklenmeyen bir locale ise yine hatadır — bu çevrilmemiş bir sayfa değil, şablondaki bir yazım hatasıdır.

Yol parametreleri olduğu gibi aktarılır. Her iki locale'de /blog/{slug} yolunda olan bir sayfa /blog/hello'dan /tr/blog/hello'ya geçer; Türkçe yazılarınızın Türkçe slug'ları varsa değiştirici bunu bilemez ve o bağlantıyı içeriğinizden kendiniz kurarsınız.

Arama motorlarına çevirileri bildirmek

Dil değiştirici okuyucular içindir. Arama motorları bir sayfanın çevirilerini, head'indeki <link rel="alternate" hreflang="…"> öğelerinden öğrenir; bunları her dil için bir tane olmak üzere rc.HoistAlternate(hreflang, href) (v0.10.0'dan itibaren) bildirir. Bunu, sayfanın var olduğu her locale için app.URL ile birlikte kullanın — Head ve SEO sayfasında bunu her sayfa için yapan bir layout var.

Go'da — adı verilen bir sayfaya yönlendiren bir action, her sayfayı listeleyen bir sitemap, bir sayfanın head'indeki alternatifler — app.URL kullanın:

target, err := app.URL("blog-post", "tr", map[string]string{"slug": post.Slug})
if err != nil {
	return nil, err
}
return collage.SeeOther(target), nil
func (a *App) URL(name, locale string, params map[string]string) (string, error)

Boş bir locale varsayılan locale demektir. pageURLIn kadar katıdır: bilinmeyen bir ad collage.ErrUnknownRoute, route'un yolu olmadığı bir locale collage.ErrNoPathInLocale, pattern'i tam olarak doldurmayan parametreler collage.ErrRouteParams, hiçbir URL'nin taşıyamayacağı bir locale — desteklenmeyen ya da DisablePathLocale açıkken varsayılan dışındaki herhangi biri — ise collage.ErrLocaleUnreachable hatasıdır. Sonuç, önek dahil bir yoldur; mutlak bir URL gerektiğinde sitenizin origin'ini ekleyin.

Dili kendiniz belirlemek

Okuyucunun tarayıcısına göre dil seçmek siteniz hakkında bir karardır; bu yüzden bu karar sizindir ve middleware'de verilir. Bunu yapmanın iki dürüst yolu var.

Varsayılan locale'in ana sayfasına Türkçe bir tarayıcıyla ya da size ait bir dil çereziyle gelen okuyucuyu /tr'ye yönlendirin. Her URL yine tek bir anlama gelir; yalnızca yeni bir okuyucunun nereden başlayacağını seçmiş olursunuz.

app.Use(func(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/" && strings.HasPrefix(r.Header.Get("Accept-Language"), "tr") {
			http.Redirect(w, r, "/tr", http.StatusFound)
			return
		}
		next.ServeHTTP(w, r)
	})
})

Tek bir URL'yi okuyucunun dilinde render edin. Kararı middleware'de verin, istek context'i üzerinden data handler'lara iletin ve önbelleğin dil başına bir kopya tutması için bunu collage'a collage.Vary ile bildirin:

type langKey struct{}

app.Use(func(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		lang := "en"
		if strings.HasPrefix(r.Header.Get("Accept-Language"), "tr") {
			lang = "tr"
		}
		collage.Vary(r, "Accept-Language", lang)
		next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), langKey{}, lang)))
	})
})
func pageData(ctx context.Context, rc *collage.RenderContext) (view, []string, error) {
	lang, _ := ctx.Value(langKey{}).(string)
	// ...
}

Vary, çözümlediğiniz değeri — tarayıcının header'ının tamamını değil, "tr"'yi — sayfanın önbellek anahtarına, header adını da yanıtın Vary header'ına koyar; böylece bir CDN de sürümleri birbirinden ayrı tutar. Middleware'den çağrılmalıdır. v0.11.0'dan itibaren Vary, routing başladığında her route'ta — önbelleğe alınsın ya da alınmasın — kapanır; bu yüzden bir data handler'dan ya da routing'den sonraki herhangi bir yerden yapılan çağrı hiçbir şeyi değiştirmez ve collage.ErrVaryTooLate döner. Bu, tek URL, tek içerik ilkesinin faydalarından vazgeçen yoldur; bu yüzden onu bilerek seçin — genellikle DisablePathLocale ile birlikte.

Bir statik dışa aktarma istek olmadan render eder, dolayısıyla onun için hiçbir middleware çalışmaz: dışa aktarılmış bir sitenin locale'leri yalnızca URL'lerinde olabilir.