Skip to content

Commit 0d82a65

Browse files
authored
fix: include virtual pages in sitemap (#111)
GenerateSitemap now accepts []*model.VirtualPage alongside articles. Each virtual page with a non-empty URL is emitted as a <url><loc>...</loc></url> entry, positioned after the locale index pages and before article entries. This ensures plugin-generated pages like /bookshelf/ and /ja/bookshelf/ appear in sitemap.xml for SEO indexing. Update build.go call site to pass site.VirtualPages. Add TestGenerateSitemap_VirtualPages test.
1 parent c3146cf commit 0d82a65

3 files changed

Lines changed: 44 additions & 14 deletions

File tree

cmd/gohan/build.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ func runBuild(args []string) error {
217217
}
218218

219219
// Sitemap + feeds.
220-
if err := generator.GenerateSitemap(outDir, cfg.Site.BaseURL, processed, *cfg); err != nil {
220+
if err := generator.GenerateSitemap(outDir, cfg.Site.BaseURL, processed, site.VirtualPages, *cfg); err != nil {
221221
fmt.Fprintf(os.Stderr, "warn: sitemap: %v\n", err)
222222
}
223223
if err := generator.GenerateFeeds(outDir, cfg.Site.BaseURL, cfg.Site.Title, processed, *cfg); err != nil {

internal/generator/sitemap.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ import (
1111
"github.com/bmf-san/gohan/internal/model"
1212
)
1313

14-
// GenerateSitemap writes sitemap.xml to outDir, listing all article URLs.
14+
// GenerateSitemap writes sitemap.xml to outDir, listing all article URLs and
15+
// any virtual pages produced by SitePlugins (e.g. /bookshelf/).
1516
// When articles have Translations populated (i18n), xhtml:link hreflang
1617
// alternates are included for SEO.
1718
// Articles are sorted newest-first. baseURL must not have a trailing slash.
1819
// When cfg has I18n.Locales configured, the locale index pages (/ and /ja/
1920
// etc.) are prepended to the sitemap as important entry points.
20-
func GenerateSitemap(outDir, baseURL string, articles []*model.ProcessedArticle, cfg model.Config) error {
21+
func GenerateSitemap(outDir, baseURL string, articles []*model.ProcessedArticle, virtualPages []*model.VirtualPage, cfg model.Config) error {
2122
sorted := make([]*model.ProcessedArticle, len(articles))
2223
copy(sorted, articles)
2324
sort.Slice(sorted, func(i, j int) bool {
@@ -56,6 +57,16 @@ func GenerateSitemap(outDir, baseURL string, articles []*model.ProcessedArticle,
5657
}
5758
}
5859

60+
// Emit virtual page URLs (e.g. /bookshelf/, /ja/bookshelf/).
61+
for _, vp := range virtualPages {
62+
if vp.URL == "" {
63+
continue
64+
}
65+
buf.WriteString(" <url>\n")
66+
buf.WriteString(" <loc>" + html.EscapeString(baseURL+vp.URL) + "</loc>\n")
67+
buf.WriteString(" </url>\n")
68+
}
69+
5970
for _, a := range sorted {
6071
// Prefer pre-computed URL; fall back to slug-based path for single-lang sites.
6172
articleURL := a.URL

internal/generator/sitemap_feed_test.go

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func makeArticles() []*model.ProcessedArticle {
2222

2323
func TestGenerateSitemap_Valid(t *testing.T) {
2424
dir := t.TempDir()
25-
if err := GenerateSitemap(dir, "https://example.com", makeArticles(), model.Config{}); err != nil {
25+
if err := GenerateSitemap(dir, "https://example.com", makeArticles(), nil, model.Config{}); err != nil {
2626
t.Fatalf("GenerateSitemap: %v", err)
2727
}
2828
data, _ := os.ReadFile(filepath.Join(dir, "sitemap.xml"))
@@ -40,7 +40,7 @@ func TestGenerateSitemap_Valid(t *testing.T) {
4040

4141
func TestGenerateSitemap_Empty(t *testing.T) {
4242
dir := t.TempDir()
43-
if err := GenerateSitemap(dir, "https://example.com", nil, model.Config{}); err != nil {
43+
if err := GenerateSitemap(dir, "https://example.com", nil, nil, model.Config{}); err != nil {
4444
t.Fatalf("GenerateSitemap empty: %v", err)
4545
}
4646
data, _ := os.ReadFile(filepath.Join(dir, "sitemap.xml"))
@@ -51,7 +51,7 @@ func TestGenerateSitemap_Empty(t *testing.T) {
5151

5252
func TestGenerateSitemap_WellFormedXML(t *testing.T) {
5353
dir := t.TempDir()
54-
if err := GenerateSitemap(dir, "https://example.com", makeArticles(), model.Config{}); err != nil {
54+
if err := GenerateSitemap(dir, "https://example.com", makeArticles(), nil, model.Config{}); err != nil {
5555
t.Fatal(err)
5656
}
5757
data, _ := os.ReadFile(filepath.Join(dir, "sitemap.xml"))
@@ -120,6 +120,25 @@ func TestGenerateFeeds_SlugifiesTitle(t *testing.T) {
120120
}
121121
}
122122

123+
func TestGenerateSitemap_VirtualPages(t *testing.T) {
124+
dir := t.TempDir()
125+
vps := []*model.VirtualPage{
126+
{URL: "/bookshelf/"},
127+
{URL: "/ja/bookshelf/"},
128+
}
129+
if err := GenerateSitemap(dir, "https://example.com", nil, vps, model.Config{}); err != nil {
130+
t.Fatalf("GenerateSitemap: %v", err)
131+
}
132+
data, _ := os.ReadFile(filepath.Join(dir, "sitemap.xml"))
133+
s := string(data)
134+
if !strings.Contains(s, "https://example.com/bookshelf/") {
135+
t.Errorf("expected /bookshelf/ in sitemap:\n%s", s)
136+
}
137+
if !strings.Contains(s, "https://example.com/ja/bookshelf/") {
138+
t.Errorf("expected /ja/bookshelf/ in sitemap:\n%s", s)
139+
}
140+
}
141+
123142
func TestGenerateSitemap_LastmodFromField(t *testing.T) {
124143
date := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
125144
lastmod := time.Date(2026, 3, 15, 0, 0, 0, 0, time.UTC)
@@ -134,7 +153,7 @@ func TestGenerateSitemap_LastmodFromField(t *testing.T) {
134153
},
135154
}
136155
dir := t.TempDir()
137-
if err := GenerateSitemap(dir, "https://example.com", articles, model.Config{}); err != nil {
156+
if err := GenerateSitemap(dir, "https://example.com", articles, nil, model.Config{}); err != nil {
138157
t.Fatalf("GenerateSitemap: %v", err)
139158
}
140159
data, _ := os.ReadFile(filepath.Join(dir, "sitemap.xml"))
@@ -156,7 +175,7 @@ func TestGenerateSitemap_LastmodFallsBackToDate(t *testing.T) {
156175
},
157176
}
158177
dir := t.TempDir()
159-
if err := GenerateSitemap(dir, "https://example.com", articles, model.Config{}); err != nil {
178+
if err := GenerateSitemap(dir, "https://example.com", articles, nil, model.Config{}); err != nil {
160179
t.Fatalf("GenerateSitemap: %v", err)
161180
}
162181
data, _ := os.ReadFile(filepath.Join(dir, "sitemap.xml"))
@@ -182,7 +201,7 @@ func TestGenerateSitemap_HreflangAlternates(t *testing.T) {
182201
},
183202
}
184203
dir := t.TempDir()
185-
if err := GenerateSitemap(dir, "https://example.com", articles, model.Config{}); err != nil {
204+
if err := GenerateSitemap(dir, "https://example.com", articles, nil, model.Config{}); err != nil {
186205
t.Fatalf("GenerateSitemap: %v", err)
187206
}
188207
data, _ := os.ReadFile(filepath.Join(dir, "sitemap.xml"))
@@ -203,7 +222,7 @@ func TestGenerateSitemap_I18nIndexPages(t *testing.T) {
203222
cfg := model.Config{}
204223
cfg.I18n.DefaultLocale = "en"
205224
cfg.I18n.Locales = []string{"en", "ja"}
206-
if err := GenerateSitemap(dir, "https://example.com", nil, cfg); err != nil {
225+
if err := GenerateSitemap(dir, "https://example.com", nil, nil, cfg); err != nil {
207226
t.Fatalf("GenerateSitemap: %v", err)
208227
}
209228
data, _ := os.ReadFile(filepath.Join(dir, "sitemap.xml"))
@@ -224,7 +243,7 @@ func TestGenerateSitemap_UsesPrecomputedURL(t *testing.T) {
224243
URL: "/ja/posts/my-url/",
225244
},
226245
}
227-
if err := GenerateSitemap(dir, "https://example.com", articles, model.Config{}); err != nil {
246+
if err := GenerateSitemap(dir, "https://example.com", articles, nil, model.Config{}); err != nil {
228247
t.Fatalf("GenerateSitemap: %v", err)
229248
}
230249
data, _ := os.ReadFile(filepath.Join(dir, "sitemap.xml"))
@@ -303,7 +322,7 @@ func TestGenerateSitemap_XDefault_DefaultLocale(t *testing.T) {
303322
},
304323
}
305324
dir := t.TempDir()
306-
if err := GenerateSitemap(dir, "https://example.com", articles, cfg); err != nil {
325+
if err := GenerateSitemap(dir, "https://example.com", articles, nil, cfg); err != nil {
307326
t.Fatalf("GenerateSitemap: %v", err)
308327
}
309328
data, _ := os.ReadFile(filepath.Join(dir, "sitemap.xml"))
@@ -328,7 +347,7 @@ func TestGenerateSitemap_XDefault_NonDefaultLocale(t *testing.T) {
328347
},
329348
}
330349
dir := t.TempDir()
331-
if err := GenerateSitemap(dir, "https://example.com", articles, cfg); err != nil {
350+
if err := GenerateSitemap(dir, "https://example.com", articles, nil, cfg); err != nil {
332351
t.Fatalf("GenerateSitemap: %v", err)
333352
}
334353
data, _ := os.ReadFile(filepath.Join(dir, "sitemap.xml"))
@@ -352,7 +371,7 @@ func TestGenerateSitemap_XDefault_NotEmittedWithoutConfig(t *testing.T) {
352371
}
353372
dir := t.TempDir()
354373
// model.Config{} has an empty DefaultLocale
355-
if err := GenerateSitemap(dir, "https://example.com", articles, model.Config{}); err != nil {
374+
if err := GenerateSitemap(dir, "https://example.com", articles, nil, model.Config{}); err != nil {
356375
t.Fatalf("GenerateSitemap: %v", err)
357376
}
358377
data, _ := os.ReadFile(filepath.Join(dir, "sitemap.xml"))

0 commit comments

Comments
 (0)