Order stories by feed publish time, not ingest time

This commit is contained in:
prosolis
2026-05-25 18:49:47 -07:00
parent 58493006e1
commit f52f26ff8d
6 changed files with 47 additions and 16 deletions

View File

@@ -28,6 +28,7 @@ type FeedItem struct {
ArticleURL string
Source string
DirectRoute string
PublishedAt int64 // unix seconds from RSS pubDate / Atom published (or updated as fallback); 0 if absent or unparseable
}
var (
@@ -53,11 +54,12 @@ func FetchFeed(feedURL string) ([]FeedItem, error) {
items := make([]FeedItem, 0, len(feed.Items))
for _, item := range feed.Items {
fi := FeedItem{
GUID: itemGUID(item),
Headline: strings.TrimSpace(item.Title),
Lede: extractLede(item.Description),
ImageURL: NormalizeImageURL(extractImageURL(item)),
ArticleURL: strings.TrimSpace(item.Link),
GUID: itemGUID(item),
Headline: strings.TrimSpace(item.Title),
Lede: extractLede(item.Description),
ImageURL: NormalizeImageURL(extractImageURL(item)),
ArticleURL: strings.TrimSpace(item.Link),
PublishedAt: itemPublished(item),
}
if fi.GUID == "" || fi.ArticleURL == "" {
continue
@@ -76,6 +78,19 @@ func itemGUID(item *gofeed.Item) string {
return item.Link
}
// itemPublished returns the item's publish time as unix seconds, falling back
// to the updated time if pubDate was absent. Returns 0 when neither parses;
// the poller clamps future timestamps to ingest time.
func itemPublished(item *gofeed.Item) int64 {
if item.PublishedParsed != nil {
return item.PublishedParsed.Unix()
}
if item.UpdatedParsed != nil {
return item.UpdatedParsed.Unix()
}
return 0
}
// extractLede returns the feed description verbatim, with HTML stripped.
func extractLede(desc string) string {
if desc == "" {

View File

@@ -188,7 +188,15 @@ func (p *Poller) pollOnceWithErr(ctx context.Context, src config.SourceConfig) e
items[i].Source = src.Name
items[i].DirectRoute = src.DirectRoute
// Store immediately (before routing) to prevent re-ingestion
// Store immediately (before routing) to prevent re-ingestion.
// Future-dated pubDates (publishers occasionally post-date drafts or
// have a misconfigured clock) get clamped to ingest time so they don't
// stick at the top of the queue indefinitely.
seenAt := time.Now().Unix()
publishedAt := items[i].PublishedAt
if publishedAt > seenAt {
publishedAt = seenAt
}
if err := storage.InsertStory(&storage.Story{
GUID: items[i].GUID,
Headline: items[i].Headline,
@@ -199,7 +207,8 @@ func (p *Poller) pollOnceWithErr(ctx context.Context, src config.SourceConfig) e
HeadlineNorm: headlineNorm,
Source: items[i].Source,
Paywalled: paywalled,
SeenAt: time.Now().Unix(),
SeenAt: seenAt,
PublishedAt: publishedAt,
}); err != nil {
slog.Error("failed to insert story", "guid", items[i].GUID, "err", err)
continue