diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-10 09:49:58 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-10 09:49:58 +0300 |
| commit | 65e2c4ad6b7f8d9d1685d26e5c976dd846453252 (patch) | |
| tree | 224c8e14e3a8be9c5e19740c53168324ce813012 | |
| parent | 4c10490e0488b03de70a6e0d7d7432347dcce00a (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.go | 104 | ||||
| -rw-r--r-- | internal/generator/atom/atom.go | 106 | ||||
| -rw-r--r-- | internal/generator/doc.go | 20 | ||||
| -rw-r--r-- | internal/generator/generator.go | 11 |
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. |
