add tag listings, global and section-narrowed
One global namespace (ADR-0018): /tags/{term}/ spans every section and
/{section}/tags/{term}/ narrows it. Listings group by section so one busy term
stays readable, which needed List.Groups alongside Items — list.html renders
whichever is set.
This is Query's second use, so it gained a Tag field rather than being generalised
on speculation: one filter, two callers. Tag slugs lowercase and hyphenate,
preserving script, so "Long Monsoon" and "long monsoon" are one term while Bengali
passes through unchanged. Hand-chosen slugs per term still wait for the type
declaration that owns overrides.
`tags` is reserved at the top level and inside every section, alongside `page` and
the language prefixes. A tag listing redirects to its canonical URL only once it is
known to exist, matching the rule bundles already followed — otherwise a canonical
URL for nothing confirms what is not there.
One stale test expectation fixed rather than worked around: it asserted tags land
in Extra, which stopped being true when tags became a named field.
Evidence: /tags/monsoon/ lists Hello World under posts and First Rain under comics;
/comics/tags/monsoon/ shows one; /tags/monsoon 301s; /tags/nothing/ and /tags/ 404.
This commit is contained in:
+49
-12
@@ -57,6 +57,15 @@ type List struct {
|
||||
Page, Pages int
|
||||
// PrevURL and NextURL are empty at the ends. Newer is "prev" because the order is newest first.
|
||||
PrevURL, NextURL string
|
||||
// Groups is set instead of Items when entries are grouped — a tag listing groups by section, so one
|
||||
// busy term stays readable (ADR-0018).
|
||||
Groups []Group
|
||||
}
|
||||
|
||||
// Group is a named run of entries within a listing.
|
||||
type Group struct {
|
||||
Name string
|
||||
Items []Item
|
||||
}
|
||||
|
||||
// Item is one entry in a listing.
|
||||
@@ -171,6 +180,42 @@ func (r *Renderer) Bundle(b content.Bundle, served string, variants []string) ([
|
||||
|
||||
// Listing renders one page of a Query result for a section.
|
||||
func (r *Renderer) Listing(section, lang string, all []content.Bundle, page int) ([]byte, error) {
|
||||
l, window := r.paginate(section, lang, content.PageURL(section, lang, page), all, page,
|
||||
func(p int) string { return content.PageURL(section, lang, p) })
|
||||
for _, b := range window {
|
||||
l.Items = append(l.Items, r.item(b, lang))
|
||||
}
|
||||
return r.execute(r.list, l, section)
|
||||
}
|
||||
|
||||
// Tag renders one page of a tag listing, grouped by section.
|
||||
//
|
||||
// section narrows the listing to one section and is empty for the global one.
|
||||
func (r *Renderer) Tag(section, slug, lang string, all []content.Bundle, page int) ([]byte, error) {
|
||||
title := "#" + slug
|
||||
if section != "" {
|
||||
title = section + " · #" + slug
|
||||
}
|
||||
l, window := r.paginate(title, lang, content.TagURL(section, slug, lang, page), all, page,
|
||||
func(p int) string { return content.TagURL(section, slug, lang, p) })
|
||||
for _, b := range window {
|
||||
sec := b.Section()
|
||||
if n := len(l.Groups); n > 0 && l.Groups[n-1].Name == sec {
|
||||
l.Groups[n-1].Items = append(l.Groups[n-1].Items, r.item(b, lang))
|
||||
continue
|
||||
}
|
||||
l.Groups = append(l.Groups, Group{Name: sec, Items: []Item{r.item(b, lang)}})
|
||||
}
|
||||
return r.execute(r.list, l, "tag "+slug)
|
||||
}
|
||||
|
||||
// item is one listing entry.
|
||||
func (r *Renderer) item(b content.Bundle, lang string) Item {
|
||||
return Item{Title: b.Title, Key: b.Key, URL: content.URL(b.Key, lang), Date: b.Date}
|
||||
}
|
||||
|
||||
// paginate builds the shell of a listing page and returns the slice of entries it shows.
|
||||
func (r *Renderer) paginate(title, lang, canonical string, all []content.Bundle, page int, url func(int) string) (List, []content.Bundle) {
|
||||
pages := (len(all) + content.PerPage - 1) / content.PerPage
|
||||
if pages < 1 {
|
||||
pages = 1
|
||||
@@ -178,25 +223,17 @@ func (r *Renderer) Listing(section, lang string, all []content.Bundle, page int)
|
||||
start := (page - 1) * content.PerPage
|
||||
end := min(start+content.PerPage, len(all))
|
||||
l := List{
|
||||
head: head{
|
||||
Title: section,
|
||||
Lang: lang,
|
||||
Canonical: content.PageURL(section, lang, page),
|
||||
Style: r.style,
|
||||
},
|
||||
head: head{Title: title, Lang: lang, Canonical: canonical, Style: r.style},
|
||||
Page: page,
|
||||
Pages: pages,
|
||||
}
|
||||
for _, b := range all[start:end] {
|
||||
l.Items = append(l.Items, Item{Title: b.Title, Key: b.Key, URL: content.URL(b.Key, lang), Date: b.Date})
|
||||
}
|
||||
if page > 1 {
|
||||
l.PrevURL = content.PageURL(section, lang, page-1)
|
||||
l.PrevURL = url(page - 1)
|
||||
}
|
||||
if page < pages {
|
||||
l.NextURL = content.PageURL(section, lang, page+1)
|
||||
l.NextURL = url(page + 1)
|
||||
}
|
||||
return r.execute(r.list, l, section)
|
||||
return l, all[start:end]
|
||||
}
|
||||
|
||||
// execute runs a template set and wraps a failure with what was being rendered.
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
{{define "main" -}}
|
||||
<h1>{{.Title}}</h1>
|
||||
{{if .Items -}}
|
||||
{{if .Groups -}}
|
||||
{{- range .Groups}}
|
||||
<h2>{{.Name}}</h2>
|
||||
<ul class="listing">
|
||||
{{- range .Items}}
|
||||
<li><a href="{{.URL}}">{{if .Title}}{{.Title}}{{else}}{{.Key}}{{end}}</a>
|
||||
{{- if not .Date.IsZero}} <time datetime="{{.Date.Format "2006-01-02"}}">{{.Date.Format "2 January 2006"}}</time>{{end}}</li>
|
||||
{{- end}}
|
||||
</ul>
|
||||
{{- end}}
|
||||
{{- else if .Items -}}
|
||||
<ul class="listing">
|
||||
{{- range .Items}}
|
||||
<li><a href="{{.URL}}">{{if .Title}}{{.Title}}{{else}}{{.Key}}{{end}}</a>
|
||||
|
||||
Reference in New Issue
Block a user