1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
|
// Package atom serializes the site Atom 1.0 feed (atom.xml). It depends only on
// internal/config and internal/post. It does not import HTML themes, shared nav
// templates, or the html/template page pipeline — those live in the parent
// generator package.
package atom
import (
"context"
"encoding/xml"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"codeberg.org/snonux/snonux/internal/config"
"codeberg.org/snonux/snonux/internal/post"
)
// feed is the root element of an Atom 1.0 feed document.
type feed struct {
XMLName xml.Name `xml:"feed"`
XMLNS string `xml:"xmlns,attr"`
Title string `xml:"title"`
Link link `xml:"link"`
Updated string `xml:"updated"`
ID string `xml:"id"`
Entries []entry `xml:"entry"`
}
type link struct {
Href string `xml:"href,attr"`
Rel string `xml:"rel,attr,omitempty"`
}
type entry struct {
Title string `xml:"title"`
Link link `xml:"link"`
ID string `xml:"id"`
Updated string `xml:"updated"`
Content content `xml:"content"`
}
type content struct {
Type string `xml:"type,attr"`
Value string `xml:",chardata"`
}
// Generate writes atom.xml to cfg.OutputDir containing the most recent
// min(len(posts), config.PostsPerPage) entries. Posts must already be sorted
// newest-first (as produced by generator.Run).
// The context is currently accepted for API consistency and future
// cancellation propagation; no blocking I/O operations currently observe it.
func Generate(ctx context.Context, posts []*post.Post, cfg *config.Config) error {
limit := config.PostsPerPage
if len(posts) < limit {
limit = len(posts)
}
recent := posts[:limit]
entries := buildEntries(recent, cfg.BaseURL)
updated := time.Now().UTC().Format(time.RFC3339)
if len(recent) > 0 {
updated = recent[0].Timestamp.UTC().Format(time.RFC3339)
}
f := feed{
XMLNS: "http://www.w3.org/2005/Atom",
Title: "snonux.foo",
Link: link{Href: cfg.BaseURL + "/"},
Updated: updated,
ID: cfg.BaseURL + "/",
Entries: entries,
}
return writeFile(f, filepath.Join(cfg.OutputDir, "atom.xml"))
}
func buildEntries(posts []*post.Post, baseURL string) []entry {
entries := make([]entry, 0, len(posts))
base := strings.TrimSuffix(baseURL, "/")
for _, p := range posts {
// Link to the main HTML feed page, not /posts/<id>/ (no per-post HTML there;
// asset URLs in content are root-relative, e.g. posts/<id>/image.jpg).
entryURL := fmt.Sprintf("%s/#post-%s", base, p.ID)
entries = append(entries, entry{
Title: fmt.Sprintf("Post %s", p.ID),
Link: link{Href: entryURL, Rel: "alternate"},
ID: entryURL,
Updated: p.Timestamp.UTC().Format(time.RFC3339),
Content: content{Type: "html", Value: p.Content},
})
}
return entries
}
func writeFile(f feed, path string) error {
data, err := xml.MarshalIndent(f, "", " ")
if err != nil {
return fmt.Errorf("marshal atom feed: %w", err)
}
content := append([]byte(xml.Header), data...)
if err := os.WriteFile(path, content, 0o644); err != nil {
return fmt.Errorf("write atom.xml: %w", err)
}
return nil
}
|