LabCodeHub öffentliche Ansicht
Anmelden
Küpper / Quildrop öffentlich
Branch: main
Quildrop / internal / server / server.go
Verlauf Rohdaten
R Rüdiger Küpper fix: add sitemap and robots.txt
89f2d506 vor 14 Tagen
internal/server/server.go 389 Zeilen · 11.5 KB · Go
1
package server
2
3
import (
4
	"encoding/json"
5
	"fmt"
6
	"log"
7
	"net/http"
8
	"os"
9
	"path"
10
	"path/filepath"
11
	"strconv"
12
	"strings"
13
14
	"github.com/ruedigerp/newblog/internal/config"
15
	"github.com/ruedigerp/newblog/internal/content"
16
	"github.com/ruedigerp/newblog/internal/generator"
17
	"github.com/ruedigerp/newblog/internal/templates"
18
)
19
20
func Start(cfg *config.Config, posts []*content.Post, pages []*content.Page) {
21
	if err := templates.Init(cfg.ThemeDir()); err != nil {
22
		log.Fatalf("Failed to init templates: %v", err)
23
	}
24
	log.Printf("Using theme %q from %s", cfg.Theme, cfg.ThemeDir())
25
26
	tagMap := content.CollectTags(posts)
27
	catMap := content.CollectCategories(posts)
28
	postMap := make(map[string]*content.Post)
29
	postIndex := make(map[string]int)
30
	for i, p := range posts {
31
		postMap[p.Slug] = p
32
		postIndex[p.Slug] = i
33
	}
34
	pageMap := make(map[string]*content.Page)
35
	for _, p := range pages {
36
		pageMap[p.Slug] = p
37
	}
38
39
	site := templates.SiteData{
40
		Title:       cfg.Title,
41
		Description: cfg.Description,
42
		Author:      cfg.Author,
43
		BaseURL:     cfg.BaseURL,
44
		Menu:        cfg.Menu,
45
	}
46
47
	mux := http.NewServeMux()
48
49
	// Static files: the site's static/ wins, the theme's static/ is the fallback.
50
	// Mirrors the merge order used by the generator.
51
	mux.Handle("/static/", http.StripPrefix("/static/",
52
		staticHandler(cfg.StaticDir, cfg.ThemeStaticDir())))
53
54
	// Images (served from static/images, so /images/... paths in posts work directly)
55
	mux.Handle("/images/", http.StripPrefix("/images/",
56
		http.FileServer(http.Dir(filepath.Join(cfg.StaticDir, "images")))))
57
58
	// RSS Feed
59
	mux.HandleFunc("/index.xml", func(w http.ResponseWriter, r *http.Request) {
60
		w.Header().Set("Content-Type", "application/rss+xml; charset=utf-8")
61
		rssCount := 20
62
		if rssCount > len(posts) {
63
			rssCount = len(posts)
64
		}
65
		if err := templates.RenderRSS(w, site, posts[:rssCount]); err != nil {
66
			log.Printf("Error rendering RSS: %v", err)
67
			http.Error(w, "Internal Server Error", 500)
68
		}
69
	})
70
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
93
	// llms.txt — concise index for LLMs (https://llmstxt.org/)
94
	if cfg.LLMs.EnabledOrDefault() {
95
		mux.HandleFunc("/llms.txt", func(w http.ResponseWriter, r *http.Request) {
96
			w.Header().Set("Content-Type", "text/markdown; charset=utf-8")
97
			if err := generator.RenderLLMsTxt(w, cfg, posts, pages); err != nil {
98
				log.Printf("Error rendering llms.txt: %v", err)
99
				http.Error(w, "Internal Server Error", 500)
100
			}
101
		})
102
	}
103
104
	// llms-full.txt — full markdown of every post
105
	if cfg.LLMs.FullOrDefault() {
106
		mux.HandleFunc("/llms-full.txt", func(w http.ResponseWriter, r *http.Request) {
107
			w.Header().Set("Content-Type", "text/markdown; charset=utf-8")
108
			if err := generator.RenderLLMsFullTxt(w, cfg, posts); err != nil {
109
				log.Printf("Error rendering llms-full.txt: %v", err)
110
				http.Error(w, "Internal Server Error", 500)
111
			}
112
		})
113
	}
114
115
	// Search index
116
	mux.HandleFunc("/search-index.json", func(w http.ResponseWriter, r *http.Request) {
117
		w.Header().Set("Content-Type", "application/json; charset=utf-8")
118
119
		months := []string{
120
			"", "Januar", "Februar", "März", "April", "Mai", "Juni",
121
			"Juli", "August", "September", "Oktober", "November", "Dezember",
122
		}
123
124
		type searchEntry struct {
125
			Title      string   `json:"title"`
126
			Slug       string   `json:"slug"`
127
			Date       string   `json:"date"`
128
			Tags       []string `json:"tags"`
129
			Categories []string `json:"categories"`
130
			Preview    string   `json:"preview"`
131
			Cover      string   `json:"cover"`
132
			URL        string   `json:"url"`
133
		}
134
135
		entries := make([]searchEntry, 0, len(posts))
136
		for _, p := range posts {
137
			d := p.Date.Time
138
			dateStr := d.Format("02.") + " " + months[d.Month()] + " " + d.Format("2006")
139
			tags := p.Tags
140
			if tags == nil {
141
				tags = []string{}
142
			}
143
			cats := p.Categories
144
			if cats == nil {
145
				cats = []string{}
146
			}
147
			entries = append(entries, searchEntry{
148
				Title:      p.Title,
149
				Slug:       p.Slug,
150
				Date:       dateStr,
151
				Tags:       tags,
152
				Categories: cats,
153
				Preview:    p.GetPreview(),
154
				Cover:      p.GetCover(),
155
				URL:        "/posts/" + p.Slug + "/",
156
			})
157
		}
158
159
		enc := json.NewEncoder(w)
160
		enc.SetIndent("", "  ")
161
		if err := enc.Encode(entries); err != nil {
162
			log.Printf("Error encoding search index: %v", err)
163
			http.Error(w, "Internal Server Error", 500)
164
		}
165
	})
166
167
	// Pagination helper
168
	perPage := cfg.PostsPerPage
169
	totalPages := (len(posts) + perPage - 1) / perPage
170
	if totalPages < 1 {
171
		totalPages = 1
172
	}
173
174
	renderHomePage := func(w http.ResponseWriter, r *http.Request, page int) {
175
		if page < 1 || page > totalPages {
176
			http.NotFound(w, r)
177
			return
178
		}
179
		start := (page - 1) * perPage
180
		end := start + perPage
181
		if end > len(posts) {
182
			end = len(posts)
183
		}
184
		data := templates.HomeData{
185
			Site:       site,
186
			Posts:      posts[start:end],
187
			Page:       page,
188
			TotalPages: totalPages,
189
		}
190
		if err := templates.RenderHome(w, data); err != nil {
191
			log.Printf("Error rendering home: %v", err)
192
			http.Error(w, "Internal Server Error", 500)
193
		}
194
	}
195
196
	// Homepage (page 1)
197
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
198
		if r.URL.Path != "/" {
199
			http.NotFound(w, r)
200
			return
201
		}
202
		renderHomePage(w, r, 1)
203
	})
204
205
	// Paginated pages: /page/2, /page/3, ...
206
	mux.HandleFunc("/page/", func(w http.ResponseWriter, r *http.Request) {
207
		numStr := strings.TrimPrefix(r.URL.Path, "/page/")
208
		numStr = strings.TrimSuffix(numStr, "/")
209
		page, err := strconv.Atoi(numStr)
210
		if err != nil || page < 1 {
211
			http.NotFound(w, r)
212
			return
213
		}
214
		if page == 1 {
215
			http.Redirect(w, r, "/", http.StatusMovedPermanently)
216
			return
217
		}
218
		renderHomePage(w, r, page)
219
	})
220
221
	// Individual posts
222
	mux.HandleFunc("/posts/", func(w http.ResponseWriter, r *http.Request) {
223
		slug := strings.TrimPrefix(r.URL.Path, "/posts/")
224
		slug = strings.TrimSuffix(slug, "/")
225
		post, ok := postMap[slug]
226
		if !ok {
227
			http.NotFound(w, r)
228
			return
229
		}
230
		data := templates.PostData{Site: site, Post: post}
231
		// Posts are sorted newest first: index 0 = newest
232
		idx := postIndex[slug]
233
		if idx > 0 {
234
			data.PrevPost = posts[idx-1] // newer
235
		}
236
		if idx < len(posts)-1 {
237
			data.NextPost = posts[idx+1] // older
238
		}
239
		if err := templates.RenderPost(w, data); err != nil {
240
			log.Printf("Error rendering post: %v", err)
241
			http.Error(w, "Internal Server Error", 500)
242
		}
243
	})
244
245
	// Static pages (sites)
246
	mux.HandleFunc("/sites/", func(w http.ResponseWriter, r *http.Request) {
247
		slug := strings.TrimPrefix(r.URL.Path, "/sites/")
248
		slug = strings.TrimSuffix(slug, "/")
249
		page, ok := pageMap[slug]
250
		if !ok {
251
			http.NotFound(w, r)
252
			return
253
		}
254
		data := templates.PageData{Site: site, Page: page}
255
		if err := templates.RenderPage(w, data); err != nil {
256
			log.Printf("Error rendering page: %v", err)
257
			http.Error(w, "Internal Server Error", 500)
258
		}
259
	})
260
261
	// Tags index + single tag
262
	renderTagsIndex := func(w http.ResponseWriter) {
263
		tagCounts := make(map[string]int)
264
		for tag, tagPosts := range tagMap {
265
			tagCounts[tag] = len(tagPosts)
266
		}
267
		data := templates.TagsData{Site: site, Tags: tagCounts}
268
		if err := templates.RenderTags(w, data); err != nil {
269
			log.Printf("Error rendering tags: %v", err)
270
			http.Error(w, "Internal Server Error", 500)
271
		}
272
	}
273
	mux.HandleFunc("/tags", func(w http.ResponseWriter, r *http.Request) {
274
		renderTagsIndex(w)
275
	})
276
	mux.HandleFunc("/tags/", func(w http.ResponseWriter, r *http.Request) {
277
		tag := strings.TrimPrefix(r.URL.Path, "/tags/")
278
		tag = strings.TrimSuffix(tag, "/")
279
		if tag == "" {
280
			renderTagsIndex(w)
281
			return
282
		}
283
		handleTag(w, r, site, tagMap)
284
	})
285
286
	// Categories index + single category
287
	renderCategoriesIndex := func(w http.ResponseWriter) {
288
		catCounts := make(map[string]int)
289
		for cat, catPosts := range catMap {
290
			catCounts[cat] = len(catPosts)
291
		}
292
		data := templates.CategoriesData{Site: site, Categories: catCounts}
293
		if err := templates.RenderCategories(w, data); err != nil {
294
			log.Printf("Error rendering categories: %v", err)
295
			http.Error(w, "Internal Server Error", 500)
296
		}
297
	}
298
	mux.HandleFunc("/categories", func(w http.ResponseWriter, r *http.Request) {
299
		renderCategoriesIndex(w)
300
	})
301
	mux.HandleFunc("/categories/", func(w http.ResponseWriter, r *http.Request) {
302
		cat := strings.TrimPrefix(r.URL.Path, "/categories/")
303
		cat = strings.TrimSuffix(cat, "/")
304
		if cat == "" {
305
			renderCategoriesIndex(w)
306
			return
307
		}
308
		handleCategory(w, r, site, catMap)
309
	})
310
311
	var handler http.Handler = mux
312
	if templates.DevMode() {
313
		// Templates are re-parsed per request; keep the browser from caching
314
		// the theme's CSS/JS so edits show up on reload.
315
		handler = noCache(mux)
316
		log.Printf("Dev mode: templates in %s are reloaded on every request", cfg.ThemeTemplatesDir())
317
	}
318
319
	addr := fmt.Sprintf(":%d", cfg.Port)
320
	log.Printf("Blog server starting on http://localhost%s", addr)
321
	log.Fatal(http.ListenAndServe(addr, handler))
322
}
323
324
// noCache disables browser caching — used in dev mode only.
325
func noCache(next http.Handler) http.Handler {
326
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
327
		w.Header().Set("Cache-Control", "no-store, max-age=0")
328
		next.ServeHTTP(w, r)
329
	})
330
}
331
332
// staticHandler serves static assets from several directories: the first
333
// directory that contains the requested file wins.
334
func staticHandler(dirs ...string) http.Handler {
335
	servers := make([]http.Handler, len(dirs))
336
	for i, d := range dirs {
337
		servers[i] = http.FileServer(http.Dir(d))
338
	}
339
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
340
		rel := filepath.FromSlash(path.Clean("/" + r.URL.Path))
341
		for i, d := range dirs {
342
			if info, err := os.Stat(filepath.Join(d, rel)); err == nil && !info.IsDir() {
343
				servers[i].ServeHTTP(w, r)
344
				return
345
			}
346
		}
347
		http.NotFound(w, r)
348
	})
349
}
350
351
func handleTag(w http.ResponseWriter, r *http.Request, site templates.SiteData, tagMap map[string][]*content.Post) {
352
	tag := strings.TrimPrefix(r.URL.Path, "/tags/")
353
	tag = strings.TrimSuffix(tag, "/")
354
	if tag == "" {
355
		http.Redirect(w, r, "/tags", http.StatusFound)
356
		return
357
	}
358
	for t, tagPosts := range tagMap {
359
		if strings.EqualFold(t, tag) {
360
			data := templates.TagData{Site: site, Tag: t, Posts: tagPosts}
361
			if err := templates.RenderTag(w, data); err != nil {
362
				log.Printf("Error rendering tag: %v", err)
363
				http.Error(w, "Internal Server Error", 500)
364
			}
365
			return
366
		}
367
	}
368
	http.NotFound(w, r)
369
}
370
371
func handleCategory(w http.ResponseWriter, r *http.Request, site templates.SiteData, catMap map[string][]*content.Post) {
372
	cat := strings.TrimPrefix(r.URL.Path, "/categories/")
373
	cat = strings.TrimSuffix(cat, "/")
374
	if cat == "" {
375
		http.Redirect(w, r, "/categories", http.StatusFound)
376
		return
377
	}
378
	for c, catPosts := range catMap {
379
		if strings.EqualFold(c, cat) {
380
			data := templates.CategoryData{Site: site, Category: c, Posts: catPosts}
381
			if err := templates.RenderCategory(w, data); err != nil {
382
				log.Printf("Error rendering category: %v", err)
383
				http.Error(w, "Internal Server Error", 500)
384
			}
385
			return
386
		}
387
	}
388
	http.NotFound(w, r)
389
}