Add Atom feed generation
This commit is contained in:
@@ -8,6 +8,7 @@ from omicron.ssg.output.directory import Directory
|
||||
from omicron.ssg.output.symlink import Symlink
|
||||
from omicron.ssg.output.tags import Tags, discover_tags
|
||||
from omicron.ssg.output.sitemap import Sitemap
|
||||
from omicron.ssg.output.feed import Feed, discover_feeds
|
||||
|
||||
__all__ = [
|
||||
"Output",
|
||||
@@ -25,4 +26,6 @@ __all__ = [
|
||||
"Tags",
|
||||
"discover_tags",
|
||||
"Sitemap",
|
||||
"Feed",
|
||||
"discover_feeds",
|
||||
]
|
||||
|
||||
@@ -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
@@ -16,8 +16,10 @@ from omicron.ssg.output import (
|
||||
Tags,
|
||||
Memory,
|
||||
Sitemap,
|
||||
Feed,
|
||||
discover_index,
|
||||
discover_tags,
|
||||
discover_feeds,
|
||||
)
|
||||
from omicron.ssg.markdown import highlight_style
|
||||
|
||||
@@ -199,8 +201,11 @@ class Site:
|
||||
sitemap = Sitemap(self)
|
||||
self.add_by_path(sitemap)
|
||||
self.other_outputs.append(sitemap)
|
||||
for feed in discover_feeds(self):
|
||||
self.add_by_path(feed)
|
||||
self.other_outputs.append(feed)
|
||||
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))
|
||||
|
||||
def clean(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user