LabCodeHub öffentliche Ansicht
Anmelden
Küpper / Quildrop öffentlich
Branch: main
Quildrop / internal / templates / sitemap.go
Verlauf Rohdaten
R Rüdiger Küpper fix: add sitemap and robots.txt
89f2d506 vor 14 Tagen
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
}