internal/templates/sitemap.go
169 Zeilen · 4.2 KB · Go
| 1 | package templates |
| 2 | |
| 3 | import ( |
| 4 | "encoding/xml" |
| 5 | "io" |
| 6 | "net/url" |
| 7 | "sort" |
| 8 | "strconv" |
| 9 | "strings" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/ruedigerp/newblog/internal/content" |
| 13 | ) |
| 14 | |
| 15 | const sitemapNS = "http://www.sitemaps.org/schemas/sitemap/0.9" |
| 16 | |
| 17 | type sitemapURL struct { |
| 18 | XMLName xml.Name `xml:"url"` |
| 19 | Loc string `xml:"loc"` |
| 20 | LastMod string `xml:"lastmod,omitempty"` |
| 21 | ChangeFreq string `xml:"changefreq,omitempty"` |
| 22 | Priority string `xml:"priority,omitempty"` |
| 23 | } |
| 24 | |
| 25 | type sitemapURLSet struct { |
| 26 | XMLName xml.Name `xml:"urlset"` |
| 27 | Xmlns string `xml:"xmlns,attr"` |
| 28 | URLs []sitemapURL `xml:"url"` |
| 29 | } |
| 30 | |
| 31 | // RenderSitemap writes a sitemaps.org 0.9 XML sitemap covering the homepage, |
| 32 | // its pagination pages, all posts, all static pages and the tag/category |
| 33 | // listings — i.e. every URL the generator produces as HTML. |
| 34 | func RenderSitemap(w io.Writer, site SiteData, posts []*content.Post, pages []*content.Page, postsPerPage int) error { |
| 35 | base := strings.TrimRight(site.BaseURL, "/") |
| 36 | |
| 37 | // Newest post date doubles as lastmod for the index pages. |
| 38 | siteMod := "" |
| 39 | if len(posts) > 0 { |
| 40 | siteMod = sitemapDate(postModTime(posts[0])) |
| 41 | } |
| 42 | |
| 43 | urls := []sitemapURL{{ |
| 44 | Loc: base + "/", |
| 45 | LastMod: siteMod, |
| 46 | ChangeFreq: "daily", |
| 47 | Priority: "1.0", |
| 48 | }} |
| 49 | |
| 50 | // Pagination: /page/2/, /page/3/, ... (page 1 is the homepage) |
| 51 | if postsPerPage > 0 { |
| 52 | totalPages := (len(posts) + postsPerPage - 1) / postsPerPage |
| 53 | for page := 2; page <= totalPages; page++ { |
| 54 | urls = append(urls, sitemapURL{ |
| 55 | Loc: base + "/page/" + strconv.Itoa(page) + "/", |
| 56 | LastMod: siteMod, |
| 57 | ChangeFreq: "weekly", |
| 58 | Priority: "0.4", |
| 59 | }) |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | // Posts |
| 64 | for _, p := range posts { |
| 65 | urls = append(urls, sitemapURL{ |
| 66 | Loc: base + "/posts/" + escapePath(p.Slug) + "/", |
| 67 | LastMod: sitemapDate(postModTime(p)), |
| 68 | ChangeFreq: "monthly", |
| 69 | Priority: "0.8", |
| 70 | }) |
| 71 | } |
| 72 | |
| 73 | // Static pages |
| 74 | for _, pg := range pages { |
| 75 | urls = append(urls, sitemapURL{ |
| 76 | Loc: base + "/sites/" + escapePath(pg.Slug) + "/", |
| 77 | ChangeFreq: "monthly", |
| 78 | Priority: "0.6", |
| 79 | }) |
| 80 | } |
| 81 | |
| 82 | // Tag and category listings |
| 83 | tagMap := content.CollectTags(posts) |
| 84 | catMap := content.CollectCategories(posts) |
| 85 | if len(tagMap) > 0 { |
| 86 | urls = append(urls, sitemapURL{ |
| 87 | Loc: base + "/tags/", |
| 88 | LastMod: siteMod, |
| 89 | ChangeFreq: "weekly", |
| 90 | Priority: "0.5", |
| 91 | }) |
| 92 | urls = append(urls, taxonomyURLs(base, "tags", tagMap)...) |
| 93 | } |
| 94 | if len(catMap) > 0 { |
| 95 | urls = append(urls, sitemapURL{ |
| 96 | Loc: base + "/categories/", |
| 97 | LastMod: siteMod, |
| 98 | ChangeFreq: "weekly", |
| 99 | Priority: "0.5", |
| 100 | }) |
| 101 | urls = append(urls, taxonomyURLs(base, "categories", catMap)...) |
| 102 | } |
| 103 | |
| 104 | set := sitemapURLSet{Xmlns: sitemapNS, URLs: urls} |
| 105 | |
| 106 | if _, err := w.Write([]byte(xml.Header)); err != nil { |
| 107 | return err |
| 108 | } |
| 109 | enc := xml.NewEncoder(w) |
| 110 | enc.Indent("", " ") |
| 111 | if err := enc.Encode(set); err != nil { |
| 112 | return err |
| 113 | } |
| 114 | _, err := w.Write([]byte("\n")) |
| 115 | return err |
| 116 | } |
| 117 | |
| 118 | // taxonomyURLs builds the per-tag / per-category URLs, sorted for stable output. |
| 119 | func taxonomyURLs(base, prefix string, m map[string][]*content.Post) []sitemapURL { |
| 120 | names := make([]string, 0, len(m)) |
| 121 | for name := range m { |
| 122 | names = append(names, name) |
| 123 | } |
| 124 | sort.Strings(names) |
| 125 | |
| 126 | out := make([]sitemapURL, 0, len(names)) |
| 127 | for _, name := range names { |
| 128 | lastMod := "" |
| 129 | for _, p := range m[name] { |
| 130 | if d := sitemapDate(postModTime(p)); d > lastMod { |
| 131 | lastMod = d |
| 132 | } |
| 133 | } |
| 134 | out = append(out, sitemapURL{ |
| 135 | // Links in the themes use the lowercased name, same as the generator's dirs. |
| 136 | Loc: base + "/" + prefix + "/" + escapePath(strings.ToLower(name)) + "/", |
| 137 | LastMod: lastMod, |
| 138 | ChangeFreq: "weekly", |
| 139 | Priority: "0.4", |
| 140 | }) |
| 141 | } |
| 142 | return out |
| 143 | } |
| 144 | |
| 145 | // postModTime prefers the update date over the publication date. |
| 146 | func postModTime(p *content.Post) time.Time { |
| 147 | if !p.Update.Time.IsZero() { |
| 148 | return p.Update.Time |
| 149 | } |
| 150 | return p.Date.Time |
| 151 | } |
| 152 | |
| 153 | // sitemapDate formats a W3C date (YYYY-MM-DD); zero times yield "". |
| 154 | func sitemapDate(t time.Time) string { |
| 155 | if t.IsZero() { |
| 156 | return "" |
| 157 | } |
| 158 | return t.Format("2006-01-02") |
| 159 | } |
| 160 | |
| 161 | // escapePath percent-encodes each segment of a slug so tags with spaces or |
| 162 | // umlauts stay valid URLs. |
| 163 | func escapePath(slug string) string { |
| 164 | parts := strings.Split(slug, "/") |
| 165 | for i, part := range parts { |
| 166 | parts[i] = url.PathEscape(part) |
| 167 | } |
| 168 | return strings.Join(parts, "/") |
| 169 | } |