Happy New Year, Internet! What did you do to your website today?
Iāve been refining my tooling and figured out how to replace a shell script I had written to build pages with logic embedded directly in the makefile.
#
# PROCESS WEB PAGES
#
html: $(DST_HTML) ## Process all HTML files
$(DST_DIR)/%.html: $(SRC_DIR)/%.html $(SRC_DIR)/%.sed | directories
$(eval SED_FILE := $(subst html,sed,$<))
$(eval LAYOUT_FILE := $(shell grep __LAYOUT $(SED_FILE) | cut -d'|' -f3))
sed -e "/__PAGE_CONTENT/r $<" -e "//d" $(LAYOUT_FILE) \
| hxincl -x -f -b ./includes/ \
| m4 -Q -P -E -I ./includes/ \
| sed -f $(SED_FILE) -f external-links.sed -f inspirations.sed -f projects.sed -f common.sed -f $(SED_FILE) \
-e "s|__UPDATED|$(UPDATED_DATE)|g" \
-e "s|__ISO_UPDATED|$(UPDATED_ISO)|g" \
-e "s|__DISPLAY_UPDATED|$(UPDATED_DISPLAY)|g" \
-e "s|__YEAR|$(YEAR)|g" \
> "$@"
tidy -config $(TIDY_HTML) -m "$@" 2> /dev/null || true
sed -i -f post-tidy.sed "$@"
(The code above wonāt make sense if you donāt speak UNIX, but you donāt have to speak UNIX to make your own website. Donāt mistake my psychosis for best practice.)
That pipeline does look a little complicated, to be fair. ![]()
Thatās entirely fair, and it doesnāt help that Iām also using pipe delimiters in my sed expressions even for dates, where I can get away with using the usual slashes instead.
But I will never again have to deal with npm dependency hell when Iām not getting paid. ![]()
By some miracle, I have made an update to one of my sites, more specifically Velvouetteās Smoking Lounge
The art gallery is now optimized for different screen sizes and easier for me to add more art in the future, and also a great blank slate for when I inevitably make another complicated revamp.
Youāre a monster! Cool though, this makefile would probably work nearly as-is on a 1998 stack.
Thanks. Iām glad there are people here who get it, though itās OK if most people donāt want to be this hardcore.
Iām tempted to find ISO images of Red Hat 5.2 and install them in a virtual machine to put that to the test, but that would probably only impress the sort of people who hold forth on Hacker News, and I donāt care about impressing them. Theyād say, āwell, acksually, you could just use Hugo or 11tyā.
Not to dunk on people who use Hugo or 11ty, but I want a toolchain that is likely to still work as-is in 2076 if by some malignant miracle I live that long (Iād be 98 then).
Besides, the full makefile uses avifenc, which wasnāt around in 1998 when PNG was the new hotness. It also uses pagefind to provide local site search, and Rust wasnāt a thing in 1998 either.
oedipus.mk in its entirety
#
# oedipus.mk
# Ā© 2026 Matthew Cambion <contact@starbreaker.org>
# Available under the terms of the GNU General Public License v3
# (see LICENSE.txt for details)
#
# This makefile builds a motherfucking website.
# No JavaScript. No PHP. No Node. No Python. No Ruby. No ASP.NET. No JSP. No bullshit.
# You could run this on a Thinkpad T60; I wrote this on a T60 myself, and it'll probably run on a 486.
# If it runs GNU make, it will run this. Just don't expect this to work with POSIX make because I use GNU extensions.
#
# Sites built with oedipus.mk are searchable if you install and use the Rust version of pagefind and provide a search page.
# It provides support for modern image formats (AVIF) using avifenc.
# Metadata is handled with sed.
# Deployment is handled by rsync over SSH.
# No need for CI, or a database-driven CMS running on the destination server.
#
#
# VARIABLES
# make only knows how to deal with C sources and headers by default.
# For other files, like HTML, CSS, and images, it needs explicit instructions.
# Let's abstract that out into a separate file, along with SSH variables.
#
include make.config
#
# set PHONY targets just in case
#
.PHONY: build directories html stylesheets images robots favicons sitemap rssitem rssfeed \
htaccess avatar search install downloads archive serve clean help text media
#
# DEFAULT ACTION
# make always treats the first target as the default
#
build: text media ## Build the whole website
text: html rssitem rssfeed stylesheets robots htaccess sitemap ## Build textual parts of the website
media: images favicons avatar downloads ## Build multimedia parts of the website
#
# FORCE REBUILD AFTER CHANGING INCLUDES, M4 MACROS, OR common.sed
#
.PHONY: rebuild
rebuild: ## Force a full rebuild of the website
$(MAKE) -B -j$(shell nproc) text
.PHONY: rebuild-all
rebuild-all: ## Force a full rebuild of the website
$(MAKE) -B -j$(shell nproc) text media
#
# CREATE DESTINATION PATHS
#
directories: ## Make directory paths under website/
mkdir -p $(DST_DIR)/.well-known $(TMP_DIR) $(RSSXML_DIR)
rsync -a --include='*/' --exclude='*' "${SRC_DIR}/" "${DST_DIR}/"
rsync -a --include='*/' --exclude='*' "${SRC_DIR}/" "${TMP_DIR}/"
#
# PROCESS WEB PAGES
#
html: $(DST_HTML) ## Process all HTML files
$(DST_DIR)/%.html: $(SRC_DIR)/%.html $(SRC_DIR)/%.sed | directories
$(eval SED_FILE := $(subst html,sed,$<))
$(eval LAYOUT_FILE := $(shell grep __LAYOUT $(SED_FILE) | cut -d'|' -f3))
sed -e "/__PAGE_CONTENT/r $<" -e "//d" $(LAYOUT_FILE) \
| hxincl -x -f -b ./includes/ \
| m4 -Q -P -E -I ./includes/ \
| sed -f $(SED_FILE) -f external-links.sed -f tools.sed -f inspirations.sed -f projects.sed -f common.sed -f $(SED_FILE) \
-e "s/__UPDATED/$(UPDATED_DATE)/g" \
-e "s/__ISO_UPDATED/$(UPDATED_ISO)/g" \
-e "s/__DISPLAY_UPDATED/$(UPDATED_DISPLAY)/g" \
-e "s/__YEAR/$(YEAR)/g" \
> "$@"
tidy -config $(TIDY_HTML) -m "$@" 2> /dev/null || true
sed -i -f post-tidy.sed "$@"
#
# PROCESS RSS ITEMS
#
rssitem: $(TMP_RSS_XML) ## create RSS <item> files for inclusion in RSS feeds
$(TMP_DIR)/%.xml: $(SRC_DIR)/%.html $(SRC_DIR)/%.sed | directories html
$(eval WEB_PAGE := $(subst $(SRC_DIR),$(DST_DIR),$<))
$(eval SED_FILE := $(subst html,sed,$<))
./rssitem.sh $(WEB_PAGE) | sed -f $(SED_FILE) > "$@"
install -m 644 "$@" $(RSSXML_DIR)/$(shell echo "__RSS_GUID" | sed -f $(SED_FILE) -f common.sed -e "s/https:\/\///g" -e "s/\//_/g" -e "s/[[:punct:]]\+/_/g" -e "s/tag_starbreaker_org_//g" -e "s/_html/.xml/g")
#
# PROCESS RSS FEEDS
#
rssfeed: $(DST_FEED) ## process RSS feeds using <item> partials
$(DST_DIR)/%.xml: $(SRC_DIR)/%.xml | directories rssitem
$(eval SED_FILE := $(subst html,sed,$<))
hxincl -x -f -b ./rssxml/ "$<" \
| sed -f $(SED_FILE) -f external-links.sed -f inspirations.sed -f projects.sed -f common.sed \
-e "s|__UPDATED|$(UPDATED_DATE)|g" \
-e "s|__ISO_UPDATED|$(UPDATED_ISO)|g" \
-e "s|__DISPLAY_UPDATED|$(UPDATED_DISPLAY)|g" \
-e "s|__YEAR|$(YEAR)|g" \
> "$@"
tidy -config $(TIDY_XML) -m "$@" 2> /dev/null || true
sed -i 's/<!--/\n\t<!--/g' "$@"
#
# INSTALL STYLESHEETS
#
stylesheets: $(DST_STYLES) ## Install stylesheets
$(DST_STYLES_DIR)/%: $(SRC_STYLES_DIR)/% | directories
install -m 644 "$<" "$@"
#
# IMAGE PROCESSING
#
images: $(DST_JPG_AVIF) $(DST_PNG_AVIF) $(DST_JPG) $(DST_PNG) $(DST_SVG) ## Convert legacy image formats to modern ones
$(DST_DIR)/%.avif: $(SRC_DIR)/%.jpg | directories
avifenc -q 80 --ignore-exif --ignore-icc --ignore-xmp "$<" "$@" 1> /dev/null 2> /dev/null
$(DST_DIR)/%.avif: $(SRC_DIR)/%.png | directories
avifenc -q 80 --ignore-exif --ignore-icc --ignore-xmp "$<" "$@" 1> /dev/null 2> /dev/null
$(DST_DIR)/%.jpg: $(SRC_DIR)/%.jpg | directories
install -m 644 "$<" "$@"
$(DST_DIR)/%.png: $(SRC_DIR)/%.png | directories
install -m 644 "$<" "$@"
$(DST_DIR)/%.svg: $(SRC_DIR)/%.svg | directories
install -m 644 "$<" "$@"
#
# DIRECTIVES FOR SEARCH ENGINES AND SCRAPERS
#
robots: $(DST_DIR)/robots.txt $(DST_DIR)/ai.txt ## Copy over robots.txt and ai.txt if updated
$(DST_DIR)/robots.txt: $(SRC_DIR)/robots.txt | directories
install -m 644 "$<" "$@"
$(DST_DIR)/ai.txt: $(SRC_DIR)/ai.txt | directories
install -m 644 "$<" "$@"
#
# FAVICONS
#
favicons: $(DST_DIR)/site.webmanifest $(DST_DIR)/apple-touch-icon.png $(DST_DIR)/favicon.ico $(DST_DIR)/card.png
$(DST_DIR)/site.webmanifest: $(SRC_DIR)/site.webmanifest | directories
install -m 644 "$<" "$@"
$(DST_DIR)/apple-touch-icon.png: $(SRC_DIR)/apple-touch-icon.png | directories
install -m 644 "$<" "$@"
$(DST_DIR)/favicon.ico: $(SRC_DIR)/favicon.ico | directories
install -m 644 "$<" "$@"
$(DST_DIR)/card.png: $(SRC_DIR)/card.png | directories
install -m 644 "$<" "$@"
#
# .htaccess
#
htaccess: $(DST_DIR)/.htaccess ## Copy over .htaccess if updated
$(DST_DIR)/.htaccess: $(SRC_DIR)/htaccess.conf | directories
install -m 644 "$<" "$@"
#
# .well-known/avatar
#
avatar: $(DST_DIR)/.well-known/avatar ## this is a non-standard .well-known path implemented with a 301 redirect in .htaccess
$(DST_DIR)/.well-known/avatar: $(SRC_DIR)/assets/images/author.jpg | directories
install -m 644 "$<" "$@"
#
# XML SITEMAP GENERATION
#
sitemap: $(DST_DIR)/sitemap.xml ## use awk to generate a XML sitemap for search engines
$(DST_DIR)/sitemap.xml: $(DST_HTML) | directories html
@printf "%s\n" $^ | awk -v url="$(SITE_URL)" -v root="$(DST_DIR)/" -v updated="$(UPDATED_ISO)" ' \
BEGIN { \
print "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"; \
print "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">"; \
} \
{ \
gsub(root, "", $$0); \
print " <url>"; \
print " <loc>" url "/" $$0 "</loc>"; \
print " <lastmod>" updated "</lastmod>"; \
print " </url>"; \
} \
END { print "</urlset>" }' > $@
#
# SEARCH SUPPORT
#
search: ## implement on-site search using pagefind (in Rust).
~/.cargo/bin/pagefind --site website/
#
# DEPLOYMENT
#
install: build search archive ## build and deploy the site to my hosting provider
@echo "Upload started at: $(shell date)"
rsync --rsh="ssh ${SSH_OPTS}" \
--delete-delay \
--omit-dir-times \
--info=progress2 \
-acvz $(DST_DIR)/ ${SSH_USER}@${SSH_HOST}:${SSH_PATH}
@echo "Upload completed at: $(shell date)"
#
# HELPERS
#
downloads: directories ## Copy over downloads
install -m 644 sources/assets/downloads/* website/assets/downloads/
cp -R includes/ website/assets/downloads/includes
cp -R templates/ website/assets/downloads/templates
install -m 644 *.sh website/assets/downloads/
install -m 644 *.sed website/assets/downloads/
install -m 644 oedipus.mk website/assets/downloads/makefile
install -m 644 make.config website/assets/downloads/make.config
archive: ## Zip up the website for offline reading
exiftool -overwrite_original -ext JPG -ext PNG -ext AVIF -all:all= -r $(DST_DIR)
rm -rf $(TARBALL)
tar -cJf $(TARBALL) website/
serve: ## run a basic HTTP server on localhost
python3 -m http.server -d website/
clean: ## Clean out cruft
rm -rf $(DST_DIR) $(TMP_DIR) $(RSSXML_DIR)
debian: ## Install dependencies on Debian systems (run "apt update && apt install build-essentials" first!!)
sudo apt update
sudo apt upgrade --yes
sudo apt install --yes mawk m4 sed html-xml-utils tidy libavif-bin exiftool cargo rsync xz-utils
cargo install pagefind
# "help" target inspired by James Tomasino (https://labs.tomasino.org/makefiles-for-fun-and-profit/)
help: ## Show this help
@[ "$$NO_CLEAR" = "1" ] || clear
@grep -E -h '\s##\s' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[34m%-15s\033[0m %s\n", $$1, $$2}'
## LOOK UPON MY WORKS, YE MIGHTY, AND DESPAIR! FOR I AM MATTHEW
## CAMBION, AND THIS MAKEFILE WILL WORK FOR AS LONG AS GNU TOOLS AND
## THE LINUX KERNEL REMAIN AVAILABLE.
make.config without SSH variables
#
# make.config file used by oedipus.mk
# Ā© 2025 Matthew Cambion <contact@starbreaker.org>
# Available under the terms of the GNU General Public License v3
# (see LICENSE.txt for details)
#
# base URL (for generating sitemap.xml and possibly RSS feeds)
SITE_URL := https://starbreaker.org
# tarball location for archive
TARBALL := website/assets/downloads/website.tar.xz
# build date in various formats
UPDATED_DATE := $(shell date --rfc-822)
UPDATED_ISO := $(shell date -Iseconds)
UPDATED_DISPLAY := $(shell date +'%A, %e %B %Y')
YEAR := $(shell date +'%Y')
# sources
SRC_DIR := sources
SRC_IMAGES_DIR := $(SRC_DIR)/assets/images
SRC_STYLES_DIR := $(SRC_DIR)/assets/styles
SRC_HTML := $(shell find $(SRC_DIR) -type f -name '*.html')
SRC_SED := $(patsubst $(SRC_DIR)/%.html, $(SRC_DIR)/%.sed, $(SRC_HTML))
SRC_RSS_SED := $(shell grep -r __RSS_GUID $(SRC_DIR) | cut -d: -f1)
SRC_RSS_HTML := $(patsubst $(SRC_DIR)/%.sed, $(SRC_DIR)/%.html, $(SRC_RSS_SED))
SRC_FEED_XML := $(shell find $(SRC_DIR) -type f -name '*.xml')
SRC_FEED_SED := $(patsubst $(SRC_DIR)/%.xml, $(SRC_DIR)/%.sed, $(SRC_FEED_XML))
SRC_STYLES := $(shell find $(SRC_STYLES_DIR) -type f)
SRC_JPG := $(shell find $(SRC_IMAGES_DIR) -type f -name '*.jpg')
SRC_PNG := $(shell find $(SRC_IMAGES_DIR) -type f -name '*.png')
SRC_SVG := $(shell find $(SRC_IMAGES_DIR) -type f -name '*.svg')
# destinations
DST_DIR := website
DST_IMAGES_DIR := $(DST_DIR)/assets/images
DST_STYLES_DIR := $(DST_DIR)/assets/styles
DST_HTML := $(patsubst $(SRC_DIR)/%.html, $(DST_DIR)/%.html, $(SRC_HTML))
DST_FEED := $(patsubst $(SRC_DIR)/%.xml, $(DST_DIR)/%.xml, $(SRC_FEED_XML))
DST_STYLES := $(patsubst $(SRC_DIR)/%, $(DST_DIR)/%, $(SRC_STYLES))
DST_JPG_AVIF := $(patsubst $(SRC_IMAGES_DIR)/%.jpg, $(DST_IMAGES_DIR)/%.avif, $(SRC_JPG))
DST_PNG_AVIF := $(patsubst $(SRC_IMAGES_DIR)/%.png, $(DST_IMAGES_DIR)/%.avif, $(SRC_PNG))
DST_JPG := $(patsubst $(SRC_IMAGES_DIR)/%.jpg, $(DST_IMAGES_DIR)/%.jpg, $(SRC_JPG))
DST_PNG := $(patsubst $(SRC_IMAGES_DIR)/%.png, $(DST_IMAGES_DIR)/%.png, $(SRC_PNG))
DST_SVG := $(patsubst $(SRC_IMAGES_DIR)/%.svg, $(DST_IMAGES_DIR)/%.svg, $(SRC_SVG))
# temp directory
TMP_DIR := tmp
TMP_RSS_XML := $(patsubst $(SRC_DIR)/%.html, $(TMP_DIR)/%.xml, $(SRC_RSS_HTML))
# RSS includes
RSSXML_DIR := rssxml
# SSH settings
SSH_OPTS := -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=10
SSH_USER := LOL_REDACTED
SSH_HOST := LOL_REDACTED
SSH_PATH := LOL_REDACTED
# tidy settings
TIDY_HTML := tidy-html.conf
TIDY_XML := tidy-xml.conf
WARNING: These makefiles require GNU make. While macOS has UNIX utils, they are BSD tools that stick to the POSIX spec with none of GNUās extensions. Youād have to install GNU make, GNU m4, and GNU coreutils and adjust these makefiles to use the GNU versions.
Now that I think of it, I should provide rssitem.sh, too. Aside from shell tools, HTML partials, sed scripts, and M4 macros, itās really the only external dependency oedipus.mk has. And anybody using this might want to build their own templates, partials, sed scripts, and macros to taste.
contents of rssitem.sh
#!/usr/bin/env bash
# rssitem.sh
# Ā© 2026 Matthew Cambion <contact@starbreaker.org>
# available under the terms of the GNU General Public License v3
# Given a raw HTML file let's use hxselect to extract the contents of
# the "#content" selector and wrap it in an <item> element for
# inclusion into one or more RSS feeds.
#
# Because we're generating a heredoc with cat, we can't do this
# directly in a makefile. So we'll wrap it in a shell script and then
# pipe the output through sed and whatever other processing needs
# doing.
#
# Turns out we need to process HTML partials and M4 macros before we
# extract <main id="content"> or <article id="content">. Otherwise, M4
# macros won't get expanded. Oh well. We might as well abstract the
# text from the layout, too. Fortunately, I figured out how to con
# make into getting the corresponding output file for each input HTML
# file, so all of that processing need not happen twice. That's why
# the rssitem target in oedipus.mk depends on both the directories and
# html targets.
HTML_FILE="${1}"
CONTENT=$(sed -f absolute.sed "${HTML_FILE}" | hxclean | hxselect -c "#content")
cat <<EOF
<item>
<pubDate>__CREATED</pubDate>
<title>__TITLE</title>
<link>__BASE/__URL</link>
<author>__AUTHOR_EMAIL (__AUTHOR_NAME)</author>
<description>__DESCRIPTION</description>
<content:encoded>
<![CDATA[
${CONTENT}
<hr />
<p>
Thanks for using <a href="https://aboutfeeds.com/">web feeds</a> to keep up with this website!
</p>
<p>
If youād like to get in touch, please <a href="mailto:__AUTHOR_EMAIL?subject=RE: __TITLE">reply by email</a>.
I can also be reached via <a href="https://signal.org/">Signal</a> by texting
<a href="https://signal.me/#eu/rLUQHAt6iG5v_5Pee2BbSON_aa1t88FhO6GgpJfS_ROOyq43F9NHXJAreegQLw_j">starbreaker.84</a>.
</p>
]]>
</content:encoded>
<guid isPermaLink="false">__RSS_GUID</guid>
</item>
EOF
Oh, and hereās absolute.sed.
contents of absolute.sed
# āWhen in doubt, use brute force.ā āKen Thompson
s|\.\./\.\./\.\./\.\./\.\./|__BASE/|g
s|\.\./\.\./\.\./\.\./|__BASE/|g
s|\.\./\.\./\.\./|__BASE/|g
s|\.\./\.\./|__BASE/|g
s|\.\./|__BASE/|g
s|\./|__BASE/|g
Now that I think of it, I should probably explain basic find/replace with sed for readers who donāt speak UNIX. It works like this:
s/OLD_TEXT/NEW_TEXT/g
- āsā stands for āsubstituteā.
- The ā/ā is a delimiter. You can also use ā|ā or ā:ā if your old or new text contains slashes; having to escape lots of slashes with backslashes is not fun.
- āgā makes the replacement global; by default
sedwould only execute the specified substitution once. If thatās what you want, youād do something like this instead:
s/OLD_TEXT/NEW_TEXT/
So, if you want metadata for a file like sources/index.html, youād create a corresponding sources/index.sed and insert commands like these:
contents of sources/index.sed
s|__LAYOUT|templates/homepage.html|g
s|__TITLE|home page|g
s|__DESCRIPTION|rock operatic science fantasy and other sacraments of defiance|g
s|__CREATED|Fri, 29 May 2020 15:57:26 -0400|g
s|__ISO_CREATED|2020-05-29T15:57:26-04:00|g
s|__DISPLAY_CREATED|Friday, 29 May 2020|g
s|__URL|index.html|g
s|__HEADING_SLUG_01|welcome|g
s|__HEADING_TITLE_01|Welcome š|g
s|__HEADING_SLUG_02|start-here|g
s|__HEADING_TITLE_02|Start Here...|g
s|__HEADING_SLUG_03|grimoire-sections|g
s|__HEADING_TITLE_03|Grimoire Sections|g
s|__HEADING_SLUG_04|offline-reading|g
s|__HEADING_TITLE_04|Offline Reading|g
s|__HEADING_SLUG_05|supporting-real-web|g
s|__HEADING_TITLE_05|Supporting the <em>Real</em> WWW|g
s|__HEADING_SLUG_06|webrings|g
s|__HEADING_TITLE_06|Web Rings|g
s|__HEADING_SLUG_07|linking|g
s|__HEADING_TITLE_07|Linking to __SITE|g
s|__HEADING_SLUG_08|more-slash-pages|g
s|__HEADING_TITLE_08|More Slash Pages|g
s|__RELATIVE|.|g
And with my makefile, if you change either a HTML source or its corresponding sed file, the page will get rebuilt.
Incidentally, this is very similar to how substitutions work in vi-style editors:
# single substitution
:s/OLD_TEXT/NEW_TEXT/
# global substitution
:%s/OLD_TEXT/NEW_TEXT/
# if you forgot how to save and quit vi =^..^=
:wq
Now that I think of it, I should include my macros.m4 file so that people can see what basic M4 looks like:
macros.m4; or, how to do web components without JavaScript and party like it's 1977
m4_divert(-1)
# macros.m4
# Ā© 2026 Matthew Cambion <contact@starbreaker.org>
# available under the terms of the GNU General Public License v3
# All built-in m4 commands are prefixed with "m4_". When using this
# macro file, invoke "m4 -P". Refer to "man m4" for details; this may
# be a GNU extension and not part of BSD m4 as specified in POSIX.
# I prefix my macros with "M4_" (the capitalization matters) because I
# prefix sed targets with "__" and because unprefixed/non-namespaced
# macros are a bitch to debug.
# The default opening and closing quotes for m4 don't make sense when
# working in HTML, and probably not even when writing C code.
m4_changequote(`[', `]')
# Create a semantic HTML heading styled so that the font size is the
# same as body text. Use for headings in <aside> and <nav> blocks.
#
# $1 = heading
# $2 = distinct heading ID and link target
# $3 = heading text
m4_define([M4_HEADING_NO_TOC], [
<h$1 class="no-toc" id="$2">$3</h$1>
])
# Create a semantic HTML heading template.
#
# $1 = heading level
# $2 = heading slug and title to be replaced via sed
m4_define([M4_HEADING], [
<h$1 id="__HEADING_SLUG_$2">
<a href="__RELATIVE/__URL#__HEADING_SLUG_$2">
__HEADING_TITLE_$2
</a>
</h$1>
])
# Create list items in an on-page navigation menu, typically a table of contents.
#
# $1 = heading slug and title to be replaced via sed
m4_define([M4_NAV_ITEM], [
<li><a href="__RELATIVE/__URL#__HEADING_SLUG_$1">__HEADING_TITLE_$1</a></li>
])
# Create a "card" for richer display of a page than a mere link in a
# list item. The link is in the title, we provide a summary, and then
# a permalink.
#
# $1 = page title
# $2 = page summary
# $3 = page URL
m4_define([M4_PAGE_CARD], [
<article>
<header>
<h2 class="no-toc normal"><a href="__RELATIVE/$3"><cite>$1</cite></a></h2>
<p><q lang="__LANG">$2</q></p>
</header>
<footer>
<p>This pageās permalink is <code>__BASE/$3</code>.</p>
</footer>
</article>
])
m4_define([M4_POST_CARD], [
<article>
<header>
<h2 class="no-toc normal"><cite>$4</cite></h2>
<p><q lang="__LANG">$5</q></p>
</header>
<blockquote cite="__BASE/$3">
<p><q>$6</q></p>
</blockquote>
<footer>
<p>This text was published to <a href="__RELATIVE/$3"><code>/$3</code></a> on <time datetime="$1">$2</time>.</p>
</footer>
</article>
])
m4_define([M4_ALBUM_CARD], [
<article>
<header>
__PIC_JPG($8, 200, 200, [cover art for $5 by $4])
<h2 class="no-top-margin">$4: <cite>$5</cite> — $3</h2>
<p>released on <time datetime="$1">$2</time> by <b>$6</b></p>
</header>
</article>
])
m4_define([M4_ALBUM_CARD_LINK], [
<article>
<header>
__PIC_JPG($8, 200, 200, [cover art for $5 by $4])
<h2 class="no-top-margin">$4: <cite>$5</cite> — $3</h2>
<p>released on <time datetime="$1">$2</time> by <b>$6</b></p>
</header>
<footer>
<p>Further details are available at <a href="__RELATIVE/$7"><code>$7</code></a>.
</footer>
</article>
])
m4_define([M4_FICTION_CARD], [
<article>
<header>
<h3 class="no-toc normal"><cite>$1</cite></h3>
<p><q lang="__LANG">$2</q></p>
</header>
<blockquote cite="__BASE/$3">
<p><q lang="__LANG">$4</q></p>
</blockquote>
<footer>
<p>This project is available for viewing at <a href="__RELATIVE/$3"><code>/$3</code></a>.</p>
</footer>
</article>
])
m4_define([M4_CHAPTER_CARD], [
<section>
<header>
<h3 class="no-toc normal"><a href="__RELATIVE/$3">$1</a></h3>
<p>
<i lang="__LANG">$2</i>
</p>
</header>
<blockquote cite="__BASE/$3">
$4
</blockquote>
<footer>
<p>
permalink: <code>__BASE/$3</code>
</p>
</footer>
</section>
])
m4_define([M4_ADDRESS_BLOCK], [
<address aria-label="online contact info">
<ul>
<li>You can send email to <a href="mailto:__AUTHOR_EMAIL">contact [at] starbreaker [dot] org</a>.</li>
<li>For marginally safer and more private (than email) communication Iām on <a href="https://signal.org">Signal</a> as <a href="https://signal.me/#eu/rLUQHAt6iG5v_5Pee2BbSON_aa1t88FhO6GgpJfS_ROOyq43F9NHXJAreegQLw_j">starbreaker [dot] 84</a>.</li>
</ul>
<p>
Under no circumstance should either of these methods be used to discuss anything that might bring unwanted attention from the state.<br>
Think things through before hitting āsendā.
</p>
</address>
])
m4_define([M4_FICTION_NOTES], [
<a href="__RELATIVE/fiction/notes/$1.html">$2</a>
])
m4_define([M4_FEED_ITEM], [
<dt>$1</dt>
<dd>
$2 ( <a href="__RELATIVE/feeds/$1.xml">RSS</a> )
</dd>
])
m4_define([M4_LOGO], [
<a href="__RELATIVE/index.html" title="return to __SITE home page">
<picture>
<source srcset="__RELATIVE/assets/images/starbreaker.avif" />
<img src="__RELATIVE/assets/images/starbreaker.png" width="88" height="31" alt="88x31 button for __SITE" loading="lazy" class="no-margin undecorated">
</picture>
</a>
])
m4_define([M4_LINK_BUTTON], [
<a href="$6" title="$5">
<img class="undecorated" src="__RELATIVE/assets/images/$1.avif" width="$3" height="$4" alt="$5" loading="lazy">
</>
])
m4_define([M4_BUTTON], [
<img class="undecorated" src="__RELATIVE/assets/images/$1.avif" width="$3" height="$4" alt="$5" loading="lazy">
])
m4_define([M4_POSTER], [
<picture>
<source srcset="__RELATIVE/assets/images/$1.avif" />
<img src="__RELATIVE/assets/images/$1.png" width="$2" height="$3" alt="$4" loading="lazy" class="no-margin no-print">
</picture>
])
m4_define([M4_HCARD_PIC_JPG], [
<img class="u-photo" rel="me author" src="__RELATIVE/assets/images/$1.avif" width="$2" height="$3" alt="$4" loading="lazy">
])
m4_define([M4_PIC_JPG], [
<picture>
<source srcset="__RELATIVE/assets/images/$1.avif" />
<img src="__RELATIVE/assets/images/$1.jpg" width="$2" height="$3" alt="$4" loading="lazy">
</picture>
])
m4_define([M4_PIC_PNG], [
<picture>
<source srcset="__RELATIVE/assets/images/$1.avif" />
<img src="__RELATIVE/assets/images/$1.png" width="$2" height="$3" alt="$4" loading="lazy">
</picture>
])
m4_define([M4_FIG_JPG], [
<figure>
M4_PIC_JPG($1, $2, $3, $4)
<figcaption>$5</figcaption>
</figure>
])
m4_define([M4_FIG_PNG], [
<figure>
M4_PIC_PNG($1, $2, $3, $4)
<figcaption>$5</figcaption>
</figure>
])
m4_define([M4_YOUTUBE], [
<figure>
M4_PIC_JPG($3, $4, $5, $2)
<figcaption>
<a href="$1"
title="YouTube: $2"
target="_blank"
rel="noopener noreferrer nofollow">
$2
</a>
</figcaption>
</figure>
])
m4_define([M4_GRIMOIRE], [
<hr>
<aside>
<p>
<strong>This post is <a href="__RELATIVE/grimoire/$1/index.html">an excerpt</a> from <a href="__RELATIVE/grimoire/index.html">my grimoire</a>, and thereās more to find.<strong>
</p>
</aside>
])
m4_define([M4_WEBSITE_ENTRY], [
<dt>
<a href="$2">$1</a>
</dt>
<dd>$3</dd>
])
m4_define([M4_BLOG_ENTRY], [
<dt>
<a href="$2">$1</a>
(<a href="$3" title="web feed for $1">feed</a>)
</dt>
<dd>$4</dd>
])
m4_define([M4_DOWNLOAD], [
<tr>
<td><a href="__RELATIVE/assets/downloads/$1">$1</a></td>
<td>$2</td>
</tr>
])
m4_define([M4_PDF_DOWNLOAD], [
<tr>
<td><a href="__RELATIVE/assets/downloads/$1" title="WARNING: $1 is a PDF document, may require a separate viewer, and may not be accessible to visitors using assistive technologies.">$1</a></td>
<td>$2</td>
</tr>
])
m4_define([M4_POPOVER_BUTTON], [<button popovertarget="$1" popovertargetaction="show">$2</button>])
m4_define([M4_POPOVER_NOTE], [
<dialog popover="auto" id="$1" aria-modal="true">
<aside>
<h2 class="no-toc">$2</h2>
$3
<div class="right">
<button popovertarget="$1" popovertargetaction="hide">close</button>
</div>
</aside>
</dialog>
])
m4_define([M4_POPOVER_LYRIC], [
<dialog popover="auto" id="$1" aria-modal="true">
$2
<div class="right">
<button popovertarget="$1" popovertargetaction="hide">close</button>
</div>
</dialog>
])
m4_define([M4_YOUTUBE], [
<figure>
M4_PIC_JPG($3, $4, $5, $2)
<figcaption>
<a href="$1"
title="YouTube: $2"
target="_blank"
rel="noopener noreferrer nofollow">
$2
</a>
</figcaption>
</figure>
])
m4_divert
Finally, a page template would look something like this.
m4_include(macros.m4)
<!doctype html>
<html lang="__LANG">
<!-- include "head/main.html" -->
<body>
<!-- include nav/skip.html -->
<!-- include "header/home.html" -->
__PAGE_CONTENT
<!-- include "footer/standard.html" -->
</body>
</html>
Those āincludeā comments arenāt server-side includes here; theyāre handled by hxincl. Partials can include partials, incidentally.
I made a whole nother website for my personal artwork, which I scaled back from a ābrandā to a hobby. I do like keeping it separate from my personal web life most of the time, because art is great and all but between teaching it and making it its my other interests that get neglected.
Eventually I want to add webrings and a status.cafe, favicons, really bling it out and all but not today!
I just opened it on my phone and it worked perfectly.
yayyy thanks for telling me!
It looks great! Itās really cool to see some of your work ![]()
I realized that I have been updating my book list faster than any other section on my site. Clearly a new RSS feed for it is necessary. So, I messed around with my Eleventy config to break each of my book reviews into separate pages, and then generated a new RSS feed for them. :)
EDIT: Oh no, I just realized that the dates are defaulting to the first of the month since I donāt list the actual date on my reading listā¦. problem for later
Iāve modified my makefile to cache the most recent git commit for my project repository and information about the current playing song in Audacious.
# get current song info from Audacious
AUD_ARTIST := $(shell audtool current-song-tuple-data artist)
AUD_TITLE := $(shell audtool current-song-tuple-data title)
AUD_ALBUM := $(shell audtool current-song-tuple-data album)
AUD_YEAR := $(shell audtool current-song-tuple-data year)
# get recent git history
LAST_COMMIT := $(shell git log -1 --oneline | tr -d '\n')
I then use sed expressions to inject them into pages as I build them:
#
# PROCESS WEB PAGES
#
html: $(DST_HTML) ## Process all HTML files
$(DST_DIR)/%.html: $(SRC_DIR)/%.html $(SRC_DIR)/%.sed $(DEP_TEMPLATES) $(DEP_INCLUDES) $(DEP_MACROS) $(COMMON_SED_FILE) | directories common.sed
$(eval PAGE_SED_FILE := $(subst html,sed,$<))
$(eval LAYOUT_FILE := $(shell grep __LAYOUT $(PAGE_SED_FILE) | cut -d'|' -f3))
sed -e "/__PAGE_CONTENT/r $<" -e "//d" $(LAYOUT_FILE) \
| hxincl -x -f -b ./includes/ \
| m4 -Q -P -E -I ./includes/ \
| sed -f $(PAGE_SED_FILE) -f $(COMMON_SED_FILE) -f $(PAGE_SED_FILE) \
-e "s/__UPDATED/$(UPDATED_DATE)/g" \
-e "s/__ISO_UPDATED/$(UPDATED_ISO)/g" \
-e "s/__DISPLAY_UPDATED/$(UPDATED_DISPLAY)/g" \
-e "s/__YEAR/$(YEAR)/g" \
-e "s/__LAST_COMMIT/$(LAST_COMMIT)/g" \
-e "s/__AUD_ARTIST/$(AUD_ARTIST)/g" \
-e "s/__AUD_TITLE/$(AUD_TITLE)/g" \
-e "s/__AUD_ALBUM/$(AUD_ALBUM)/g" \
-e "s/__AUD_YEAR/$(AUD_YEAR)/g" \
> "$@"
tidy -config $(TIDY_HTML) -m "$@" 2> /dev/null || true
sed -i -f post-tidy.sed "$@"
sed then replaces variables in my html.
<ul class="no-top-margin no-print">
<li><small>created on <time class="dt-created" datetime="__ISO_CREATED" title="created on __CREATED">__DISPLAY_CREATED</time></small></li>
<li><small>updated on <time class="dt-updated" datetime="__ISO_UPDATED" title="updated on __UPDATED">__DISPLAY_UPDATED</time></small></li>
<li><small>current page permalink: <a href="__BASE/__URL" aria-current="page" title="link to this page on your own site, or (if you must) on your socials">__BASE/__URL</a></small></li>
<li><small>last commit: <code>__LAST_COMMIT</code></small></li>
<li><small>now playing: ā__AUD_TITLEā by __AUD_ARTIST, from <cite>__AUD_ALBUM</cite> (__AUD_YEAR)</small></li>
</ul>
I had also adjusted the makefile so that if I change a template, a partial, or a M4 macro then all pages get rebuilt. However, if I only change a single page or its associated sidecar file (a sed script), then only that page gets rebuilt.
The upside is that when using make, I get incremental builds for free; traditional SSGs like Jekyll, Hugo, and 11ty canāt do that on their own.
Iāve had a busy week so far! I got rid of my /library page (where I tracked my reading without offering any commentary ā which was pretty boring for visitors, IMO) and replaced it with a new /recommendations page for the books, video games, movies, TV shows, and podcasts that I think are worth checking out. I used YAML for my data entry instead of laboriously writing everything in HTML (which means it will be much more manageable than my old Library going forward). The recommendations Iāve made so far are mostly old all-time favourites of mine to get the ball rolling ⦠they will probably seem a bit pedestrian to some, but just know that Iāll be adding in more recommendations for newer stuff you might not have yet encountered as time goes on. ![]()
I also added in a /sitemap page intended for human visitors (because why should bots have all the fun?) and updated my /homepage, which now features a section for my five most-recent recommendations in each category. I also completely rewrote my /bookmarks page; like the recommendations page, it now uses YAML to cut down on the amount of time I have to spend writing or copying/pasting HTML.
I rebuilt leilukin.comās Git repository, after realising that old versions and commits of my website, when I was a much less experienced web weaver, ended up ballooning the .git folder and the size of my repo to whopping 75 MB.
I also decided to move the majority of the images of leilukin.com to a dedicated subdomain for hosting files to avoid inflating the size of the websiteās Git repository.
I love your recommendations page!
Iāve only read a handful of Discworld books - in publication order, up to and including Sourcery - but loved them.
Iām also a big fan of The Expanse TV series. I picked up the first book but didnāt really get into it. Youāve nudged me to give it another try.
I made a Recommendations page for a āprofessionalā website I launched this month (undecided if I want to share it here / link it to my personal site), but it was more about recommending other peopleās work.
I think youāve made a great choice, keeping it simple and easy to maintain. Even though you said you debated adding images, I think it looks great as-is. The icons are cute.
I also like your bookmarks page. Iāve been trying to think how to better use all bookmarks I accumulate and how to surface the interesting things I find throughout the personal web - like a āthis week Iām readingā page - but I like your approach better.
Iām currently in the middle of breaking my website as I rebuild it (I really should set up another branch so I can still post updates while I work on it, but itās the reading experience Iām trying to improve) so have been writing offline. However, Iām loving all these ideas for slash pages, such as yours, and @small_cypress /systems page so looking forward to creating my own ![]()
Despite my deep love of yapping frequently throughout the day, I got incredibly frustrated with the interaction I would get on my Mastodon account from people who found my toots without any context, so I nerfed it and replaced it with a thoughtlog on my site instead.
This is really cool. Iāve been yapping into the void in my notes app of choice for the past few weeks, but planning to migrate them all to a dedicated microblogging section of my site.
I was thinking of hooking it up to post out to micro.blog but, ultimately I just want a place for my daily stray thoughts to live. Iām really enjoying slower ways of interacting online at the moment - forums, blog post responses, email - and I think what youāve done ties into that ![]()
Thank you so much for the feedback, re: my recommendations and bookmarks pages! Iāve tweaked my bookmarks page slightly since I posted my last update; a brief summary now appears when mousing over or tabbing to each link. I did briefly try using CSS accordions to reveal each summary, but I ultimately felt that clicking on each accordion heading added too many steps for users. The mouse-over / tab solution is the simplest and cleanest way to do it, IMO. No clicking involved. I just need to figure out how to make it work for mobile users too⦠I might just have to reveal all summaries for mobile users, even though that would add a lot more clutter to the page.
Looking forward to seeing what you come up with for your own upcoming slash pages! If you want more inspiration for presenting your bookmarks, I really like Robinās approach (and I totally boosted his idea of using colour-coded bullet points to demarcate different kinds of links).
As for novels, I highly recommend checking out more of the Discworld series. The City Watch story arc (which starts with Guards! Guards!) is my favourite, with the Witches story arc coming in at a close second. You get a small taste of the witches in Equal Rites, but Terry didnāt really have them worked all the way out until he got to Wyrd Sisters (which has to be one of the funniest novels Iāve ever read).
And yes, definitely give The Expanse novels another go! The first novel is decent enough, but the second is where it really gets going. In the second novel, you get introduced to Bobbie Draper, Avasarala⦠and more of the political stuff starts coming to the forefront.
