summaryrefslogtreecommitdiff
path: root/internal/generator/atom.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-09 20:44:58 +0300
committerPaul Buetow <paul@buetow.org>2026-04-09 20:44:58 +0300
commit3e61d09873065f5342efc414ee3ea0d5fdc4c767 (patch)
tree7d0ac51cfb41b4774db6292deeb0cc3dce93cf07 /internal/generator/atom.go
parent51f95f88ca78471a50b3fc62dbcea8edb609dc80 (diff)
add snonux static microblog generator
Full Go implementation with: - txt/md/image/audio input processing, URL auto-linking in .txt files - Paginated HTML output with Atom feed - 11 visual themes: neon, terminal, synthwave, minimal, brutalist, paper, aurora, matrix, ocean, retro, glass (selectable via --theme flag) - Keyboard navigation (j/k/arrows, Enter modal, h/l page nav) - Shared nav templates (navhints, navmodal, navscript) across all themes - Magefile build automation; integration test suite covering all themes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/generator/atom.go')
-rw-r--r--internal/generator/atom.go104
1 files changed, 104 insertions, 0 deletions
diff --git a/internal/generator/atom.go b/internal/generator/atom.go
new file mode 100644
index 0000000..259301c
--- /dev/null
+++ b/internal/generator/atom.go
@@ -0,0 +1,104 @@
+package generator
+
+import (
+ "encoding/xml"
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+
+ "codeberg.org/snonux/snonux/internal/config"
+ "codeberg.org/snonux/snonux/internal/post"
+)
+
+// atomFeed is the root element of an Atom 1.0 feed document.
+type atomFeed struct {
+ XMLName xml.Name `xml:"feed"`
+ XMLNS string `xml:"xmlns,attr"`
+ Title string `xml:"title"`
+ Link atomLink `xml:"link"`
+ Updated string `xml:"updated"`
+ ID string `xml:"id"`
+ Entries []atomEntry `xml:"entry"`
+}
+
+type atomLink struct {
+ Href string `xml:"href,attr"`
+ Rel string `xml:"rel,attr,omitempty"`
+}
+
+type atomEntry struct {
+ Title string `xml:"title"`
+ Link atomLink `xml:"link"`
+ ID string `xml:"id"`
+ Updated string `xml:"updated"`
+ Content atomContent `xml:"content"`
+}
+
+type atomContent struct {
+ Type string `xml:"type,attr"`
+ Value string `xml:",chardata"`
+}
+
+// generateAtom writes atom.xml to cfg.OutputDir containing the most recent
+// min(len(posts), config.PostsPerPage) entries.
+func generateAtom(posts []*post.Post, cfg *config.Config) error {
+ limit := config.PostsPerPage
+ if len(posts) < limit {
+ limit = len(posts)
+ }
+
+ recent := posts[:limit]
+ entries := buildAtomEntries(recent, cfg.BaseURL)
+
+ updated := time.Now().UTC().Format(time.RFC3339)
+ if len(recent) > 0 {
+ updated = recent[0].Timestamp.UTC().Format(time.RFC3339)
+ }
+
+ feed := atomFeed{
+ XMLNS: "http://www.w3.org/2005/Atom",
+ Title: "snonux.foo",
+ Link: atomLink{Href: cfg.BaseURL + "/"},
+ Updated: updated,
+ ID: cfg.BaseURL + "/",
+ Entries: entries,
+ }
+
+ return writeAtomFile(feed, filepath.Join(cfg.OutputDir, "atom.xml"))
+}
+
+// buildAtomEntries converts a slice of posts into Atom entry elements.
+func buildAtomEntries(posts []*post.Post, baseURL string) []atomEntry {
+ entries := make([]atomEntry, 0, len(posts))
+
+ for _, p := range posts {
+ entryURL := fmt.Sprintf("%s/posts/%s/", baseURL, p.ID)
+ entry := atomEntry{
+ Title: fmt.Sprintf("Post %s", p.ID),
+ Link: atomLink{Href: entryURL, Rel: "alternate"},
+ ID: entryURL,
+ Updated: p.Timestamp.UTC().Format(time.RFC3339),
+ Content: atomContent{Type: "html", Value: p.Content},
+ }
+ entries = append(entries, entry)
+ }
+
+ return entries
+}
+
+// writeAtomFile marshals feed to XML and writes it to path with XML declaration.
+func writeAtomFile(feed atomFeed, path string) error {
+ data, err := xml.MarshalIndent(feed, "", " ")
+ 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
+}