feat: add navigation for previous and next posts
R
Rüdiger Küpper
<rpr@9it.de>
committete am 19.02.2026 02:21
d1bf374fa130243a8a3a18ff4545bbdd270e9448
| @@ -68,9 +68,17 @@ func Generate(cfg *config.Config, posts []*content.Post, pages []*content.Page) | ||
| 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 | } |
| @@ -22,6 +22,8 @@ func Start(cfg *config.Config, posts []*content.Post, pages []*content.Page) { | ||
| 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) |
| @@ -175,4 +177,12 @@ func Start(cfg *config.Config, posts []*content.Post, pages []*content.Page) { | ||
| 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) |
| @@ -25,4 +25,22 @@ | ||
| 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">← 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 →</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">← Zurück zur Startseite</a> |
| @@ -32,6 +32,8 @@ type HomeData struct { | ||
| 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 |
| @@ -902,5 +902,8 @@ body { | ||
| 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; |
| @@ -908,4 +911,67 @@ body { | ||
| 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; |