Adding an Atom feed and syntax highlighting to a static site build script
Continuing on from publishing this TIL collection as a static site, I wanted two things a real TIL site should have: an Atom feed people can subscribe to, and syntax-highlighted code blocks instead of flat gray <pre> boxes.
Atom feed
build_site.py already looped over every topic directory to render per-entry HTML pages. I had it also append each rendered entry (title, date, url, and the rendered HTML body) to a flat list as it went, so after the loop I have everything needed to build a feed without re-parsing anything.
The feed itself is plain string templating, no library:
FEED_ENTRY_TEMPLATE = """<entry>
<title>{title}</title>
<link href="{url}"/>
<id>{url}</id>
<updated>{updated}</updated>
<content type="html">{content}</content>
</entry>"""
Two things mattered for correctness:
- XML-escaping the content. An Atom
<content type="html">element holds an HTML string as escaped text, not as literal child elements (that would requiretype="xhtml"with different rules). Every entry body has to go throughxml.sax.saxutils.escape()before being dropped into the template, otherwise a<p>tag in the TIL body would be parsed as an actual feed XML element and break the document. - Capping the length. Sorting all entries by date descending and slicing to the most recent 50 keeps the feed from growing forever — feed readers only care about what's new.
I validated the output the boring way: xml.dom.minidom.parse() on the generated file. If it doesn't raise, the XML is well-formed.
Syntax highlighting
Python-Markdown ships a codehilite extension that hands fenced code blocks to Pygments. Turning it on is one line:
markdown.markdown(
body_text,
extensions=["fenced_code", "tables", "codehilite"],
extension_configs={"codehilite": {"guess_lang": False}},
)
(guess_lang: False matters — without it, codehilite tries to detect the language of unlabeled fenced blocks, which is slow and often wrong. Since I always label my fences with a language, I don't need the guesser.)
The highlighted output is <span> tags with class names like .nt, .p, .s1 — Pygments' generic token classes. Those classes need a stylesheet to mean anything, and rather than hand-write one (and have it drift out of sync with whatever Pygments version generates the spans), I generate it at build time:
from pygments.formatters import HtmlFormatter
formatter = HtmlFormatter(style="default")
css = formatter.get_style_defs(".codehilite")
That gets appended straight into style.css alongside my hand-written layout rules. Change style="default" to any other Pygments theme name and the CSS updates itself on the next build — no separate file to keep in sync.
Result
Both are live at https://til.housni.eu/ — the feed at /feed.atom, discoverable via <link rel="alternate" type="application/atom+xml"> in every page's <head>.
Created 2026-09-05T02:43:33+02:00, updated 2026-09-29T22:48:40+02:00 · History · Edit