Site kurmak
Document'ler: sitemap'ler, feed'ler, robots.txt
HTML yerine bayt dönen route'lar — sitemap'ler, feed'ler, robots.txt, JSON — sayfalar gibi önbelleğe alınır ve geçersiz kılınır.
Bir sitenin sunduğu her şey sayfa değildir. Bir crawler /sitemap.xml ve
/robots.txt ister, bir feed okuyucu /feed.xml ister, bir yük dengeleyici
/healthz ister. collage'da bunların her biri bir document'tir: handler'ı bayt ve
bir içerik tipi dönen; şablonu, layout'u ve fragment'i olmayan bir route.
Bir document geri kalan her şeyi bir sayfayla paylaşır. Aynı router'da yaşar; bu
yüzden bir sayfayla çakışan bir yol, ikisinden ikincisi kaydedilirken
collage.ErrDuplicateRoute ile reddedilir. Aynı anahtar altında önbelleğe alınır,
aynı içerik hash'ine dayalı ETag'i ve 304 yanıtlarını alır, aynı üç stratejiyi
kullanır, bağımlılık etiketleri taşır ve aynı app.InvalidateTags çağrısıyla düşürülür.
robots := collage.NewDocument("robots", "text/plain; charset=utf-8").
WithPath("en", "/robots.txt").
WithHandler(func(context.Context, *collage.RenderContext) ([]byte, []string, error) {
return []byte("User-agent: *\nAllow: /\n"), nil, nil
}).
Static().
Build()
if err := app.RegisterDocument(robots); err != nil {
log.Fatal(err)
}
Neden sayfa değil
Bir sayfa HTML'dir ve html/template üzerinden render edilir. Kaçış kuralları
HTML'in kurallarıdır: XML için de JSON için de yanlıştır ve bunlarla üretilen bir feed,
tam da kaçışa en çok ihtiyaç duyan karakterlerde sessizce bozulur. Hata yoktur; yalnızca
bir crawler'ın reddettiği bir dosya vardır.
Bu yüzden bir document hiçbir şey render etmez. Gövdeyi formatı bilen bir encoder'la —
encoding/xml, encoding/json — ya da gerçekten bir şablon istiyorsanız
text/template ile üretin. Framework döndüğünüz şeyi bayt bayt sunar.
Builder
| Çağrı | Ne yapar |
|---|---|
collage.NewDocument(name, contentType) |
Builder'ı başlatır. İkisi de zorunludur. |
AtRoot(pattern) |
Document'e her locale'in dışında ulaşan URL pattern'i: locale yapılandırması ne olursa olsun önek yok. Sitenin kendi dosyaları için — /robots.txt, /llms.txt. v0.14.1'den itibaren. |
WithPath(locale, pattern) |
locale'de document'e ulaşan URL pattern'i. {param} segmentleri sayfalardaki gibi çalışır. Bir yer tutucu bütün bir segmenttir: /feeds/{category}/rss.xml geçerlidir, /feeds/{category}.xml ise kayıt sırasında collage.ErrInvalidPattern ile reddedilir (v0.11.0'dan itibaren). |
WithHandler(fn) |
Gövdeyi üreten fonksiyon. Zorunludur: bir document'in geri düşebileceği bir şablonu yoktur. |
Dynamic() |
Handler'ı her istekte çalıştırır. Varsayılan budur. |
Static() |
Bir kez çalıştırır, bir etiket geçersiz kılana kadar önbellekten sunar. |
Incremental(ttl) |
Önbellekten sunar, ttl geçtikten sonra yeniden çalıştırır. |
WithCacheParams(names...) |
Bir sayfada olduğu gibi, hangi query parametrelerinin önbellek anahtarına katıldığı. |
WithDependency(tags...) |
Bu document'ten gelen her yanıtın taşıdığı etiketler. |
WithRedirect(from, to, status) / WithPermanentRedirect(from, to) |
Buraya yönlendiren eski yollar. |
Build() / BuildErr() |
Document ve zincirin topladığı hatalar. |
Varsayılana dikkat edin. Strateji vermeyi unuttuğunuz bir sayfa yine bir sayfadır;
strateji vermeyi unuttuğunuz bir document ise handler'ını her istekte çalıştırır ve
statik dışa aktarmada atlanır. Sitemap'ler, feed'ler ve robots.txt neredeyse her
zaman Static() ya da Incremental(ttl) ister.
Hiç handler ayarlanmamışsa Build, collage.ErrNoDocumentHandler'ı kaydeder.
Builder'ın kaydettiği şey document'in üzerinde kalır ve BuildErr'i çağırmış olun ya
da olmayın RegisterDocument onu adıyla reddeder; bu yüzden kendiniz denetlemeniz
isteğe bağlıdır.
İçerik tipi
İçerik tipi document'i build ettiğinizde sabitlenir ve önbellekten ya da taze, her
yanıta olduğu gibi yazılır. Önbelleğe alınmış gövdeyle birlikte saklanmaz ve baytlardan
tahmin edilmez. Format gerektiriyorsa charset'i ekleyin:
text/plain; charset=utf-8, application/json, application/xml,
application/rss+xml.
Handler
type DocumentHandlerFunc func(ctx context.Context, rc *collage.RenderContext) (body []byte, tags []string, err error)
rc, bir data handler'ın aldığı *collage.RenderContext'in aynısıdır: yol
parametresi için rc.Param("slug"), çözümlenmiş locale için rc.Locale, istek için
rc.Request. Render edilen bir sayfa olmadığı için rc.Page nil'dir ve hoist
edilecek bir head yoktur.
Bir data handler'ın sahip olduğu iki şey burada da çalışır (v0.10.0'dan itibaren).
collage.Cached veri önbelleğini
sayfalarla paylaşır; böylece her yazı sayfasının zaten çektiği yazıları okuyan bir
feed, hiçbirini yeniden çekmez ve ona verilen etiketler document'inkilere katılır.
rc.Asset(path), mount edilmiş bir dosyanın içerik adresli URL'sidir — örneğin bir
web manifest'indeki bir ikon.
Döndüğünüz etiketler document'in WithDependency etiketleriyle birleştirilir. Bir
data handler'da olduğu gibi, bir hata da dönseniz okunurlar; böylece neye bağlı
olduğunu öğrenip ardından başarısız olan bir handler, onu neyin bayatlatacağını yine
de söylemiş olur.
Bilmeye değer üç kural var:
- nil hatayla boş bir gövde bir başarısızlıktır. 500 ile yanıtlanır ve
collage.ErrEmptyDocumentBodyolarak bildirilir; çünkü gövdeyi doldurmayı unutmuş bir handler'dan ayırt edilemez. Gerçekten boş olan bir document tek bir satır sonu döner. collage.ErrNotFound'u sarmalayan bir hata 404'tür. Diğer her şey 500'dür.- Bir panic yakalanır ve sıradan bir hataya dönüşür; böylece bozuk tek bir feed süreci çökertmez.
Bir document'in kendi zaman aşımı yoktur. Handler'ı Config.Template.Timeout
(varsayılan olarak beş saniye) altında çalışır — fragment'ler için varsayılan data
handler zaman aşımı olan ayarın aynısı. Sınır size verilen ctx'e uygulanır; bu
yüzden onu çağırdığınız her şeye aktarın.
Hatalar düz metindir
Başarısız olan bir document hiçbir zaman bir HTML hata sayfasıyla yanıt vermez:
sitemap.xml isteyen bir crawler'ın onunla işi yoktur. Buna document'in yanıt
vermediği bir metot için dönen 405 de dahildir (v0.11.0'dan itibaren düz metin).
Doğru status ile text/plain alır; production'da tek satırlık genel bir mesaj,
geliştirmede route adı ve hata zincirinin tamamı; ayrıca Cache-Control: no-store.
Formata özgü bir hata istiyorsanız — bir JSON {"error": "..."} — hatayı handler'ın
içinde ele alın ve o gövdeyi kendiniz dönün. Handler ne dönerse o sunulur.
Önbellekleme
Önbellekleme sayfasındaki her şey geçerlidir. Önbellek anahtarı yol,
locale, yol parametreleri ve query string'dir; önbellekten yalnızca GET ve HEAD
sunulur; Cache-Control header'ı stratejiyi izler.
Yazılarınızdan kurulan bir sitemap, bir yazı yayımlandığında düşürülmelidir; etiketler tam da bunun içindir:
// Declared by the sitemap, the feed, the blog index and every post page.
if err := app.InvalidateTags(ctx, "blog:posts"); err != nil {
log.Printf("invalidate: %v", err)
}
Plugin'ler de document'leri görür: collage.DocumentRenderedHook'u gerçekleyen bir
plugin, bir document'in gövdesini önbelleğe alınıp sunulmadan önce yeniden yazabilir —
örneğin bir minifier. Hiçbir şey render edilmediği için sayfa render hook'ları
(OnBeforeRender, OnAfterRender) tetiklenmez. Bkz.
Plugin yazmak.
Örnek: bir sitemap
Bir sitemap mutlak URL'leri listeler. Bunları sayfa adlarından app.URL ile kurun;
böylece bir sayfanın yolu değiştiğinde sitemap onu izler. Önlerine de sitenin
origin'ini koyun.
// origin is where the site is published. app.URL returns paths, and a sitemap
// needs absolute URLs.
const origin = "https://example.com"
type urlSet struct {
XMLName xml.Name `xml:"urlset"`
Xmlns string `xml:"xmlns,attr"`
URLs []sitemapURL `xml:"url"`
}
type sitemapURL struct {
Loc string `xml:"loc"`
LastMod string `xml:"lastmod,omitempty"`
}
func SitemapDocument(app *collage.App, posts *store.Posts) *collage.Document {
return collage.NewDocument("sitemap", "application/xml").
WithPath("en", "/sitemap.xml").
WithHandler(func(ctx context.Context, _ *collage.RenderContext) ([]byte, []string, error) {
set := urlSet{Xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9"}
for _, name := range []string{"home", "about", "blog"} {
path, err := app.URL(name, "", nil)
if err != nil {
return nil, nil, err
}
set.URLs = append(set.URLs, sitemapURL{Loc: origin + path})
}
list, err := posts.List(ctx)
if err != nil {
return nil, nil, fmt.Errorf("sitemap: list posts: %w", err)
}
for _, post := range list {
path, err := app.URL("blog-post", "", map[string]string{"slug": post.Slug})
if err != nil {
return nil, nil, err
}
set.URLs = append(set.URLs, sitemapURL{
Loc: origin + path,
LastMod: post.Updated.Format("2006-01-02"),
})
}
body, err := xml.MarshalIndent(set, "", " ")
if err != nil {
return nil, nil, fmt.Errorf("sitemap: marshal: %w", err)
}
return append([]byte(xml.Header), body...), []string{"blog:posts"}, nil
}).
Incremental(time.Hour).
Build()
}
Handler app'i closure içine alır ve app.URL'i build edildiğinde değil, çalıştığında
çağırır — o sırada her sayfa kaydedilmiştir. app.URL katıdır: var olmayan bir sayfa
adı ya da pattern'i doldurmayan parametreler, sitemap'inizde bozuk bir bağlantı değil,
bir hatadır. Birden çok locale'i olan bir site için onu her locale için bir kez, locale'i
ikinci argüman olarak vererek çağırın; sonuç locale önekini içerir.
Strateji Incremental(time.Hour)'dur ve handler blog:posts etiketini döner; böylece
sitemap en az saatte bir ve bir şey o etiketi geçersiz kıldığında hemen yeniden
kurulur.
Örnek: bir RSS feed'i
type rss struct {
XMLName xml.Name `xml:"rss"`
Version string `xml:"version,attr"`
Channel channel `xml:"channel"`
}
type channel struct {
Title string `xml:"title"`
Link string `xml:"link"`
Description string `xml:"description"`
Items []item `xml:"item"`
}
type item struct {
Title string `xml:"title"`
Link string `xml:"link"`
GUID string `xml:"guid"`
PubDate string `xml:"pubDate"`
Summary string `xml:"description"`
}
func FeedDocument(app *collage.App, posts *store.Posts) *collage.Document {
return collage.NewDocument("feed", "application/rss+xml").
WithPath("en", "/feed.xml").
WithHandler(func(ctx context.Context, _ *collage.RenderContext) ([]byte, []string, error) {
latest, err := posts.Latest(ctx, 20)
if err != nil {
return nil, nil, fmt.Errorf("feed: %w", err)
}
feed := rss{Version: "2.0", Channel: channel{
Title: "Example blog",
Link: origin + "/",
Description: "Posts from the example blog.",
}}
tags := []string{"blog:posts"}
for _, post := range latest {
path, err := app.URL("blog-post", "", map[string]string{"slug": post.Slug})
if err != nil {
return nil, nil, err
}
feed.Channel.Items = append(feed.Channel.Items, item{
Title: post.Title,
Link: origin + path,
GUID: origin + path,
PubDate: post.Published.Format(time.RFC1123Z),
Summary: post.Summary,
})
tags = append(tags, "post:"+post.Slug)
}
body, err := xml.MarshalIndent(feed, "", " ")
if err != nil {
return nil, nil, fmt.Errorf("feed: marshal: %w", err)
}
return append([]byte(xml.Header), body...), tags, nil
}).
Static().
Build()
}
encoding/xml başlıkları ve özetleri doğru şekilde kaçışlar; sayfa kullanmamanın
bütün nedeni de budur. Feed Static()'tir: yalnızca bir yazı değiştiğinde değişir ve
her yazının etiketi üzerindedir; bu yüzden feed'deki herhangi bir yazıyı düzenlemek
onu düşürür.
Okuyucuları layout'tan bir <link rel="alternate"> ile ona yönlendirin — bkz.
Head ve SEO.
Örnek: robots.txt
func RobotsDocument(app *collage.App) *collage.Document {
return collage.NewDocument("robots", "text/plain; charset=utf-8").
AtRoot("/robots.txt").
WithHandler(func(context.Context, *collage.RenderContext) ([]byte, []string, error) {
sitemap, err := app.URL("sitemap", "", nil)
if err != nil {
return nil, nil, err
}
body := "User-agent: *\n" +
"Disallow: /api/\n" +
"\n" +
"Sitemap: " + origin + sitemap + "\n"
return []byte(body), nil, nil
}).
Static().
Build()
}
robots.txt bir dilin değil sitenin dosyasıdır ve crawler'lar onu yalnızca kökte
arar; bu yüzden AtRoot ile kurulur: varsayılan locale'i /en/ altında sunulan bir
sitede bile tek adresi /robots.txt'dir.
app.URL sayfalar kadar document'ler için de çalışır; bu yüzden robots.txt
sitemap'i adıyla bulur. Aynı adı paylaşan bir sayfaya ve bir document'e o adla
bağlantı verilemez — app.URL hangisini kastettiğinizi tahmin etmeyi reddeder — bu
yüzden document'lere kendilerine ait adlar verin.
Birden çok locale'de document'ler
Bir document'in yolları, bir sayfanınkiler gibi locale'e göre anahtarlanır:
collage.NewDocument("feed", "application/rss+xml").
WithPath("en", "/feed.xml").
WithPath("tr", "/feed.xml").
WithHandler(feedHandler).
Static().
Build()
Pattern locale önekini asla tekrarlamaz. tr destekliyken /tr/feed.xml isteğinin
/tr'si routing'den önce çıkarılır ve istek tr kaydının /feed.xml'iyle eşleşir;
WithPath("tr", "/tr/feed.xml") yazarsanız ona yalnızca /tr/tr/feed.xml ile
ulaşılır. Handler rc.Locale'i okur ve locale önbellek anahtarının bir parçasıdır; bu
yüzden iki feed ayrı ayrı önbelleğe alınır. Bkz.
Bağlantılar ve locale'ler.
Statik dışa aktarma, her locale'in document'ini tıpkı bir sayfada olduğu gibi onu
sunan URL'ye yazar: en feed'ini feed.xml'e, tr feed'ini tr/feed.xml'e; yani
iki locale'deki tek bir pattern iki dosyadır.
PrefixDefault ile varsayılan
locale'in document'i de önek alır — /en/feed.xml, en/feed.xml'e yazılır — ve
/feed.xml oraya yönlendirilir. Her dil için ayrı sitemap'i olan bir site hepsini
robots.txt'de listeler; robots.txt istediği kadar sitemap adlandırabilir:
Sitemap: https://example.com/en/sitemap.xml
Sitemap: https://example.com/tr/sitemap.xml
AtRoot ile kurulan bir document hiçbir locale'de değildir: her yapılandırmada
öneksiz yolundadır, her dildeki bir sayfadan adıyla bağlantı verilebilir ve
rc.Locale varsayılan locale'e ayarlanmış olarak render edilir.
Statik dışa aktarmada document'ler
Statik dışa aktarma bir document'i kendi birebir yoluna yazar:
/sitemap.xml, dist/sitemap.xml/index.html değil dist/sitemap.xml olur; çünkü
/sitemap.xml isteyen bir crawler'a bir dizin gitmemelidir. Varsayılan dışında bir
locale'deki document, sunulduğu yer olan o locale'in öneki altına yazılır:
dist/tr/sitemap.xml.
Static()veIncremental(ttl)document'leri yazılır.Dynamic()olanlar atlanır ve raporda adlarıyla belirtilir; iskeletteki/healthz'indist/'te hiç görünmemesinin nedeni budur.- Boş bir gövde
collage.ErrEmptyDocumentBodyile reddedilir ve hiçbir dosya yazılmaz. {param}içeren bir pattern'in somut yollarını listelemek için birBuildOptions.DocumentPathProvidergerekir; yoksacollage.ErrDynamicPathUnresolvedile atlanır.WithCacheParamskullanan bir document — sayfalanmış bir feed — bir dosyanın query string'i olamayacağı için query string olmadan yazılır ve rapor, bir sayfada olduğu gibi, bununla ilgili uyarır.- Tek bir dosyaya çözümlenen iki yol — bir yolu iki kez dönen bir provider — bir kez
build edilir; geri kalanlar
collage.ErrDuplicateOutputPathile atlanır.
DocumentPathProvider, PathProvider'ın document'lerdeki karşılığıdır — ayrı bir
interface'tir, böylece sayfalar için yazılmış bir provider'ın değişmesi gerekmez:
// categoryFeeds expands "/feeds/{category}/rss.xml" into one path per category.
type categoryFeeds struct{ categories []string }
func (p categoryFeeds) Paths(_ context.Context, doc *collage.Document, locale string) ([]collage.PathInstance, error) {
if doc.Name != "category-feed" {
return nil, nil
}
var paths []collage.PathInstance
for _, category := range p.categories {
paths = append(paths, collage.PathInstance{
Path: "/feeds/" + category + "/rss.xml",
Params: map[string]string{"category": category},
})
}
return paths, nil
}
Genişlettiği document, yer tutucu bütün bir segment olacak şekilde kaydedilir —
ardındaki birebir rss.xml, dışa aktarılan dosyayı feeds/go/rss.xml yapan şeydir:
collage.NewDocument("category-feed", "application/rss+xml").
WithPath("en", "/feeds/{category}/rss.xml").
WithHandler(categoryFeedHandler).
Static().
Build()
builder, err := collage.NewBuilder(app, collage.BuildOptions{
OutDir: outDir,
PathProvider: postPaths{store},
DocumentPathProvider: categoryFeeds{categories},
})
Params, handler'ın rc.Param üzerinden okuduğu şeydir; canlı bir isteğin yakalayacağı
değerlerin aynısı.
Document'lerin yapmadıkları
- Şablon, fragment ya da slot yok. Varlık nedenleri de bu.
Rangeistekleri yok. Gövde bellekte kurulur ve bütün olarak sunulur. Ses, video ve büyük indirmeler mount edilmiş bir dosya sistemine aittir — bkz. Statik dosyalar.- Büyük hiçbir şey yok. Önbelleğe alınabilen bir document sayfa önbelleğinde tutulur; bu önbellek bayt sayısıyla değil kayıt sayısıyla sınırlanır, bu yüzden 50 MB'lık tek bir document binlerce sayfa kadar yer kaplar.
Bir document'in ifade edemediği her şey için — akış hâlinde bir yanıt, bir istek
gövdesi, GET dışındaki metotlar — bkz.
Middleware ve kendi API'niz.