Add Atom feed generation

This commit is contained in:
2026-08-26 05:07:15 +02:00
parent bfb0d371c8
commit f18392c899
3 changed files with 92 additions and 1 deletions
+3
View File
@@ -8,6 +8,7 @@ from omicron.ssg.output.directory import Directory
from omicron.ssg.output.symlink import Symlink from omicron.ssg.output.symlink import Symlink
from omicron.ssg.output.tags import Tags, discover_tags from omicron.ssg.output.tags import Tags, discover_tags
from omicron.ssg.output.sitemap import Sitemap from omicron.ssg.output.sitemap import Sitemap
from omicron.ssg.output.feed import Feed, discover_feeds
__all__ = [ __all__ = [
"Output", "Output",
@@ -25,4 +26,6 @@ __all__ = [
"Tags", "Tags",
"discover_tags", "discover_tags",
"Sitemap", "Sitemap",
"Feed",
"discover_feeds",
] ]
+83
View File
@@ -0,0 +1,83 @@
import logging
from datetime import date
from pathlib import Path
from typing import TYPE_CHECKING, Literal
from xml.etree.ElementTree import Element, ElementTree, SubElement
from omicron.ssg.output.output import Output
from omicron.ssg.output.content import Content
log = logging.getLogger(__name__)
if TYPE_CHECKING:
from omicron.ssg.site import Site # for static checking with mypy
else:
Site = "omicron.ssg.site.Site" # for runtime checking with beartype
ITEMS_PER_FEED = 20
def atom_date(d: date) -> str:
return f"{d.isoformat()}T00:00:00Z"
class Feed(Output):
NAMESPACE = "http://www.w3.org/2005/Atom"
def __init__(
self,
site: Site,
kind: Literal["root", "section"],
items: list[Content],
label: str | None,
):
super().__init__(site, Feed.build_path(kind, label))
self.kind = kind
self.label = label
self.items = sorted(items, key=lambda c: c.created, reverse=True)[
:ITEMS_PER_FEED
]
@staticmethod
def build_path(kind: Literal["root", "section"], label: str | None) -> Path:
file = Path("atom.xml")
if kind == "root":
return file
assert label, "label can't be None if kind is not root"
return Path(label) / file
def write(self) -> None:
feed_url = f"{self.site.origin}{self.url}"
title = f"{self.site.name}{self.label}" if self.label else self.site.name
updated = max(
(c.updated or c.created for c in self.items), default=date.today()
)
feed = Element("feed", xmlns=Feed.NAMESPACE)
SubElement(feed, "title").text = title
SubElement(feed, "id").text = feed_url
SubElement(feed, "link", rel="self", href=feed_url)
SubElement(feed, "updated").text = atom_date(updated)
for item in self.items:
entry = SubElement(feed, "entry")
item_url = f"{self.site.origin}{item.url}"
SubElement(entry, "title").text = item.title
SubElement(entry, "id").text = item_url
SubElement(entry, "link", href=item_url)
SubElement(entry, "published").text = atom_date(item.created)
SubElement(entry, "updated").text = atom_date(item.updated or item.created)
if item.summary:
summary = SubElement(entry, "summary", type="html")
summary.text = str(item.summary)
dest = self.site.output_path / self.destination
dest.parent.mkdir(parents=True, exist_ok=True)
ElementTree(feed).write(dest, encoding="utf-8", xml_declaration=True)
log.debug("Wrote %s", dest)
def discover_feeds(site: Site) -> list[Feed]:
outputs = [Feed(site, "root", site.content, None)]
for section, items in site.by_section.items():
outputs.append(Feed(site, "section", items, section))
return outputs
+6 -1
View File
@@ -16,8 +16,10 @@ from omicron.ssg.output import (
Tags, Tags,
Memory, Memory,
Sitemap, Sitemap,
Feed,
discover_index, discover_index,
discover_tags, discover_tags,
discover_feeds,
) )
from omicron.ssg.markdown import highlight_style from omicron.ssg.markdown import highlight_style
@@ -199,8 +201,11 @@ class Site:
sitemap = Sitemap(self) sitemap = Sitemap(self)
self.add_by_path(sitemap) self.add_by_path(sitemap)
self.other_outputs.append(sitemap) self.other_outputs.append(sitemap)
for feed in discover_feeds(self):
self.add_by_path(feed)
self.other_outputs.append(feed)
else: else:
log.debug("Skipping sitemap, base_url has no domain name") log.debug("Skipping sitemap and feeds, base_url has no domain name")
log.info("Discovered %d content items", len(self.content)) log.info("Discovered %d content items", len(self.content))
def clean(self) -> None: def clean(self) -> None: