Ross's Blog

Welcome to the New Setup

A Brand New Look

If you’ve followed me for any amount of time…

Actually, as an aside… why would you do that? How did you even find this blog?

…anyway, you’ll know that I used to run a different blog format. I was using Hugo as a static site generator, I had a little button to change the CSS on the page, and I had an rss system based on article tags. You might wonder why I decided to shake things up.

Why I moved

I updated Hugo once and the theme stopped working. That was the whole story.

The longer version is that I had built a very fragile system for my blog. I was pushing my blog and theme up to github, where a hook took the artifacts, built them on netlify, and deployed to a little container, with DNS pointed at a secondary cloud provider because of tech stack drift.

What ended up happening was that the base container updated and hugo’s new version had elements that were no longer compatible with the theme I was using (Papermod). I fixed it, but I found the process tedious. It did it again about a month later, and I grew really weary of having to manage dependency hell for what amounted to automated pandoc + css + rss.

In my laziness, I turned to the AI tool Claude, which does a great job of translating pseudocode into bash and asked it to write a script that was similar to luke smith’s lb with a few creature comforts for my taste. For example, Luke’s version will drop you into an html editor. I prefer markdown, so I added a little pandoc thing. I also had it convert the papermod look into CSS. I actually think that the end result looks and functions better. It even works better on lynx browser and newsboat.

Now, instead of having to manage:

I write my stuff in markdown, run a single script and I’m done.

My New Build Pipeline:

Here’s the entire publish flow:

$EDITOR posts/whatever.md
./blog
rsync -a --delete site/ server:/var/www/blog/

That’s it. code looks like this, and links like crt.sh still work the way you’d hope.

The blog code

#!/bin/sh
# blog — build a markdown blog into static HTML + RSS.
#
# Model: posts/ is the single source of truth. This script wipes and rebuilds
# site/ from whatever .md files are in posts/. There is no database and no
# hidden state, so adding/editing/deleting a post is just adding/editing/
# deleting a file and re-running ./blog.
#
# Each post is a markdown file that starts with a header block, then a blank
# line, then the body:
#
#     Title: My First Post
#     Date: 2025-06-29
#
#     Body markdown starts here...
#
# Dependencies: a POSIX shell + one markdown converter (lowdown, cmark, smu,
# markdown, or pandoc — whichever is found first). Nothing auto-updates; vendor
# this script into your repo and it is frozen.

set -eu

# ---- config (override via env, e.g. URL=https://x.com ./blog) ----------------
SITE=${SITE:-"Your Blog"}
URL=${URL:-"https://www.your-url.com"}            # no trailing slash
DESC=${DESC:-"A description of your blog."}
SRC=${SRC:-posts}                                  # markdown lives here
OUT=${OUT:-site}                                   # generated site (do not edit)
CSS=${CSS:-style.css}                              # your theme
TAB=$(printf '\t')

# ---- helpers -----------------------------------------------------------------

# field FILE KEY  -> value of "KEY:" from the header block (before first blank).
field() {
    awk -v k="$2" '
        { sub(/\r$/, "") }
        /^[[:space:]]*$/ { exit }
        { i = index($0, ": ")
          if (i > 0 && substr($0, 1, i-1) == k) { print substr($0, i+2); exit } }
    ' "$1"
}

# body FILE  -> everything after the first blank line (the markdown body).
body() {
    awk '
        seen { sub(/\r$/, ""); print; next }
        /^[[:space:]]*$/ { seen = 1 }
    ' "$1"
}

# esc  -> escape &, <, > from stdin (for plain-text fields in HTML/XML).
esc() { sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g'; }

# render FILE  -> convert the markdown body to an HTML fragment.
render() {
    tmp=$(mktemp)
    body "$1" >"$tmp"
    if   command -v lowdown  >/dev/null 2>&1; then lowdown -Thtml "$tmp"
    elif command -v cmark    >/dev/null 2>&1; then cmark "$tmp"
    elif command -v smu      >/dev/null 2>&1; then smu "$tmp"
    elif command -v markdown >/dev/null 2>&1; then markdown "$tmp"
    elif command -v pandoc   >/dev/null 2>&1; then pandoc -f markdown -t html "$tmp"
    else
        echo "blog: no markdown converter found (install lowdown, cmark, or smu)" >&2
        rm -f "$tmp"; exit 1
    fi
    rm -f "$tmp"
}

# rfc822 YYYY-MM-DD  -> RFC-822 date for RSS (GNU, then BSD, then raw fallback).
rfc822() {
    date -u -d "$1" +'%a, %d %b %Y 00:00:00 +0000' 2>/dev/null \
        || date -u -j -f '%Y-%m-%d' "$1" +'%a, %d %b %Y 00:00:00 +0000' 2>/dev/null \
        || printf '%s' "$1"
}

# list  -> "DATE<tab>FILE" for every post, newest first.
list() {
    for f in "$SRC"/*.md; do
        [ -e "$f" ] || continue
        printf '%s\t%s\n' "$(field "$f" Date)" "$f"
    done | sort -r
}

# ---- templates (this + style.css is your whole "theme") ----------------------

page_top() { # page_top  TITLE
    cat <<EOF
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#2e2e33">
<title>$1</title>
<link rel="stylesheet" href="/style.css">
<link rel="alternate" type="application/rss+xml" title="$SITE" href="/rss.xml">
</head>
<body>
<header class="site"><a href="/">$SITE</a></header>
<main>
EOF
}

page_bottom() {
    cat <<EOF
</main>
<footer class="site">© $(date +%Y) $SITE · <a href="/rss.xml">rss</a></footer>
</body>
</html>
EOF
}

# ---- builders ----------------------------------------------------------------

build_post() { # build_post  FILE
    f=$1
    name=$(basename "$f" .md)
    d=$(field "$f" Date)
    te=$(field "$f" Title | esc)
    {
        page_top "$te — $SITE"
        printf '<article>\n<h1>%s</h1>\n<time datetime="%s">%s</time>\n' "$te" "$d" "$d"
        render "$f"
        printf '\n</article>\n'
        page_bottom
    } >"$OUT/posts/$name.html"
}

build_index() {
    {
        page_top "$SITE"
        printf '<ul class="index">\n'
        list | while IFS="$TAB" read -r d f; do
            [ -n "$f" ] || continue
            name=$(basename "$f" .md)
            te=$(field "$f" Title | esc)
            printf '<li class="entry"><time>%s</time> <a href="/posts/%s.html">%s</a></li>\n' \
                "$d" "$name" "$te"
        done
        printf '</ul>\n'
        page_bottom
    } >"$OUT/index.html"
}

build_rss() {
    {
        printf '<?xml version="1.0" encoding="UTF-8"?>\n<rss version="2.0">\n<channel>\n'
        printf '<title>%s</title>\n<link>%s/</link>\n<description>%s</description>\n' \
            "$(printf '%s' "$SITE" | esc)" "$URL" "$(printf '%s' "$DESC" | esc)"
        list | while IFS="$TAB" read -r d f; do
            [ -n "$f" ] || continue
            name=$(basename "$f" .md)
            te=$(field "$f" Title | esc)
            printf '<item>\n<title>%s</title>\n<link>%s/posts/%s.html</link>\n' "$te" "$URL" "$name"
            printf '<guid>%s/posts/%s.html</guid>\n<pubDate>%s</pubDate>\n' "$URL" "$name" "$(rfc822 "$d")"
            printf '<description><![CDATA['
            render "$f" | sed 's/]]>/]]]]><![CDATA[>/g'
            printf ']]></description>\n</item>\n'
        done
        printf '</channel>\n</rss>\n'
    } >"$OUT/rss.xml"
}

# ---- main --------------------------------------------------------------------

rm -rf "$OUT"
mkdir -p "$OUT/posts"
[ -f "$CSS" ] && cp "$CSS" "$OUT/style.css" && chmod 644 "$OUT/style.css"

count=0
for f in "$SRC"/*.md; do
    [ -e "$f" ] || continue
    build_post "$f"
    count=$((count + 1))
done

build_index
build_rss

printf 'built %d post(s) -> %s/\n' "$count" "$OUT"