summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Makefile15
-rw-r--r--share/mkwb/site.mk466
-rwxr-xr-xsrc/rules.in6
-rwxr-xr-xtests/integration.sh94
-rw-r--r--tests/resources/expected/hello.html.0.txt4
-rw-r--r--tests/resources/expected/hello.html.1.txt1
-rw-r--r--tests/resources/expected/hello.htmlbody54
-rw-r--r--tests/resources/expected/hello.links2
-rw-r--r--tests/resources/expected/hello.snippets2
-rw-r--r--tests/resources/expected/src/content/en/blog/2024-01-15-launch.sortdata1
-rw-r--r--tests/resources/expected/src/content/en/blog/2025-03-20-feature.sortdata1
-rw-r--r--tests/resources/expected/src/content/en/news/2024-06-01-founding.sortdata1
-rw-r--r--tests/resources/expected/src/content/en/news/2025-06-01-anniversary.sortdata1
-rw-r--r--tests/resources/site/src/content/en/blog/2024/01/15/launch.adoc4
-rw-r--r--tests/resources/site/src/content/en/blog/2024/01/15/launch.conf2
-rw-r--r--tests/resources/site/src/content/en/blog/2025/03/20/feature.adoc4
-rw-r--r--tests/resources/site/src/content/en/blog/2025/03/20/feature.conf2
-rw-r--r--tests/resources/site/src/content/en/news/2024/06/01/founding.adoc4
-rw-r--r--tests/resources/site/src/content/en/news/2024/06/01/founding.conf2
-rw-r--r--tests/resources/site/src/content/en/news/2025/06/01/anniversary.adoc4
-rw-r--r--tests/resources/site/src/content/en/news/2025/06/01/anniversary.conf2
-rw-r--r--tests/resources/site/src/content/hello.adoc25
23 files changed, 695 insertions, 3 deletions
diff --git a/.gitignore b/.gitignore
index cbe1900..7183f87 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
/src/mkwb
+/src/rules
diff --git a/Makefile b/Makefile
index 306519f..d4a524b 100644
--- a/Makefile
+++ b/Makefile
@@ -27,7 +27,8 @@ LDLIBS =
.in:
sed \
- 's:@LIBEXECDIR@:$(LIBEXECDIR):g' \
+ -e 's:@LIBEXECDIR@:$(LIBEXECDIR):g' \
+ -e 's:@SHAREDIR@:$(SHAREDIR):g' \
< $< > $@
if [ -x $< ]; then chmod +x $@; fi
@@ -39,10 +40,14 @@ include deps.mk
sources = \
$(libexec.sh) \
src/$(NAME).in \
+ src/rules.in \
+share-files = \
+ share/$(NAME)/site.mk \
derived-assets = \
src/$(NAME) \
+ src/rules \
side-assets = \
@@ -57,7 +62,8 @@ all: $(derived-assets)
check-unit:
-check-integration:
+check-integration: src/$(NAME)
+ ./tests/integration.sh
## Run all tests. Each test suite is isolated, so that a parallel
@@ -79,10 +85,12 @@ install: all
mkdir -p \
'$(DESTDIR)$(BINDIR)' \
'$(DESTDIR)$(LIBEXECDIR)' \
+ '$(DESTDIR)$(SHAREDIR)/$(NAME)' \
'$(DESTDIR)$(SRCDIR)' \
cp src/$(NAME) '$(DESTDIR)$(BINDIR)'
- cp $(libexec.sh) '$(DESTDIR)$(LIBEXECDIR)'
+ cp $(libexec.sh) src/rules '$(DESTDIR)$(LIBEXECDIR)'
+ cp $(share-files) '$(DESTDIR)$(SHAREDIR)/$(NAME)'
cp $(sources) '$(DESTDIR)$(SRCDIR)'
@@ -93,6 +101,7 @@ uninstall:
rm -rf \
'$(DESTDIR)$(BINDIR)'/$(NAME) \
'$(DESTDIR)$(LIBEXECDIR)' \
+ '$(DESTDIR)$(SHAREDIR)/$(NAME)' \
'$(DESTDIR)$(SRCDIR)' \
diff --git a/share/mkwb/site.mk b/share/mkwb/site.mk
new file mode 100644
index 0000000..76e9e51
--- /dev/null
+++ b/share/mkwb/site.mk
@@ -0,0 +1,466 @@
+## mkwb canonical site rules. Sites consume this transitively
+## via deps.mk: mkdeps.sh emits `include <mkwb rules path>` as
+## the last line of deps.mk, so the site Makefile only needs an
+## `all:` anchor followed by `include deps.mk`. Order matters:
+## `all:` must come first (otherwise deps.mk's first target
+## becomes the default), and the site.mk include must come AFTER
+## deps.mk's variable assignments so that this file's
+## `all: $(derived-assets)` line can resolve its prereq list at
+## parse time.
+##
+## The including Makefile is expected to define at minimum:
+##
+## NAME -- site name (used for SRCDIR/HTMLDIR install paths)
+## VERSION
+## DATE
+##
+## To override any of PREFIX / BINDIR / SHAREDIR / SRCDIR /
+## HTMLDIR / PUBURL / BASEURL / FFMFLAGS, set them AFTER the
+## include (plain `=`, POSIX -- no `?=` conditional assignment),
+## or pass `-e` to make to let env vars take precedence.
+
+
+
+## Default values. Sites override AFTER the include or via env.
+PREFIX = /usr
+BINDIR = $(PREFIX)/bin
+LIBDIR = $(PREFIX)/lib
+INCLUDEDIR = $(PREFIX)/include
+SHAREDIR = $(PREFIX)/share
+LOCALEDIR = $(SHAREDIR)/locale
+MANDIR = $(SHAREDIR)/man
+DOCDIR = $(SHAREDIR)/doc/$(NAME)
+HTMLDIR = $(SHAREDIR)/html/$(NAME)
+SRCDIR = $(PREFIX)/src/$(NAME)
+DESTDIR =
+PUBURL = public.asc.txt
+BASEURL = /
+FFMFLAGS = -y -hide_banner -loglevel warning
+
+
+
+.SUFFIXES:
+.SUFFIXES: .adoc .conf .snippets .indexentry .feedentry .mapentry .sortdata .xml
+.SUFFIXES: .htmlbody .htmlheader .htmlfooter .htmllisting .html .links .caslinks
+.SUFFIXES: .txt .categorydata .gz .torrent .flac .ogg .ps .pdf .sentinel
+
+.adoc.conf:
+ mkwb conf src/global.conf $< > $@
+
+.adoc.htmlbody:
+ mkwb htmlbody $< > $@
+
+.htmlbody.html:
+ mkwb html $< > $@
+
+.conf.htmlheader:
+ mkwb html -H $< > $@
+
+.conf.htmlfooter:
+ mkwb html -F $< > $@
+
+.adoc.snippets:
+ mkwb snippets $< > $@
+
+.conf.indexentry:
+ mkwb indexentry $< > $@
+
+.htmlbody.feedentry:
+ mkwb feedentry $< > $@
+
+.conf.mapentry:
+ mkwb mapentry $< > $@
+
+.conf.sortdata:
+ mkwb sortdata $< > $@
+
+.conf.categorydata:
+ mkwb categorydata $< > $@
+
+.adoc.links:
+ mkwb links $< > $@
+
+.links.caslinks:
+ grep -Ev '^(link|image):' $< | xargs -I_ sh -c '\
+ printf "%s\n" "_" | sha256sum | \
+ printf "%s\t%s\n" "`cut -d" " -f1`" "_"' > $@
+
+.flac.ogg:
+ ffmpeg $(FFMFLAGS) -i $< -ar 48000 -vn -c:a libvorbis -b:a 320k $@
+
+.adoc.ps:
+ eslaides < $< > $@
+
+.ps.pdf:
+ ps2pdf - < $< > $@
+
+
+
+## Derived file lists from auto-generated source lists in deps.mk.
+listings.adoc = $(categories.adoc) $(indexes.adoc)
+sources.adoc = $(articles.adoc) $(listings.adoc) $(pages.adoc)
+sources.htmlbody = $(sources.adoc:.adoc=.htmlbody)
+sources.html = $(sources.adoc:.adoc=.html)
+sources.snippets = $(sources.adoc:.adoc=.snippets)
+sources.snippets.gz = $(sources.adoc:.adoc=.snippets.gz)
+sources.conf = $(sources.adoc:.adoc=.conf)
+sources.links = $(sources.adoc:.adoc=.links)
+sources.caslinks = $(sources.adoc:.adoc=.caslinks)
+sources.mapentry = $(sources.adoc:.adoc=.mapentry)
+articles.indexentry = $(articles.adoc:.adoc=.indexentry)
+articles.feedentry = $(articles.adoc:.adoc=.feedentry)
+articles.sortdata = $(articles.adoc:.adoc=.sortdata)
+articles.categorydata = $(articles.adoc:.adoc=.categorydata)
+listings.htmlheader = $(listings.adoc:.adoc=.htmlheader)
+listings.htmlfooter = $(listings.adoc:.adoc=.htmlfooter)
+listings.html = $(listings.adoc:.adoc=.html)
+indexes.htmllisting = $(indexes.adoc:.adoc=.htmllisting)
+categories.htmllisting = $(categories.adoc:.adoc=.htmllisting)
+categories.txt = $(categories.adoc:.adoc=.txt)
+categories.xml = $(categories.adoc:.adoc=.xml)
+categories.xml.gz = $(categories.adoc:.adoc=.xml.gz)
+sources.media.torrent = $(sources.media:=.torrent)
+slides.ps = $(slides.adoc:.adoc=.ps)
+slides.pdf = $(slides.adoc:.adoc=.pdf)
+
+sources = \
+ $(sources.adoc) \
+ $(sources.extras) \
+ $(images.svg) \
+ src/content/favicon.ico \
+ src/content/favicon.png \
+ src/content/style.css \
+ src/content/$(PUBURL) \
+
+dynamic-contents = \
+ $(sources.html) \
+ $(slides.pdf) \
+ $(feeds.xml) \
+ $(sources.media.torrent) \
+ src/content/sitemap.xml \
+
+static-contents = \
+ $(sources.extras) \
+ $(images.svg) \
+ src/content/favicon.ico \
+ src/content/favicon.png \
+ src/content/style.css \
+ src/content/$(PUBURL) \
+ src/content/.well-known/security.txt \
+
+dynamic-contents.gz = $(dynamic-contents:=.gz)
+
+static-contents.gz = \
+ $(images.svg:=.gz) \
+ src/content/favicon.ico.gz \
+ src/content/style.css.gz \
+ src/content/$(PUBURL).gz \
+ src/content/.well-known/security.txt.gz \
+
+contents.gz = \
+ $(dynamic-contents.gz) \
+ $(static-contents.gz) \
+
+contents = \
+ $(dynamic-contents) \
+ $(static-contents) \
+
+all-filelists = \
+ $(sources.snippets) \
+ $(sources.snippets.gz) \
+ $(categories.xml) \
+ $(categories.xml.gz) \
+
+all-contents = \
+ $(contents) \
+ $(contents.gz) \
+
+captured-assets = \
+ src/content/$(PUBURL) \
+ src/content/favicon.ico \
+ src/content/favicon.png \
+
+captured-assets.sentinel = $(captured-assets:=.sentinel)
+
+derived-assets = \
+ $(dynamic-contents) \
+ $(contents.gz) \
+ $(sources.html) \
+ $(sources.htmlbody) \
+ $(sources.snippets) \
+ $(sources.snippets.gz) \
+ $(sources.conf) \
+ $(sources.links) \
+ $(sources.caslinks) \
+ $(sources.mapentry) \
+ src/dyn.conf \
+ src/base.conf \
+ src/global.conf \
+ $(articles.indexentry) \
+ $(articles.feedentry) \
+ $(articles.sortdata) \
+ $(articles.categorydata) \
+ $(listings.htmlheader) \
+ $(listings.htmlfooter) \
+ $(listings.html) \
+ $(indexes.htmllisting) \
+ $(categories.htmllisting) \
+ $(categories.txt) \
+ $(categories.xml) \
+ $(categories.xml.gz) \
+ $(slides.ps) \
+ $(slides.pdf) \
+ email.txt \
+ baseurl.txt \
+ fingerprint.txt \
+ expiry.txt \
+ expiry-epoch.txt \
+ now.txt \
+ src/content/.well-known/security.txt \
+ src/all-contents.txt \
+ src/all-filelists.txt \
+ src/all-symlinks.txt \
+ src/install.txt \
+ src/sort-expected.txt \
+ src/sort-given.txt \
+ src/sources.txt \
+ install.txt \
+ sources.txt \
+ src/content/.gitignore \
+ $(captured-assets.sentinel) \
+
+side-assets = \
+ src/collections/*/*/*/*/*/*.html.*.txt \
+ src/collections/*/*/*/*/*/*.txt.gz \
+ src/collections/*/*/index.html.*.txt \
+ src/collections/*/*/sortdata.txt \
+ src/collections/*/*/feed.*.xml \
+ src/collections/*/*/feed.*.xml.gz \
+ src/collections/*/*/*.sortdata \
+ src/pages/*/*.html.*.txt \
+ src/content/.well-known/ \
+ `cat src/all-symlinks.txt 2>/dev/null` \
+ `cat src/linkonly-dirs.txt 2>/dev/null` \
+
+
+
+## Default target -- builds all derived + captured artifacts.
+all: $(derived-assets) $(captured-assets)
+
+
+$(sources.conf): src/global.conf
+
+
+src/content/.gitignore: src/symlinks.txt
+ cd src/content/ && mkwb symlinks ../symlinks.txt > $(@F)
+
+src/dyn.conf: email.txt baseurl.txt fingerprint.txt
+ printf "export url_pre='%s'\n" "`cat baseurl.txt`" > $@
+ printf "export email='%s'\n" "`cat email.txt`" >> $@
+ printf "export publickey='%s'\n" "`cat fingerprint.txt`" >> $@
+ printf "export publickey_url='$(PUBURL)'\n" >> $@
+ printf 'export sourcecode_url="$$url_pre/git/$(NAME)"\n' >> $@
+
+src/base.conf: src/dyn.conf src/static.conf
+ cat src/dyn.conf src/static.conf > $@
+
+src/global.conf: src/base.conf
+ mkwb conf -G src/base.conf > $@
+
+$(listings.html):
+ cat $*.htmlheader $*.htmlbody $*.htmllisting $*.htmlfooter > $@
+
+$(indexes.htmllisting):
+ mkwb indexbody $*.conf > $@
+
+$(categories.htmllisting):
+ mkwb categoriesbody $*.conf > $@
+
+$(categories.txt): src/global.conf
+ mkwb categories src/global.conf $(@D) > $@
+
+$(categories.xml):
+ for f in `cat $*.txt`; do \
+ c="`printf '%s\n' "$$f" | cut -d. -f2`"; \
+ mkwb feed src/global.conf "$$f" > $(@D)/feed."$$c".xml; \
+ printf '%s\n' $(@D)/feed."$$c".xml; \
+ done > $@
+
+$(feeds.xml):
+ mkwb feed src/global.conf $(@D)/sortdata.txt > $@
+
+$(contents.gz):
+ gzip -9fk $*
+ touch $@
+
+$(sources.snippets.gz) $(categories.xml.gz):
+ if [ -s $* ]; then gzip -9fk `cat $*`; fi
+ sed 's/$$/.gz/' $* > $@
+
+src/content/$(PUBURL).gz: src/content/$(PUBURL).sentinel
+src/content/$(PUBURL).sentinel: email.txt
+ gpg --export --armour "`cat email.txt`" | ifne ifnew $*
+ touch $@
+
+src/content/favicon.ico.gz: src/content/favicon.ico.sentinel
+src/content/favicon.ico.sentinel: src/content/img/favicon.svg
+ convert src/content/img/favicon.svg -strip ico:- | ifnew $*
+ touch $@
+
+src/content/favicon.png.sentinel: src/content/img/favicon.svg
+ convert src/content/img/favicon.svg -strip png:- | ifnew $*
+ touch $@
+
+$(sources.media.torrent):
+ F="`printf '%s\n' $* | cut -d/ -f3-`" && \
+ mktorrent -xfd -n $(*F) -o $@ -w "https://$(NAME)$(BASEURL)$${F}" $*
+
+src/content/sitemap.xml.gz: src/content/sitemap.xml
+src/content/sitemap.xml: $(sources.mapentry)
+ mkwb sitemap $(sources.mapentry) > $@
+
+email.txt: meta.capim
+ cat meta.capim | awk '$$1 == ":email" && $$0=$$2' | tr -d '"' > $@
+
+baseurl.txt: meta.capim
+ cat meta.capim | awk '$$1 == ":baseurl" && $$0=$$2' | tr -d '"' > $@
+
+fingerprint.txt: src/content/$(PUBURL)
+ gpg --always-trust --no-keyring --show-key --with-colons \
+ src/content/$(PUBURL) | \
+ awk -F: '/^pub:/ { print $$5 }' > $@
+
+expiry.txt: src/content/$(PUBURL)
+ gpg --always-trust --no-keyring --show-key --with-colons \
+ src/content/$(PUBURL) | \
+ awk -F: '/^pub:/ { print $$7 }' | \
+ xargs -I% date -Is -d@% > $@
+
+expiry-epoch.txt: expiry.txt
+ date -d "`cat expiry.txt`" '+%s' > $@
+
+now.txt:
+ now > $@
+
+src/content/.well-known/security.txt.gz: src/content/.well-known/security.txt
+src/content/.well-known/security.txt: email.txt baseurl.txt expiry.txt
+ mkdir -p $(@D)
+ printf 'Contact: mailto:%s\n' "`cat email.txt`" > $@
+ printf 'Expires: %s\n' "`cat expiry.txt`" >> $@
+ printf 'Encryption: %s/$(PUBURL)\n' "`cat baseurl.txt`" >> $@
+ printf 'Preferred-Languages: en, pt, fr, eo, es, de\n' >> $@
+
+src/sources.txt:
+ printf '%s\n' $(sources) > $@
+
+src/all-contents.txt:
+ printf '%s\n' $(all-contents) > $@
+
+src/all-filelists.txt: src/all-symlinks.txt
+ printf '%s\n' $(all-filelists) src/all-symlinks.txt > $@
+
+src/all-symlinks.txt: src/content/.gitignore
+ sed 's|^|src/content|' src/content/.gitignore > $@
+
+src/install.txt: src/all-contents.txt src/all-filelists.txt $(all-filelists)
+ cat src/all-contents.txt `cat src/all-filelists.txt` > $@
+
+sources.txt: src/sources.txt
+install.txt: src/install.txt
+sources.txt install.txt:
+ sed 's|^src/content/||' src/$(@F) > $@
+
+
+
+## ---- check-unit-* targets ---------------------------------------
+
+src/sort-expected.txt:
+ dirname $(articles.adoc) | env LANG=POSIX.UTF-8 sort | uniq -c | \
+ awk '{ printf "%s\t%s\n", $$2, $$1 }' > $@
+
+src/sort-given.txt: $(sources.conf) src/sort-expected.txt
+ awk '{ \
+ "grep \"^export sort=\" " $$1 "/*.conf | wc -l" | getline cnt; \
+ printf "%s\t%s\n", $$1, cnt; \
+ }' src/sort-expected.txt > $@
+
+check-unit-sorting: src/sort-expected.txt src/sort-given.txt
+ diff -U10 src/sort-expected.txt src/sort-given.txt
+
+
+.SUFFIXES: .updatedat-check
+sources.updatedat-check = $(sources.adoc:.adoc=.updatedat-check)
+$(sources.updatedat-check):
+ . ./$*.conf && if [ -n "$$updatedat_epoch" ] && \
+ [ "$$updatedat_epoch" -le "$$date_epoch" ]; then exit 3; fi
+
+check-unit-updatedat: $(sources.updatedat-check)
+
+
+.SUFFIXES: .links-internal-check
+sources.links-internal-check = $(sources.adoc:.adoc=.links-internal-check)
+$(sources.links-internal-check): $(sources.html) $(slides.pdf)
+ grep -E '^(link|image):' $*.links | cut -d: -f2- | \
+ xargs -I% test -e $(*D)/%
+
+check-unit-links-internal: $(sources.links-internal-check)
+
+
+check-unit-links-external:
+
+
+symlink-deps = \
+ $(sources.html) \
+ $(feeds.xml) \
+ $(categories.xml) \
+ src/content/.well-known/security.txt \
+ $(sources.media.torrent) \
+
+check-unit-links-symlinks: src/all-symlinks.txt $(symlink-deps)
+ find `cat src/all-symlinks.txt` | xargs -n1 test -e
+
+
+check-unit-links: check-unit-links-internal check-unit-links-external
+check-unit-links: check-unit-links-symlinks
+
+
+MAXSIZE = 52428800 # from spec: https://www.sitemaps.org/protocol.html
+check-unit-sitemap-size: src/content/sitemap.xml
+ test "`stat --printf='%s' src/content/sitemap.xml`" -le $(MAXSIZE)
+
+check-unit-sitemap-count: src/content/sitemap.xml
+ test "`grep -cF '</url>' src/content/sitemap.xml`" -le 50000
+
+check-unit-sitemap: check-unit-sitemap-count check-unit-sitemap-size
+
+
+check-unit-expiry: expiry-epoch.txt now.txt
+ test "`cat expiry-epoch.txt`" -gt "`cat now.txt`"
+
+
+check-unit: check-unit-sorting check-unit-updatedat check-unit-links
+check-unit: check-unit-sitemap check-unit-expiry
+
+
+## ---- standard housekeeping --------------------------------------
+
+## Remove all derived artifacts produced during the build.
+clean:
+ rm -rf $(derived-assets) $(side-assets)
+
+
+## Install into $(DESTDIR)$(PREFIX).
+install: all
+ rsync --mkpath -a --files-from=install.txt src/content/ \
+ '$(DESTDIR)$(HTMLDIR)'
+ rsync --mkpath -a --files-from=sources.txt src/content/ \
+ '$(DESTDIR)$(SRCDIR)'
+
+## Remove what install placed.
+uninstall:
+ rm -rf \
+ '$(DESTDIR)$(SRCDIR)' \
+ '$(DESTDIR)$(HTMLDIR)' \
+
+
+ALWAYS:
diff --git a/src/rules.in b/src/rules.in
new file mode 100755
index 0000000..b998091
--- /dev/null
+++ b/src/rules.in
@@ -0,0 +1,6 @@
+#!/bin/sh
+# Print the absolute path of the canonical Makefile rules shipped
+# alongside mkwb. Sites consume it with:
+# include $(shell mkwb rules)
+# in their top-level Makefile.
+echo '@SHAREDIR@/mkwb/site.mk'
diff --git a/tests/integration.sh b/tests/integration.sh
new file mode 100755
index 0000000..b8b7c1f
--- /dev/null
+++ b/tests/integration.sh
@@ -0,0 +1,94 @@
+#!/bin/sh
+# Integration test: drives mkwb against a slim fixture site and
+# diffs every output against committed golden files under
+# tests/resources/expected/. Run in an isolated tmpdir so the
+# snapshot fixture (tests/resources/site/) stays clean.
+#
+# Required tools on PATH: mkwb (with all libexec scripts) and adoc.
+set -eu
+
+HERE="$(dirname "$(readlink -f "$0")")"
+RESOURCES="$HERE/resources"
+SITE="$RESOURCES/site"
+EXPECTED="$RESOURCES/expected"
+
+WORK="$(mktemp -d -t mkwb-integration.XXXXXX)"
+trap 'rm -rf "$WORK"' EXIT INT TERM
+
+cp -r "$SITE/." "$WORK/"
+
+CONTENT="$WORK/src/content"
+ACTUAL="$WORK/actual"
+mkdir -p "$ACTUAL"
+
+# --- single-page subcommands -------------------------------------
+# htmlbody: .adoc -> HTML body fragment
+mkwb htmlbody "$CONTENT/hello.adoc" > "$ACTUAL/hello.htmlbody"
+
+# links: extract :attr: link values from the .adoc
+mkwb links "$CONTENT/hello.adoc" > "$ACTUAL/hello.links"
+
+# snippets: emits one .txt per "----" block next to the .adoc, and
+# prints the list of filenames to stdout. Run from $CONTENT so the
+# filenames are relative.
+( cd "$CONTENT" && mkwb snippets hello.adoc ) > "$ACTUAL/hello.snippets"
+cp "$CONTENT/hello.html.0.txt" "$ACTUAL/hello.html.0.txt"
+cp "$CONTENT/hello.html.1.txt" "$ACTUAL/hello.html.1.txt"
+
+# --- sortdata: per-collection chronological sort -----------------
+# Each <collection>/YYYY/MM/DD/X.conf gets a sibling sentinel three
+# levels up at <collection>/${date_iso}-${sort}.sortdata pointing
+# back to the original .sortdata. Sorting these sentinel filenames
+# alphabetically yields the chronological order per collection.
+for conf in \
+ "$CONTENT/en/blog/2024/01/15/launch.conf" \
+ "$CONTENT/en/blog/2025/03/20/feature.conf" \
+ "$CONTENT/en/news/2024/06/01/founding.conf" \
+ "$CONTENT/en/news/2025/06/01/anniversary.conf" \
+ ; do
+ ( cd "$WORK" && mkwb sortdata "${conf#$WORK/}" ) > /dev/null
+done
+
+# --- diff every captured output against goldens ------------------
+fail=0
+diff_one() {
+ if diff -u "$EXPECTED/$1" "$2"; then
+ echo "PASS: $1"
+ else
+ echo "FAIL: $1 differs from expected" >&2
+ fail=1
+ fi
+}
+
+for f in hello.htmlbody hello.links hello.snippets \
+ hello.html.0.txt hello.html.1.txt; do
+ diff_one "$f" "$ACTUAL/$f"
+done
+
+# Diff each sentinel file content (points back to .sortdata).
+for f in src/content/en/blog/2024-01-15-launch.sortdata \
+ src/content/en/blog/2025-03-20-feature.sortdata \
+ src/content/en/news/2024-06-01-founding.sortdata \
+ src/content/en/news/2025-06-01-anniversary.sortdata; do
+ diff_one "$f" "$WORK/$f"
+done
+
+# Verify the FILENAME order per collection encodes chronological
+# order -- sort all *.sortdata under each collection and check the
+# listing matches expected.
+for col in blog news; do
+ want="$(cd "$EXPECTED/src/content/en/$col" && \
+ ls *.sortdata 2>/dev/null | sort)"
+ got="$(cd "$WORK/src/content/en/$col" && \
+ ls *.sortdata 2>/dev/null | sort)"
+ if [ "$want" = "$got" ]; then
+ echo "PASS: sortdata order in collection '$col'"
+ else
+ echo "FAIL: sortdata order in collection '$col'" >&2
+ echo " expected:"; echo "$want" | sed 's/^/ /'
+ echo " actual: "; echo "$got" | sed 's/^/ /'
+ fail=1
+ fi
+done
+
+exit "$fail"
diff --git a/tests/resources/expected/hello.html.0.txt b/tests/resources/expected/hello.html.0.txt
new file mode 100644
index 0000000..3307d1d
--- /dev/null
+++ b/tests/resources/expected/hello.html.0.txt
@@ -0,0 +1,4 @@
+{
+ "key": "value",
+ "n": 42
+}
diff --git a/tests/resources/expected/hello.html.1.txt b/tests/resources/expected/hello.html.1.txt
new file mode 100644
index 0000000..3b18e51
--- /dev/null
+++ b/tests/resources/expected/hello.html.1.txt
@@ -0,0 +1 @@
+hello world
diff --git a/tests/resources/expected/hello.htmlbody b/tests/resources/expected/hello.htmlbody
new file mode 100644
index 0000000..34badfb
--- /dev/null
+++ b/tests/resources/expected/hello.htmlbody
@@ -0,0 +1,54 @@
+ <div id="preamble">
+ <div class="sectionbody">
+ <div class="paragraph">
+ <p>A minimal asciidoc page used by mkwb’s integration test.</p>
+ </div>
+ </div>
+ </div>
+ <div class="sect1">
+ <h2 id="section">Section<a class="anchor"
+ href="#section"></a></h2>
+ <div class="sectionbody">
+ <div class="ulist">
+ <ul>
+ <li>
+ <p>item one</p>
+ </li>
+ <li>
+ <p>item two</p>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ <div class="sect1">
+ <h2 id="snippet">Snippet<a class="anchor"
+ href="#snippet"></a></h2>
+ <div class="sectionbody">
+ <div class="listingblock">
+ <div class="content">
+ <pre>{
+ "key": "value",
+ "n": 42
+}</pre>
+ </div>
+ </div>
+ <div class="paragraph plaintext">
+ <p><a href="hello.html.0.txt">plaintext</a></p>
+ </div>
+ </div>
+ </div>
+ <div class="sect1">
+ <h2 id="another-snippet">Another snippet<a class="anchor"
+ href="#another-snippet"></a></h2>
+ <div class="sectionbody">
+ <div class="listingblock">
+ <div class="content">
+ <pre>hello world</pre>
+ </div>
+ </div>
+ <div class="paragraph plaintext">
+ <p><a href="hello.html.1.txt">plaintext</a></p>
+ </div>
+ </div>
+ </div>
diff --git a/tests/resources/expected/hello.links b/tests/resources/expected/hello.links
new file mode 100644
index 0000000..222889b
--- /dev/null
+++ b/tests/resources/expected/hello.links
@@ -0,0 +1,2 @@
+https://example.com/docs
+https://www.rfc-editor.org/rfc/rfc7230
diff --git a/tests/resources/expected/hello.snippets b/tests/resources/expected/hello.snippets
new file mode 100644
index 0000000..87c2af9
--- /dev/null
+++ b/tests/resources/expected/hello.snippets
@@ -0,0 +1,2 @@
+hello.html.0.txt
+hello.html.1.txt
diff --git a/tests/resources/expected/src/content/en/blog/2024-01-15-launch.sortdata b/tests/resources/expected/src/content/en/blog/2024-01-15-launch.sortdata
new file mode 100644
index 0000000..39e52c0
--- /dev/null
+++ b/tests/resources/expected/src/content/en/blog/2024-01-15-launch.sortdata
@@ -0,0 +1 @@
+src/content/en/blog/2024/01/15/launch.sortdata
diff --git a/tests/resources/expected/src/content/en/blog/2025-03-20-feature.sortdata b/tests/resources/expected/src/content/en/blog/2025-03-20-feature.sortdata
new file mode 100644
index 0000000..e8a1011
--- /dev/null
+++ b/tests/resources/expected/src/content/en/blog/2025-03-20-feature.sortdata
@@ -0,0 +1 @@
+src/content/en/blog/2025/03/20/feature.sortdata
diff --git a/tests/resources/expected/src/content/en/news/2024-06-01-founding.sortdata b/tests/resources/expected/src/content/en/news/2024-06-01-founding.sortdata
new file mode 100644
index 0000000..72d71a6
--- /dev/null
+++ b/tests/resources/expected/src/content/en/news/2024-06-01-founding.sortdata
@@ -0,0 +1 @@
+src/content/en/news/2024/06/01/founding.sortdata
diff --git a/tests/resources/expected/src/content/en/news/2025-06-01-anniversary.sortdata b/tests/resources/expected/src/content/en/news/2025-06-01-anniversary.sortdata
new file mode 100644
index 0000000..185cf9b
--- /dev/null
+++ b/tests/resources/expected/src/content/en/news/2025-06-01-anniversary.sortdata
@@ -0,0 +1 @@
+src/content/en/news/2025/06/01/anniversary.sortdata
diff --git a/tests/resources/site/src/content/en/blog/2024/01/15/launch.adoc b/tests/resources/site/src/content/en/blog/2024/01/15/launch.adoc
new file mode 100644
index 0000000..9360a9d
--- /dev/null
+++ b/tests/resources/site/src/content/en/blog/2024/01/15/launch.adoc
@@ -0,0 +1,4 @@
+= Launch
+:updatedat: 2024-01-15
+
+We launched.
diff --git a/tests/resources/site/src/content/en/blog/2024/01/15/launch.conf b/tests/resources/site/src/content/en/blog/2024/01/15/launch.conf
new file mode 100644
index 0000000..70163d5
--- /dev/null
+++ b/tests/resources/site/src/content/en/blog/2024/01/15/launch.conf
@@ -0,0 +1,2 @@
+date_iso=2024-01-15
+sort=launch
diff --git a/tests/resources/site/src/content/en/blog/2025/03/20/feature.adoc b/tests/resources/site/src/content/en/blog/2025/03/20/feature.adoc
new file mode 100644
index 0000000..681656f
--- /dev/null
+++ b/tests/resources/site/src/content/en/blog/2025/03/20/feature.adoc
@@ -0,0 +1,4 @@
+= Feature
+:updatedat: 2025-03-20
+
+We shipped a feature.
diff --git a/tests/resources/site/src/content/en/blog/2025/03/20/feature.conf b/tests/resources/site/src/content/en/blog/2025/03/20/feature.conf
new file mode 100644
index 0000000..aead7c2
--- /dev/null
+++ b/tests/resources/site/src/content/en/blog/2025/03/20/feature.conf
@@ -0,0 +1,2 @@
+date_iso=2025-03-20
+sort=feature
diff --git a/tests/resources/site/src/content/en/news/2024/06/01/founding.adoc b/tests/resources/site/src/content/en/news/2024/06/01/founding.adoc
new file mode 100644
index 0000000..ab650b8
--- /dev/null
+++ b/tests/resources/site/src/content/en/news/2024/06/01/founding.adoc
@@ -0,0 +1,4 @@
+= Founding
+:updatedat: 2024-06-01
+
+Org founded.
diff --git a/tests/resources/site/src/content/en/news/2024/06/01/founding.conf b/tests/resources/site/src/content/en/news/2024/06/01/founding.conf
new file mode 100644
index 0000000..8504263
--- /dev/null
+++ b/tests/resources/site/src/content/en/news/2024/06/01/founding.conf
@@ -0,0 +1,2 @@
+date_iso=2024-06-01
+sort=founding
diff --git a/tests/resources/site/src/content/en/news/2025/06/01/anniversary.adoc b/tests/resources/site/src/content/en/news/2025/06/01/anniversary.adoc
new file mode 100644
index 0000000..de32ee0
--- /dev/null
+++ b/tests/resources/site/src/content/en/news/2025/06/01/anniversary.adoc
@@ -0,0 +1,4 @@
+= Anniversary
+:updatedat: 2025-06-01
+
+One year in.
diff --git a/tests/resources/site/src/content/en/news/2025/06/01/anniversary.conf b/tests/resources/site/src/content/en/news/2025/06/01/anniversary.conf
new file mode 100644
index 0000000..01e24d9
--- /dev/null
+++ b/tests/resources/site/src/content/en/news/2025/06/01/anniversary.conf
@@ -0,0 +1,2 @@
+date_iso=2025-06-01
+sort=anniversary
diff --git a/tests/resources/site/src/content/hello.adoc b/tests/resources/site/src/content/hello.adoc
new file mode 100644
index 0000000..ef6ac5b
--- /dev/null
+++ b/tests/resources/site/src/content/hello.adoc
@@ -0,0 +1,25 @@
+= Hello
+:upstream-docs: https://example.com/docs
+:rfc-link: https://www.rfc-editor.org/rfc/rfc7230
+
+A minimal asciidoc page used by mkwb's integration test.
+
+== Section
+
+* item one
+* item two
+
+== Snippet
+
+----
+{
+ "key": "value",
+ "n": 42
+}
+----
+
+== Another snippet
+
+----
+hello world
+----