LabCodeHub öffentliche Ansicht
Anmelden
Küpper / Quildrop öffentlich
Branch: main
Quildrop / internal / generator / generator.go
Verlauf Rohdaten
R Rüdiger Küpper fix: add sitemap and robots.txt
89f2d506 vor 14 Tagen
internal/generator/generator.go 229 Zeilen · 6.7 KB · Go
1
package generator
2
3
import (
4
	"fmt"
5
	"io/fs"
6
	"log"
7
	"os"
8
	"path/filepath"
9
	"strings"
10
11
	"github.com/ruedigerp/newblog/internal/config"
12
	"github.com/ruedigerp/newblog/internal/content"
13
	"github.com/ruedigerp/newblog/internal/templates"
14
)
15
16
func Generate(cfg *config.Config, posts []*content.Post, pages []*content.Page) {
17
	if err := templates.Init(cfg.ThemeDir()); err != nil {
18
		log.Fatalf("Failed to init templates: %v", err)
19
	}
20
	log.Printf("Using theme %q from %s", cfg.Theme, cfg.ThemeDir())
21
22
	out := cfg.OutputDir
23
24
	// Clean and recreate output directory
25
	os.RemoveAll(out)
26
	os.MkdirAll(out, 0755)
27
28
	site := templates.SiteData{
29
		Title:       cfg.Title,
30
		Description: cfg.Description,
31
		Author:      cfg.Author,
32
		BaseURL:     cfg.BaseURL,
33
		Menu:        cfg.Menu,
34
	}
35
36
	// 1. Homepage with pagination
37
	perPage := cfg.PostsPerPage
38
	totalPages := (len(posts) + perPage - 1) / perPage
39
	if totalPages < 1 {
40
		totalPages = 1
41
	}
42
	for page := 1; page <= totalPages; page++ {
43
		start := (page - 1) * perPage
44
		end := start + perPage
45
		if end > len(posts) {
46
			end = len(posts)
47
		}
48
		data := templates.HomeData{
49
			Site:       site,
50
			Posts:      posts[start:end],
51
			Page:       page,
52
			TotalPages: totalPages,
53
		}
54
		if page == 1 {
55
			writeTemplate(filepath.Join(out, "index.html"), func(f *os.File) error {
56
				return templates.RenderHome(f, data)
57
			})
58
		}
59
		// Also generate /page/N/ for all pages (including page 1 as redirect target)
60
		if page > 1 {
61
			dir := filepath.Join(out, "page", fmt.Sprintf("%d", page))
62
			os.MkdirAll(dir, 0755)
63
			pageCopy := data // capture for closure
64
			writeTemplate(filepath.Join(dir, "index.html"), func(f *os.File) error {
65
				return templates.RenderHome(f, pageCopy)
66
			})
67
		}
68
	}
69
70
	// 2. Individual posts
71
	for i, post := range posts {
72
		dir := filepath.Join(out, "posts", post.Slug)
73
		os.MkdirAll(dir, 0755)
74
		data := templates.PostData{Site: site, Post: post}
75
		// Posts are sorted newest first: index 0 = newest
76
		if i > 0 {
77
			data.PrevPost = posts[i-1] // newer
78
		}
79
		if i < len(posts)-1 {
80
			data.NextPost = posts[i+1] // older
81
		}
82
		writeTemplate(filepath.Join(dir, "index.html"), func(f *os.File) error {
83
			return templates.RenderPost(f, data)
84
		})
85
	}
86
87
	// 3. Tags index
88
	tagMap := content.CollectTags(posts)
89
	tagCounts := make(map[string]int)
90
	for tag, tagPosts := range tagMap {
91
		tagCounts[tag] = len(tagPosts)
92
	}
93
	os.MkdirAll(filepath.Join(out, "tags"), 0755)
94
	writeTemplate(filepath.Join(out, "tags", "index.html"), func(f *os.File) error {
95
		return templates.RenderTags(f, templates.TagsData{Site: site, Tags: tagCounts})
96
	})
97
98
	// 4. Per-tag pages
99
	for tag, tagPosts := range tagMap {
100
		dir := filepath.Join(out, "tags", strings.ToLower(tag))
101
		os.MkdirAll(dir, 0755)
102
		writeTemplate(filepath.Join(dir, "index.html"), func(f *os.File) error {
103
			return templates.RenderTag(f, templates.TagData{Site: site, Tag: tag, Posts: tagPosts})
104
		})
105
	}
106
107
	// 5. Categories index
108
	catMap := content.CollectCategories(posts)
109
	catCounts := make(map[string]int)
110
	for cat, catPosts := range catMap {
111
		catCounts[cat] = len(catPosts)
112
	}
113
	os.MkdirAll(filepath.Join(out, "categories"), 0755)
114
	writeTemplate(filepath.Join(out, "categories", "index.html"), func(f *os.File) error {
115
		return templates.RenderCategories(f, templates.CategoriesData{Site: site, Categories: catCounts})
116
	})
117
118
	// 6. Per-category pages
119
	for cat, catPosts := range catMap {
120
		dir := filepath.Join(out, "categories", strings.ToLower(cat))
121
		os.MkdirAll(dir, 0755)
122
		writeTemplate(filepath.Join(dir, "index.html"), func(f *os.File) error {
123
			return templates.RenderCategory(f, templates.CategoryData{Site: site, Category: cat, Posts: catPosts})
124
		})
125
	}
126
127
	// 7. Static pages (sites)
128
	for _, page := range pages {
129
		dir := filepath.Join(out, "sites", page.Slug)
130
		os.MkdirAll(dir, 0755)
131
		writeTemplate(filepath.Join(dir, "index.html"), func(f *os.File) error {
132
			return templates.RenderPage(f, templates.PageData{Site: site, Page: page})
133
		})
134
	}
135
136
	// 8. RSS Feed
137
	rssCount := 20
138
	if rssCount > len(posts) {
139
		rssCount = len(posts)
140
	}
141
	writeTemplate(filepath.Join(out, "index.xml"), func(f *os.File) error {
142
		return templates.RenderRSS(f, site, posts[:rssCount])
143
	})
144
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
155
	// 9. Search index
156
	GenerateSearchIndex(posts, filepath.Join(out, "search-index.json"))
157
158
	// 9b. llms.txt / llms-full.txt for LLM ingestion (https://llmstxt.org/)
159
	GenerateLLMsFiles(cfg, posts, pages, out)
160
161
	// 10. Copy static assets: theme assets first, site assets afterwards so
162
	// files in the site's static/ override the ones shipped with the theme.
163
	// (skip images/ subdirectory — handled separately)
164
	// Theme assets are copied as-is: a theme's own images belong to /static/images/.
165
	if _, err := os.Stat(cfg.ThemeStaticDir()); err == nil {
166
		copyStaticDir(cfg.ThemeStaticDir(), filepath.Join(out, "static"), nil)
167
	}
168
	copyStaticDir(cfg.StaticDir, filepath.Join(out, "static"), []string{"images", "videos"})
169
170
	// 11. Copy images + videos to root /images/ and /videos/ (cover paths use /images/...)
171
	for _, dir := range []string{"images", "videos"} {
172
		src := filepath.Join(cfg.StaticDir, dir)
173
		dst := filepath.Join(out, dir)
174
		if _, err := os.Stat(src); err == nil {
175
			copyStaticDir(src, dst, nil)
176
		}
177
	}
178
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)
188
}
189
190
func writeTemplate(path string, render func(f *os.File) error) {
191
	f, err := os.Create(path)
192
	if err != nil {
193
		log.Printf("Error creating %s: %v", path, err)
194
		return
195
	}
196
	defer f.Close()
197
	if err := render(f); err != nil {
198
		log.Printf("Error rendering %s: %v", path, err)
199
	}
200
}
201
202
func copyStaticDir(src, dst string, skipDirs []string) {
203
	skipSet := make(map[string]bool)
204
	for _, d := range skipDirs {
205
		skipSet[d] = true
206
	}
207
	filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
208
		if err != nil {
209
			return err
210
		}
211
		rel, _ := filepath.Rel(src, path)
212
		// Skip top-level directories in the skip list
213
		if d.IsDir() && rel != "." && skipSet[strings.Split(rel, string(filepath.Separator))[0]] {
214
			return filepath.SkipDir
215
		}
216
		target := filepath.Join(dst, rel)
217
		if d.IsDir() {
218
			os.MkdirAll(target, 0755)
219
			return nil
220
		}
221
		data, err := os.ReadFile(path)
222
		if err != nil {
223
			log.Printf("Error reading %s: %v", path, err)
224
			return nil
225
		}
226
		os.MkdirAll(filepath.Dir(target), 0755)
227
		return os.WriteFile(target, data, 0644)
228
	})
229
}