LabCodeHub öffentliche Ansicht
Anmelden
Küpper / Quildrop öffentlich

feat: add navigation for previous and next posts

R Rüdiger Küpper <rpr@9it.de> committete am 19.02.2026 02:21
d1bf374fa130243a8a3a18ff4545bbdd270e9448
5 geänderte Datei(en) +110 −6
Kontextzeilen: 0 1 2 3 10
geändert internal/generator/generator.go
+10 −2 Datei ansehen
@@ -60,25 +60,33 @@ func Generate(cfg *config.Config, posts []*content.Post, pages []*content.Page)
60 60
			dir := filepath.Join(out, "page", fmt.Sprintf("%d", page))
61 61
			os.MkdirAll(dir, 0755)
62 62
			pageCopy := data // capture for closure
63 63
			writeTemplate(filepath.Join(dir, "index.html"), func(f *os.File) error {
64 64
				return templates.RenderHome(f, pageCopy)
65 65
			})
66 66
		}
67 67
	}
68 68
69 69
	// 2. Individual posts
70 -
	for _, post := range posts {
70 +
	for i, post := range posts {
71 71
		dir := filepath.Join(out, "posts", post.Slug)
72 72
		os.MkdirAll(dir, 0755)
73 +
		data := templates.PostData{Site: site, Post: post}
74 +
		// Posts are sorted newest first: index 0 = newest
75 +
		if i > 0 {
76 +
			data.PrevPost = posts[i-1] // newer
77 +
		}
78 +
		if i < len(posts)-1 {
79 +
			data.NextPost = posts[i+1] // older
80 +
		}
73 81
		writeTemplate(filepath.Join(dir, "index.html"), func(f *os.File) error {
74 -
			return templates.RenderPost(f, templates.PostData{Site: site, Post: post})
82 +
			return templates.RenderPost(f, data)
75 83
		})
76 84
	}
77 85
78 86
	// 3. Tags index
79 87
	tagMap := content.CollectTags(posts)
80 88
	tagCounts := make(map[string]int)
81 89
	for tag, tagPosts := range tagMap {
82 90
		tagCounts[tag] = len(tagPosts)
83 91
	}
84 92
	os.MkdirAll(filepath.Join(out, "tags"), 0755)
geändert internal/server/server.go
+11 −1 Datei ansehen
@@ -14,22 +14,24 @@ import (
14 14
)
15 15
16 16
func Start(cfg *config.Config, posts []*content.Post, pages []*content.Page) {
17 17
	if err := templates.Init(); err != nil {
18 18
		log.Fatalf("Failed to init templates: %v", err)
19 19
	}
20 20
21 21
	tagMap := content.CollectTags(posts)
22 22
	catMap := content.CollectCategories(posts)
23 23
	postMap := make(map[string]*content.Post)
24 -
	for _, p := range posts {
24 +
	postIndex := make(map[string]int)
25 +
	for i, p := range posts {
25 26
		postMap[p.Slug] = p
27 +
		postIndex[p.Slug] = i
26 28
	}
27 29
	pageMap := make(map[string]*content.Page)
28 30
	for _, p := range pages {
29 31
		pageMap[p.Slug] = p
30 32
	}
31 33
32 34
	site := templates.SiteData{
33 35
		Title:       cfg.Title,
34 36
		Description: cfg.Description,
35 37
		Author:      cfg.Author,
@@ -167,20 +169,28 @@ func Start(cfg *config.Config, posts []*content.Post, pages []*content.Page) {
167 169
	// Individual posts
168 170
	mux.HandleFunc("/posts/", func(w http.ResponseWriter, r *http.Request) {
169 171
		slug := strings.TrimPrefix(r.URL.Path, "/posts/")
170 172
		slug = strings.TrimSuffix(slug, "/")
171 173
		post, ok := postMap[slug]
172 174
		if !ok {
173 175
			http.NotFound(w, r)
174 176
			return
175 177
		}
176 178
		data := templates.PostData{Site: site, Post: post}
179 +
		// Posts are sorted newest first: index 0 = newest
180 +
		idx := postIndex[slug]
181 +
		if idx > 0 {
182 +
			data.PrevPost = posts[idx-1] // newer
183 +
		}
184 +
		if idx < len(posts)-1 {
185 +
			data.NextPost = posts[idx+1] // older
186 +
		}
177 187
		if err := templates.RenderPost(w, data); err != nil {
178 188
			log.Printf("Error rendering post: %v", err)
179 189
			http.Error(w, "Internal Server Error", 500)
180 190
		}
181 191
	})
182 192
183 193
	// Static pages (sites)
184 194
	mux.HandleFunc("/sites/", func(w http.ResponseWriter, r *http.Request) {
185 195
		slug := strings.TrimPrefix(r.URL.Path, "/sites/")
186 196
		slug = strings.TrimSuffix(slug, "/")
geändert internal/templates/post.html
+18 −0 Datei ansehen
@@ -17,15 +17,33 @@
17 17
    </div>
18 18
    {{end}}
19 19
    {{if .Post.TOC}}
20 20
    <nav class="toc" id="toc">
21 21
        <h3>Inhaltsverzeichnis</h3>
22 22
    </nav>
23 23
    {{end}}
24 24
    <div class="post-content">
25 25
        {{safeHTML .Post.HTMLContent}}
26 26
    </div>
27 +
    <nav class="post-nav">
28 +
        {{if .PrevPost}}
29 +
        <a href="/posts/{{.PrevPost.Slug}}/" class="post-nav-link post-nav-prev">
30 +
            <span class="post-nav-label">&larr; Neuerer Artikel</span>
31 +
            <span class="post-nav-title">{{.PrevPost.Title}}</span>
32 +
        </a>
33 +
        {{else}}
34 +
        <span class="post-nav-link post-nav-prev disabled"></span>
35 +
        {{end}}
36 +
        {{if .NextPost}}
37 +
        <a href="/posts/{{.NextPost.Slug}}/" class="post-nav-link post-nav-next">
38 +
            <span class="post-nav-label">Älterer Artikel &rarr;</span>
39 +
            <span class="post-nav-title">{{.NextPost.Title}}</span>
40 +
        </a>
41 +
        {{else}}
42 +
        <span class="post-nav-link post-nav-next disabled"></span>
43 +
        {{end}}
44 +
    </nav>
27 45
    <div class="post-footer">
28 46
        <a href="/" class="back-link">&larr; Zurück zur Startseite</a>
29 47
    </div>
30 48
</article>
31 49
{{end}}
geändert internal/templates/render.go
+4 −2 Datei ansehen
@@ -24,22 +24,24 @@ type SiteData struct {
24 24
}
25 25
26 26
type HomeData struct {
27 27
	Site       SiteData
28 28
	Posts      []*content.Post
29 29
	Page       int // current page (1-based)
30 30
	TotalPages int
31 31
}
32 32
33 33
type PostData struct {
34 -
	Site SiteData
35 -
	Post *content.Post
34 +
	Site     SiteData
35 +
	Post     *content.Post
36 +
	PrevPost *content.Post // newer post (nil if this is the newest)
37 +
	NextPost *content.Post // older post (nil if this is the oldest)
36 38
}
37 39
38 40
type TagsData struct {
39 41
	Site SiteData
40 42
	Tags map[string]int
41 43
}
42 44
43 45
type TagData struct {
44 46
	Site  SiteData
45 47
	Tag   string
geändert static/css/style.css
+67 −1 Datei ansehen
@@ -894,26 +894,92 @@ body {
894 894
    font-weight: 600;
895 895
    color: var(--accent);
896 896
    background: var(--bg-card);
897 897
}
898 898
899 899
.post-content tr:hover td {
900 900
    background: var(--bg-card);
901 901
}
902 902
903 903
/* === Post Footer === */
904 -
.post-footer {
904 +
/* === Post Navigation (Prev/Next) === */
905 +
.post-nav {
906 +
    display: flex;
907 +
    gap: 1.5rem;
905 908
    margin-top: 3rem;
906 909
    padding-top: 1.5rem;
907 910
    border-top: 1px solid var(--border);
908 911
}
909 912
913 +
.post-nav-link {
914 +
    flex: 1;
915 +
    display: flex;
916 +
    flex-direction: column;
917 +
    gap: 0.3rem;
918 +
    padding: 1rem;
919 +
    border-radius: 10px;
920 +
    background: var(--bg-card);
921 +
    border: 1px solid var(--border);
922 +
    text-decoration: none;
923 +
    transition: all 0.2s ease;
924 +
}
925 +
926 +
.post-nav-link:hover {
927 +
    border-color: var(--accent);
928 +
    background: var(--bg-card-hover);
929 +
    box-shadow: 0 0 16px var(--accent-glow);
930 +
}
931 +
932 +
.post-nav-link.disabled {
933 +
    visibility: hidden;
934 +
}
935 +
936 +
.post-nav-prev {
937 +
    align-items: flex-start;
938 +
}
939 +
940 +
.post-nav-next {
941 +
    align-items: flex-end;
942 +
    text-align: right;
943 +
}
944 +
945 +
.post-nav-label {
946 +
    font-family: 'JetBrains Mono', monospace;
947 +
    font-size: 0.75rem;
948 +
    color: var(--text-muted);
949 +
    text-transform: uppercase;
950 +
    letter-spacing: 0.05em;
951 +
}
952 +
953 +
.post-nav-title {
954 +
    font-size: 0.9rem;
955 +
    font-weight: 600;
956 +
    color: var(--accent);
957 +
    line-height: 1.3;
958 +
}
959 +
960 +
@media (max-width: 640px) {
961 +
    .post-nav {
962 +
        flex-direction: column;
963 +
        gap: 0.75rem;
964 +
    }
965 +
    .post-nav-next {
966 +
        align-items: flex-start;
967 +
        text-align: left;
968 +
    }
969 +
}
970 +
971 +
.post-footer {
972 +
    margin-top: 1.5rem;
973 +
    padding-top: 1rem;
974 +
}
975 +
910 976
.back-link {
911 977
    font-family: 'JetBrains Mono', monospace;
912 978
    font-size: 0.85rem;
913 979
    color: var(--accent);
914 980
    text-decoration: none;
915 981
    transition: all 0.2s ease;
916 982
}
917 983
918 984
.back-link:hover {
919 985
    text-shadow: 0 0 12px var(--accent-glow-strong);