summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-10 09:49:58 +0300
committerPaul Buetow <paul@buetow.org>2026-04-10 09:49:58 +0300
commit65e2c4ad6b7f8d9d1685d26e5c976dd846453252 (patch)
tree224c8e14e3a8be9c5e19740c53168324ce813012
parent4c10490e0488b03de70a6e0d7d7432347dcce00a (diff)
generator: isolate Atom feed in atom subpackage, document boundaries
Move Atom 1.0 XML generation to internal/generator/atom so it depends only on config and post, not on themes or the HTML template pipeline. Add generator/doc.go mapping files (orchestration, registry, themes, shared nav templates) and dependency direction. Made-with: Cursor
-rw-r--r--internal/generator/atom.go104
-rw-r--r--internal/generator/atom/atom.go106
-rw-r--r--internal/generator/doc.go20
-rw-r--r--internal/generator/generator.go11
4 files changed, 131 insertions, 110 deletions
diff --git a/internal/generator/atom.go b/internal/generator/atom.go
deleted file mode 100644
index 259301c..0000000
--- a/internal/generator/atom.go
+++ /dev/null
@@ -1,104 +0,0 @@
-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
-}
diff --git a/internal/generator/atom/atom.go b/internal/generator/atom/atom.go
new file mode 100644
index 0000000..f57ad0d
--- /dev/null
+++ b/internal/generator/atom/atom.go
@@ -0,0 +1,106 @@
+// 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 (
+ "encoding/xml"
+ "fmt"
+ "os"
+ "path/filepath"
+ "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).
+func Generate(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))
+
+ for _, p := range posts {
+ entryURL := fmt.Sprintf("%s/posts/%s/", baseURL, 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
+}
diff --git a/internal/generator/doc.go b/internal/generator/doc.go
new file mode 100644
index 0000000..55db6cb
--- /dev/null
+++ b/internal/generator/doc.go
@@ -0,0 +1,20 @@
+// Package generator builds static HTML pages and delegates Atom feed output to
+// subpackage atom.
+//
+// Responsibilities by area (file → role):
+//
+// - generator.go — Orchestration: load posts from disk, sort newest-first,
+// paginate, parse theme+nav templates, write index.html / pageN.html,
+// then call atom.Generate.
+// - themes.go — Theme registry (name → template string) and getTheme /
+// ListThemes for the CLI.
+// - theme_*.go — One file per visual theme: full-page HTML that invokes
+// {{template "navhints" .}}, {{template "navmodal" .}}, {{template "navscript" .}}.
+// - shared.go — navDefs: shared {{define}} blocks merged at parse time with
+// the chosen theme so a single html/template parse sees every name.
+// - templates.go — Short pointer: where templates and registry live.
+//
+// Dependency direction: themes and shared nav templates are composed only for
+// the HTML path (generator.go). Package atom depends on config and post only,
+// not on themes or html/template, so feed logic stays isolated from page chrome.
+package generator
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 595bb62..9fed673 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -1,5 +1,3 @@
-// Package generator reads all post directories from outdir/posts/, sorts them by
-// timestamp descending, paginates them into HTML pages, and writes atom.xml.
package generator
import (
@@ -13,14 +11,15 @@ import (
"time"
"codeberg.org/snonux/snonux/internal/config"
+ "codeberg.org/snonux/snonux/internal/generator/atom"
"codeberg.org/snonux/snonux/internal/post"
)
// pageData holds the template variables for a single HTML page.
type pageData struct {
- Posts []postView
- PrevPage string // URL of the newer page, empty if none
- NextPage string // URL of the older page, empty if none
+ Posts []postView
+ PrevPage string // URL of the newer page, empty if none
+ NextPage string // URL of the older page, empty if none
PrevPageJSON template.JS
NextPageJSON template.JS
}
@@ -59,7 +58,7 @@ func Run(cfg *config.Config) error {
}
}
- return generateAtom(posts, cfg)
+ return atom.Generate(posts, cfg)
}
// loadAllPosts walks outdir/posts/ and deserialises every post.json found.