fix: add sitemap and robots.txt
R
Rüdiger Küpper
<rpr@9it.de>
committete am 18.08.2026 04:17
89f2d506160faa065851bc8d95d24aca64dbd65b
| @@ -183,0 +184,41 @@ Automatisch generierter RSS 2.0 Feed unter `/index.xml` mit: | ||
| 184 | +### Sitemap |
|
| 185 | + | |
| 186 | +Parallel zum RSS-Feed wird eine `sitemap.xml` nach dem [sitemaps.org-Protokoll 0.9](https://www.sitemaps.org/protocol.html) erzeugt - unter `/sitemap.xml` sowohl im dynamischen Server als auch in der statischen Generierung. Enthalten sind: |
|
| 187 | + | |
| 188 | +- Startseite und alle Pagination-Seiten (`/page/2/`, ...) |
|
| 189 | +- Alle Blog-Posts (`lastmod` aus `update`, sonst `date`) |
|
| 190 | +- Alle statischen Seiten aus `sites/` |
|
| 191 | +- Tag- und Kategorie-Übersichten sowie alle einzelnen Tag-/Kategorie-Seiten |
|
| 192 | + | |
| 193 | +Die Sitemap lässt sich in der Config an- und abschalten (Default: aktiv): |
|
| 194 | + | |
| 195 | +```yaml |
|
| 196 | +sitemap: |
|
| 197 | +enabled: true # false schaltet /sitemap.xml komplett ab |
|
| 198 | +``` |
|
| 199 | + | |
| 200 | +### robots.txt |
|
| 201 | + | |
| 202 | +Ebenfalls automatisch erzeugt: eine `robots.txt` unter `/robots.txt`. Sie verweist automatisch auf die Sitemap, sobald diese aktiviert ist, und lässt sich in der Config konfigurieren (Default: aktiv): |
|
| 203 | + | |
| 204 | +```yaml |
|
| 205 | +robots: |
|
| 206 | +enabled: true |
|
| 207 | +userAgent: "*" # optional, Default "*" |
|
| 208 | +disallow: # optional |
|
| 209 | +- /search-index.json |
|
| 210 | +allow: # optional |
|
| 211 | +- / |
|
| 212 | +``` |
|
| 213 | + | |
| 214 | +Ohne eigene `allow`/`disallow`-Regeln wird `Allow: /` geschrieben, d.h. die ganze Seite ist freigegeben: |
|
| 215 | + | |
| 216 | +``` |
|
| 217 | +User-agent: * |
|
| 218 | +Allow: / |
|
| 219 | + | |
| 220 | +Sitemap: https://blog.kuepper.nrw/sitemap.xml |
|
| 221 | +``` |
|
| 222 | + | |
| 223 | +Ist `sitemap.enabled: false` oder `baseURL` leer, entfällt die `Sitemap:`-Zeile. |
|
| 224 | + | |
| @@ -237,0 +279 @@ quilldrop/ | ||
| 279 | +│ │ ├── robots.go # robots.txt Generator |
|
| @@ -241 +283,2 @@ quilldrop/ | ||
| 241 | -│ └── rss.go # RSS Feed Generator |
|
| 283 | +│ ├── rss.go # RSS Feed Generator |
|
| 284 | +│ └── sitemap.go # sitemap.xml Generator |
|
| @@ -359 +402 @@ menu: | ||
| 359 | -| `baseURL` | - | Basis-URL für RSS und absolute Links | |
|
| 402 | +| `baseURL` | - | Basis-URL für RSS, Sitemap und absolute Links | |
|
| @@ -368,0 +412,5 @@ menu: | ||
| 412 | +| `sitemap.enabled` | `true` | Erzeugt/serviert `/sitemap.xml` | |
|
| 413 | +| `robots.enabled` | `true` | Erzeugt/serviert `/robots.txt` (mit Sitemap-Verweis) | |
|
| 414 | +| `robots.userAgent` | `*` | User-agent-Zeile in der `robots.txt` | |
|
| 415 | +| `robots.allow` | `[]` | Zusätzliche `Allow:`-Regeln | |
|
| 416 | +| `robots.disallow` | `[]` | Zusätzliche `Disallow:`-Regeln | |
|
| @@ -452,0 +501,2 @@ Alle URLs verwenden konsequent Trailing Slashes, um serverseitige Redirects zu v | ||
| 501 | +| `/sitemap.xml` | Sitemap für Suchmaschinen (falls aktiviert) | |
|
| 502 | +| `/robots.txt` | Crawler-Regeln + Sitemap-Verweis (falls aktiviert) | |
| @@ -185,0 +186,41 @@ Automatically generated RSS 2.0 feed at `/index.xml` with: | ||
| 186 | +### Sitemap |
|
| 187 | + | |
| 188 | +Alongside the RSS feed, a `sitemap.xml` following the [sitemaps.org protocol 0.9](https://www.sitemaps.org/protocol.html) is produced — served at `/sitemap.xml` by the dynamic server and written by the static generator. It contains: |
|
| 189 | + | |
| 190 | +- Homepage and every pagination page (`/page/2/`, ...) |
|
| 191 | +- All blog posts (`lastmod` from `update`, falling back to `date`) |
|
| 192 | +- All static pages from `sites/` |
|
| 193 | +- Tag and category overviews plus every single tag/category page |
|
| 194 | + | |
| 195 | +The sitemap can be switched on and off in the config (default: on): |
|
| 196 | + | |
| 197 | +```yaml |
|
| 198 | +sitemap: |
|
| 199 | +enabled: true # false disables /sitemap.xml entirely |
|
| 200 | +``` |
|
| 201 | + | |
| 202 | +### robots.txt |
|
| 203 | + | |
| 204 | +A `robots.txt` is generated as well and served at `/robots.txt`. It references the sitemap automatically whenever sitemap generation is enabled, and is configurable (default: on): |
|
| 205 | + | |
| 206 | +```yaml |
|
| 207 | +robots: |
|
| 208 | +enabled: true |
|
| 209 | +userAgent: "*" # optional, defaults to "*" |
|
| 210 | +disallow: # optional |
|
| 211 | +- /search-index.json |
|
| 212 | +allow: # optional |
|
| 213 | +- / |
|
| 214 | +``` |
|
| 215 | + | |
| 216 | +Without custom `allow`/`disallow` rules an `Allow: /` is written, i.e. the whole site is crawlable: |
|
| 217 | + | |
| 218 | +``` |
|
| 219 | +User-agent: * |
|
| 220 | +Allow: / |
|
| 221 | + | |
| 222 | +Sitemap: https://blog.kuepper.nrw/sitemap.xml |
|
| 223 | +``` |
|
| 224 | + | |
| 225 | +If `sitemap.enabled: false` or `baseURL` is empty, the `Sitemap:` line is omitted. |
|
| 226 | + | |
| @@ -239,0 +281 @@ quilldrop/ | ||
| 281 | +│ │ ├── robots.go # robots.txt generator |
|
| @@ -243 +285,2 @@ quilldrop/ | ||
| 243 | -│ └── rss.go # RSS feed generator |
|
| 285 | +│ ├── rss.go # RSS feed generator |
|
| 286 | +│ └── sitemap.go # sitemap.xml generator |
|
| @@ -359 +402 @@ menu: | ||
| 359 | -| `baseURL` | — | Base URL for RSS and absolute links | |
|
| 402 | +| `baseURL` | — | Base URL for RSS, sitemap and absolute links | |
|
| @@ -368,0 +412,5 @@ menu: | ||
| 412 | +| `sitemap.enabled` | `true` | Generates/serves `/sitemap.xml` | |
|
| 413 | +| `robots.enabled` | `true` | Generates/serves `/robots.txt` (with sitemap reference) | |
|
| 414 | +| `robots.userAgent` | `*` | User-agent line in `robots.txt` | |
|
| 415 | +| `robots.allow` | `[]` | Additional `Allow:` rules | |
|
| 416 | +| `robots.disallow` | `[]` | Additional `Disallow:` rules | |
|
| @@ -452,0 +501,2 @@ All URLs consistently use trailing slashes to avoid server-side redirects: | ||
| 501 | +| `/sitemap.xml` | Sitemap for search engines (if enabled) | |
|
| 502 | +| `/robots.txt` | Crawler rules + sitemap reference (if enabled) | |
| @@ -15,0 +16,13 @@ theme: "ehc" | ||
| 16 | +# sitemap.xml für Suchmaschinen (Standard: true) |
|
| 17 | +sitemap: |
|
| 18 | +enabled: true |
|
| 19 | + | |
| 20 | +# robots.txt (Standard: true), verweist automatisch auf die Sitemap |
|
| 21 | +robots: |
|
| 22 | +enabled: true |
|
| 23 | +# userAgent: "*" |
|
| 24 | +# allow: |
|
| 25 | +# - / |
|
| 26 | +# disallow: |
|
| 27 | +# - /search-index.json |
|
| 28 | + |
| @@ -38,0 +39,40 @@ func (l LLMsConfig) FullOrDefault() bool { | ||
| 39 | +// SitemapConfig toggles generation/serving of sitemap.xml for search engines. |
|
| 40 | +// Defaults to true so behaviour without the block stays unchanged. |
|
| 41 | +type SitemapConfig struct { |
|
| 42 | +Enabled *bool `yaml:"enabled,omitempty"` // sitemap.xml |
|
| 43 | +} |
|
| 44 | + | |
| 45 | +// EnabledOrDefault returns whether sitemap.xml should be produced (default: true). |
|
| 46 | +func (s SitemapConfig) EnabledOrDefault() bool { |
|
| 47 | + if s.Enabled == nil { |
|
| 48 | +return true |
|
| 49 | +} |
|
| 50 | +return *s.Enabled |
|
| 51 | +} |
|
| 52 | + | |
| 53 | +// RobotsConfig toggles generation/serving of robots.txt and lets the site |
|
| 54 | +// define its own Allow/Disallow rules. Defaults to true so behaviour without |
|
| 55 | +// the block stays unchanged. |
|
| 56 | +type RobotsConfig struct { |
|
| 57 | +Enabled *bool `yaml:"enabled,omitempty"` // robots.txt |
|
| 58 | +UserAgent string `yaml:"userAgent,omitempty"` // default: "*" |
|
| 59 | +Allow []string `yaml:"allow,omitempty"` |
|
| 60 | +Disallow []string `yaml:"disallow,omitempty"` |
|
| 61 | +} |
|
| 62 | + | |
| 63 | +// EnabledOrDefault returns whether robots.txt should be produced (default: true). |
|
| 64 | +func (r RobotsConfig) EnabledOrDefault() bool { |
|
| 65 | + if r.Enabled == nil { |
|
| 66 | +return true |
|
| 67 | +} |
|
| 68 | +return *r.Enabled |
|
| 69 | +} |
|
| 70 | + | |
| 71 | +// UserAgentOrDefault returns the User-agent line's value (default: "*"). |
|
| 72 | +func (r RobotsConfig) UserAgentOrDefault() string { |
|
| 73 | + if r.UserAgent == "" { |
|
| 74 | +return "*" |
|
| 75 | +} |
|
| 76 | +return r.UserAgent |
|
| 77 | +} |
|
| 78 | + | |
| @@ -40,14 +80,16 @@ type Config struct { | ||
| 40 | -Title string `yaml:"title"` |
|
| 41 | -Description string `yaml:"description"` |
|
| 42 | -Author string `yaml:"author"` |
|
| 43 | -BaseURL string `yaml:"baseURL"` |
|
| 44 | -Port int `yaml:"port"` |
|
| 45 | -PostsPerPage int `yaml:"postsPerPage"` |
|
| 46 | -ContentDir string `yaml:"contentDir"` |
|
| 47 | -SitesDir string `yaml:"sitesDir"` |
|
| 48 | -StaticDir string `yaml:"staticDir"` |
|
| 49 | -OutputDir string `yaml:"outputDir"` |
|
| 50 | -ThemesDir string `yaml:"themesDir"` |
|
| 51 | -Theme string `yaml:"theme"` |
|
| 52 | -Menu []MenuItem `yaml:"menu"` |
|
| 53 | -LLMs LLMsConfig `yaml:"llms"` |
|
| 80 | +Title string `yaml:"title"` |
|
| 81 | +Description string `yaml:"description"` |
|
| 82 | +Author string `yaml:"author"` |
|
| 83 | +BaseURL string `yaml:"baseURL"` |
|
| 84 | +Port int `yaml:"port"` |
|
| 85 | +PostsPerPage int `yaml:"postsPerPage"` |
|
| 86 | +ContentDir string `yaml:"contentDir"` |
|
| 87 | +SitesDir string `yaml:"sitesDir"` |
|
| 88 | +StaticDir string `yaml:"staticDir"` |
|
| 89 | +OutputDir string `yaml:"outputDir"` |
|
| 90 | +ThemesDir string `yaml:"themesDir"` |
|
| 91 | +Theme string `yaml:"theme"` |
|
| 92 | +Menu []MenuItem `yaml:"menu"` |
|
| 93 | +LLMs LLMsConfig `yaml:"llms"` |
|
| 94 | +Sitemap SitemapConfig `yaml:"sitemap"` |
|
| 95 | +Robots RobotsConfig `yaml:"robots"` |
| @@ -144,0 +145,10 @@ func Generate(cfg *config.Config, posts []*content.Post, pages []*content.Page) | ||
| 145 | +// 8b. sitemap.xml for search engines |
|
| 146 | + if cfg.Sitemap.EnabledOrDefault() { |
|
| 147 | + writeTemplate(filepath.Join(out, "sitemap.xml"), func(f *os.File) error { |
|
| 148 | +return templates.RenderSitemap(f, site, posts, pages, cfg.PostsPerPage) |
|
| 149 | +}) |
|
| 150 | +} |
|
| 151 | + | |
| 152 | +// 8c. robots.txt (references the sitemap when enabled) |
|
| 153 | +GenerateRobotsTxt(cfg, out) |
|
| 154 | + | |
| @@ -169,2 +179,9 @@ func Generate(cfg *config.Config, posts []*content.Post, pages []*content.Page) | ||
| 169 | - log.Printf("Generated %d posts, %d pages, %d tag pages, %d category pages, RSS feed into %s/", |
|
| 170 | -len(posts), len(pages), len(tagMap), len(catMap), out) |
|
| 179 | +extras := "RSS feed" |
|
| 180 | + if cfg.Sitemap.EnabledOrDefault() { |
|
| 181 | +extras += ", sitemap" |
|
| 182 | +} |
|
| 183 | + if cfg.Robots.EnabledOrDefault() { |
|
| 184 | +extras += ", robots.txt" |
|
| 185 | +} |
|
| 186 | + log.Printf("Generated %d posts, %d pages, %d tag pages, %d category pages, %s into %s/", |
|
| 187 | +len(posts), len(pages), len(tagMap), len(catMap), extras, out) |
| @@ -0,0 +1,48 @@ | ||
| 1 | +package generator |
|
| 2 | + | |
| 3 | +import ( |
|
| 4 | +"io" |
|
| 5 | +"log" |
|
| 6 | +"os" |
|
| 7 | +"strings" |
|
| 8 | + | |
| 9 | +"github.com/ruedigerp/newblog/internal/config" |
|
| 10 | +) |
|
| 11 | + | |
| 12 | +// RenderRobotsTxt writes a robots.txt for the site. Without explicit rules the |
|
| 13 | +// whole site is allowed; the sitemap is referenced automatically whenever |
|
| 14 | +// sitemap generation is enabled. |
|
| 15 | +func RenderRobotsTxt(w io.Writer, cfg *config.Config) error { |
|
| 16 | + bw := &errWriter{w: w} |
|
| 17 | + | |
| 18 | + bw.printf("User-agent: %s\n", cfg.Robots.UserAgentOrDefault()) |
|
| 19 | + for _, rule := range cfg.Robots.Allow { |
|
| 20 | + bw.printf("Allow: %s\n", rule) |
|
| 21 | +} |
|
| 22 | + for _, rule := range cfg.Robots.Disallow { |
|
| 23 | + bw.printf("Disallow: %s\n", rule) |
|
| 24 | +} |
|
| 25 | +// A crawler needs at least one rule; allow everything when none are configured. |
|
| 26 | + if len(cfg.Robots.Allow) == 0 && len(cfg.Robots.Disallow) == 0 { |
|
| 27 | + bw.printf("Allow: /\n") |
|
| 28 | +} |
|
| 29 | + | |
| 30 | + if cfg.Sitemap.EnabledOrDefault() && cfg.BaseURL != "" { |
|
| 31 | + bw.printf("\nSitemap: %s/sitemap.xml\n", strings.TrimRight(cfg.BaseURL, "/")) |
|
| 32 | +} |
|
| 33 | + | |
| 34 | +return bw.err |
|
| 35 | +} |
|
| 36 | + | |
| 37 | +// GenerateRobotsTxt writes robots.txt into outDir, honoring cfg.Robots.Enabled |
|
| 38 | +// (defaults to true). |
|
| 39 | +func GenerateRobotsTxt(cfg *config.Config, outDir string) { |
|
| 40 | + if !cfg.Robots.EnabledOrDefault() { |
|
| 41 | +return |
|
| 42 | +} |
|
| 43 | + if err := writeFile(outDir+"/robots.txt", func(f *os.File) error { |
|
| 44 | +return RenderRobotsTxt(f, cfg) |
|
| 45 | + }); err != nil { |
|
| 46 | + log.Printf("Error writing robots.txt: %v", err) |
|
| 47 | +} |
|
| 48 | +} |
| @@ -70,0 +71,22 @@ func Start(cfg *config.Config, posts []*content.Post, pages []*content.Page) { | ||
| 71 | +// sitemap.xml — URL index for search engines |
|
| 72 | + if cfg.Sitemap.EnabledOrDefault() { |
|
| 73 | + mux.HandleFunc("/sitemap.xml", func(w http.ResponseWriter, r *http.Request) { |
|
| 74 | + w.Header().Set("Content-Type", "application/xml; charset=utf-8") |
|
| 75 | + if err := templates.RenderSitemap(w, site, posts, pages, cfg.PostsPerPage); err != nil { |
|
| 76 | + log.Printf("Error rendering sitemap: %v", err) |
|
| 77 | +http.Error(w, "Internal Server Error", 500) |
|
| 78 | +} |
|
| 79 | +}) |
|
| 80 | +} |
|
| 81 | + | |
| 82 | +// robots.txt — crawler rules + sitemap reference |
|
| 83 | + if cfg.Robots.EnabledOrDefault() { |
|
| 84 | + mux.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) { |
|
| 85 | + w.Header().Set("Content-Type", "text/plain; charset=utf-8") |
|
| 86 | + if err := generator.RenderRobotsTxt(w, cfg); err != nil { |
|
| 87 | + log.Printf("Error rendering robots.txt: %v", err) |
|
| 88 | +http.Error(w, "Internal Server Error", 500) |
|
| 89 | +} |
|
| 90 | +}) |
|
| 91 | +} |
|
| 92 | + |
| @@ -0,0 +1,169 @@ | ||
| 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 | +} |