diff --git a/users/tazjin/atom-feed/default.nix b/users/tazjin/atom-feed/default.nix
new file mode 100644
index 000000000..369295da2
--- /dev/null
+++ b/users/tazjin/atom-feed/default.nix
@@ -0,0 +1,139 @@
+# This file defines functions for generating an Atom feed.
+
+{ depot, lib, ... }:
+
+with depot.nix.yants;
+
+let
+ inherit (builtins) map readFile replaceStrings;
+ inherit (lib) concatStrings concatStringsSep removeSuffix;
+ inherit (depot.third_party) runCommandNoCC;
+
+ # 'link' describes a related link to a feed, or feed element.
+ #
+ # https://validator.w3.org/feed/docs/atom.html#link
+ link = struct "link" {
+ rel = string;
+ href = string;
+ };
+
+ # 'entry' describes a feed entry, for example a single post on a
+ # blog. Some optional fields have been omitted.
+ #
+ # https://validator.w3.org/feed/docs/atom.html#requiredEntryElements
+ entry = struct "entry" {
+ # Identifies the entry using a universally unique and permanent URI.
+ id = string;
+
+ # Contains a human readable title for the entry. This value should
+ # not be blank.
+ title = string;
+
+ # Content of the entry. This element is technically optional, but
+ # only if an alternate link is provided. In practice it should
+ # always be present in the feeds generated by this code.
+ content = string;
+
+ # Indicates the last time the entry was modified in a significant
+ # way (in seconds since epoch).
+ updated = int;
+
+ # Names authors of the entry. Recommended element.
+ authors = option (list string);
+
+ # Related web pages, such as the web location of a blog post.
+ links = option (list link);
+
+ # Conveys a short summary, abstract, or excerpt of the entry.
+ summary = option string;
+
+ # Contains the time of the initial creation or first availability
+ # of the entry.
+ published = option int;
+
+ # Conveys information about rights, e.g. copyrights, held in and
+ # over the entry.
+ rights = option string;
+ };
+
+ # 'feed' describes the metadata of the Atom feed itself.
+ #
+ # Some optional fields have been omitted.
+ #
+ # https://validator.w3.org/feed/docs/atom.html#requiredFeedElements
+ feed = struct "feed" {
+ # Identifies the feed using a universally unique and permanent URI.
+ id = string;
+
+ # Contains a human readable title for the feed.
+ title = string;
+
+ # Indicates the last time the feed was modified in a significant
+ # way (in seconds since epoch). Recommended element.
+ updated = int;
+
+ # Entries contained within the feed.
+ entries = list entry;
+
+ # Names authors of the feed. Recommended element.
+ authors = option (list string);
+
+ # Related web locations. Recommended element.
+ links = option (list link);
+
+ # Conveys information about rights, e.g. copyrights, held in and
+ # over the feed.
+ rights = option string;
+
+ # Contains a human-readable description or subtitle for the feed.
+ subtitle = option string;
+ };
+
+ # Feed generation functions:
+
+ renderEpoch = epoch: removeSuffix "\n" (readFile (runCommandNoCC "date-${toString epoch}" {} ''
+ date --date='@${toString epoch}' --utc --iso-8601='seconds' > $out
+ ''));
+
+ escape = replaceStrings [ "<" ">" "&" "'" ] [ "<" ">" "&" "'" ];
+
+ elem = name: content: ''<${name}>${escape content}${name}>'';
+
+ renderLink = defun [ link string ] (l: ''
+
+ '');
+
+ # Technically the author element can also contain 'uri' and 'email'
+ # fields, but they are not used for the purpose of this feed and are
+ # omitted.
+ renderAuthor = author: ''${escape author}'';
+
+ renderEntry = defun [ entry string ] (e: ''
+
+ ${elem "title" e.title}
+ ${elem "id" e.id}
+ ${elem "updated" (renderEpoch e.updated)}
+ ${escape e.content}
+ ${concatStrings (map renderAuthor (e.authors or []))}
+ ${if e ? subtitle then elem "subtitle" e.subtitle else ""}
+ ${if e ? rights then elem "rights" e.rights else ""}
+ ${concatStrings (map renderLink (e.links or []))}
+
+ '');
+
+ renderFeed = defun [ feed string ] (f: ''
+
+
+ ${elem "id" f.id}
+ ${elem "title" f.title}
+ ${elem "updated" (renderEpoch f.updated)}
+ ${concatStringsSep "\n" (map renderAuthor (f.authors or []))}
+ ${if f ? subtitle then elem "subtitle" f.subtitle else ""}
+ ${if f ? rights then elem "rights" f.rights else ""}
+ ${concatStrings (map renderLink (f.links or []))}
+ ${concatStrings (map renderEntry f.entries)}
+
+ '');
+in {
+ inherit entry feed renderFeed renderEpoch;
+}
diff --git a/users/tazjin/blog/fragments.nix b/users/tazjin/blog/fragments.nix
index 18416e4c4..978dfa2a2 100644
--- a/users/tazjin/blog/fragments.nix
+++ b/users/tazjin/blog/fragments.nix
@@ -5,7 +5,8 @@
# An entire post is rendered by `renderPost`, which assembles the
# fragments together in a runCommand execution.
#
-# The post index is generated by //web/homepage, not by this code.
+# The post index is generated by //users/tazjin/homepage, not by this
+# code.
{ depot, lib, ... }:
let