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.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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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,
|
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:
|
||||||
|
|||||||
Reference in New Issue
Block a user