// RSS rendering for the recommendations feed: hand-rolled RSS 2.0 via // encoding/xml, no feed library. The render source is the runs.jsonl tail; // items appear newest batch first, kept order within a batch. Guid embeds // the original Miniflux entry ID (mf:) for traceability. package cmds import ( "encoding/xml" "fmt" "time" "fluxrec/core" ) const rssXMLHeader = `` + "\n" type rssGUID struct { IsPermaLink bool `xml:"isPermaLink,attr"` Value string `xml:",chardata"` } type rssItem struct { Title string `xml:"title"` Link string `xml:"link"` GUID rssGUID `xml:"guid"` PubDate string `xml:"pubDate"` Description string `xml:"description"` } type rssChannel struct { Title string `xml:"title"` Description string `xml:"description"` Items []rssItem `xml:"item"` } type rssDocument struct { XMLName xml.Name `xml:"rss"` Version string `xml:"version,attr"` Channel rssChannel `xml:"channel"` } // RenderRSS renders runs (oldest to newest, as read from runs.jsonl) as an // RSS 2.0 document. The source feed title prefixes each item description so // provenance is visible in-reader; item content was already cleaned when // the run row was written. func RenderRSS(runs []core.RunRow) ([]byte, error) { doc := rssDocument{ Version: "2.0", Channel: rssChannel{ Title: "fluxrec recommendations", Description: "Top-scored Miniflux entries, refreshed on a schedule.", }, } for i := len(runs) - 1; i >= 0; i-- { for _, k := range runs[i].Kept { doc.Channel.Items = append(doc.Channel.Items, rssItem{ Title: k.Title, Link: k.URL, GUID: rssGUID{IsPermaLink: false, Value: fmt.Sprintf("mf:%d", k.EntryID)}, PubDate: k.PublishedAt.Format(time.RFC1123Z), Description: rssDescription(k), }) } } body, err := xml.MarshalIndent(doc, "", " ") if err != nil { return nil, fmt.Errorf("marshaling RSS: %w", err) } return append([]byte(rssXMLHeader), body...), nil } // rssDescription builds the item body: source feed name on the first line, // then the cleaned content (if any). func rssDescription(k core.KeptItem) string { if k.Content == "" { return k.FeedTitle } if k.FeedTitle == "" { return k.Content } return k.FeedTitle + "\n\n" + k.Content }