I Wanted A Blog

I'd already been learning Rust for a while, so I spent three days working through Watery Desert's Rust Axum Askama tutorial. It was an excellent playlist that taught me new things about:

  • Axum - web framework

  • Askama - Jinja-like templating engine

  • Tower - middleware library

  • Tower-HTTP - HTTP-specific middleware and utilities

  • Just - Command runner like make

  • VSCode - a code editor

  • and probably more

Then I Just Used AI

I already knew AI was great for learning new things. I use the Claude desktop app pretty often for that. But Claude Code is something completely different - it really does things. I decided to use it to build the blog, taking it slow, one feature at a time, and it just did everything for me...

Ultimately "we" created a self-contained blog engine built in Rust that parses Org-mode files into HTML using orgize with frontmatter support. It renders them through Askama templates served by an Axum web framework. Content, static assets, and the web server itself is packaged into a single binary using rust-embed.

The styling uses a custom CSS design system ported from Protesilaos Stavrou's ef-maris Emacs theme with rainbow-colored headings. Code blocks get syntect-powered syntax highlighting. A theme toggle switches between dark and light modes with localStorage persistence and system preference detection.

The site has search with server-side matching and context excerpts. Custom typography uses Farro for headings and JetBrains Mono for code. The server runs through tower-http middleware with gzip compression.

Rate limiting protects the site using the tower-governor crate, which applies limits based on the X-Forwarded-For header to properly handle requests coming through Cloudflare and the Caddy reverse proxy.

Posts automatically display last updated timestamps extracted from git commit history - a build script pulls the last commit date for each post and embeds it at compile time, only showing the updated date when it differs from the publication date.

The standalone binary is deployed in an LXC container on my Proxmox home server, behind a Caddy reverse proxy with automatic HTTPS, and fronted by Cloudflare.

I also asked Claude Code to create an AGENTS.md file with formatting rules for writing posts. This adds hyperlinks where necessary and formats code fragments.

Claude Code also handled all the git stuff for me - commits, branches, the whole thing. I didn't have to think about version control at all, which is a huge relief.

I had a small home server I bought earlier this year but forgot how I'd set it up. Claude Code helped me figure out what was going on and get everything working again.

The complete source code is available on GitHub.

Hiccups

There were a few hiccups. Once it tried to install Docker in an LXC container, which wasn't necessary. Just asking what it was doing set it back on track though.

Security needed attention. When setting up port forwarding on my router, Claude Code didn't make sure I only opened those ports to Cloudflare, my web reverse-proxy service. I had to catch that myself.

Claude Code doesn't read the AGENTS.md file on startup - only the .claude/CLAUDE.md file. I wanted formatting rules and guidelines in a separate file that Claude Code would follow for every session. The solution was adding instructions to .claude/CLAUDE.md telling it to read the AGENTS.md file at the start of each conversation.

Version upgrades were surprisingly difficult. When I asked Claude Code to upgrade to a major version of Askama, it got really confused about some pretty simple things. It couldn't figure out basic Askama usage patterns until I explicitly explained how the templating worked. This was strange because it had been working with Askama successfully earlier.

When adding rate limiting, Claude Code hallucinated and used a completely wrong method. This was the only straight up hallucination I saw during the project.

Claude Code's world knowledge is outdated. It tried to use once_cell instead of the built-in std::sync::LazyLock that's now available in standard Rust. I had to prompt it to fix this.

Future Ideas For Working With Claude Code

Next time I work with Claude Code, I'm going to ask it to generate more shell scripts instead of running commands directly. Sometimes it's difficult to follow everything it's doing. Having scripts I can review and execute myself would give me better visibility and control over the process.

I also need to find a tool to review the diffs side-by-side. It would be easier to see what's changing.

Adding the exa-mcp server was helpful for getting info on libraries and APIs, so I'll continue doing that.

Starting with the latest versions of libraries would avoid upgrade problems down the line. The version upgrade difficulties showed that Claude Code handles greenfield development better than migrating existing code to new major versions.

Using The Blog Engine

The blog engine is simple to use once it's set up. To run the local development server, just use:

just run

This starts the server locally so you can preview changes before deploying.

To setup a remote Ubuntu server, run:

just setup <IP>

This configures the server infrastructure including Caddy, systemd services, and required directories. After setup is complete, deploy to the server with:

just deploy <IP>

The Just command runner handles all the compilation and deployment steps.

Adding new posts is straightforward - create a new org mode file in the content/posts/ directory with frontmatter like #+TITLE: and #+DATE:. The blog engine automatically picks up new files and renders them. Editing existing posts works the same way - just update the .org file and redeploy.

Everything It Did, Or Walked Me Through

Here's a list of everything Claude Code either implemented directly or guided me through setting up. It's honestly impressive how much ground "we" covered:

Initial Blog Engine Creation

  • Built entire blog from scratch using Rust, Axum, and Askama templating

  • Org-mode support - Parse and render .org files with frontmatter (TITLE, DATE) using orgize

  • Embedded architecture - Single binary with all content and static files baked in using rust-embed

  • Syntax highlighting - Integrated syntect for code blocks in org-mode posts

  • Basic routes - Index page listing posts, individual post pages, static file serving

Theme & Design System

  • Ported ef-maris themes from Emacs - Brought over both dark and light variants from Protesilaos Stavrou's ef-themes

  • Theme toggle - Built functional dark/light mode switcher in header with SVG sun/moon icons

  • Smart theme detection - Respects system preference (prefers-color-scheme) on first visit

  • LocalStorage persistence - Remembers user's theme choice

  • System theme listener - Auto-switches with OS theme if user hasn't manually set preference

  • Smooth transitions - CSS transitions between theme switches

Typography & Color

  • Custom font stack:

  • Rainbow org-mode headings - Implemented colored H1 - H6 using ef-maris palette:

    • H1: green-cooler, H2: blue-warmer, H3: green-warmer

    • H4: cyan, H5: magenta-cooler, H6: blue-cooler

  • Full CSS variables - Comprehensive design system with --fg-primary, --bg-secondary, --color-link, etc.

Search Functionality

  • Server-side search with /search?q=query endpoint

  • Header search form - Integrated into header next to theme toggle

  • Search results page with context excerpts showing ~200 chars around matches

  • HTML entity decoding - Cleaned up entities in search results (quotes, ampersands, etc.)

  • Smart excerpt generation - Shows context around search terms with word boundary detection

UI/UX Polish

  • Aligned header controls - Theme toggle and search button same height

  • Dynamic copyright year - JavaScript-based auto-updating footer

  • Code copy buttons - Added to all code blocks with visual feedback

  • Responsive design - Mobile-friendly search forms and layouts

  • Accessibility - ARIA labels, semantic HTML, keyboard navigation

Deployment Infrastructure

  • LXC container setup in Proxmox with proper configuration

  • Caddy reverse proxy - Automatic HTTPS with Let's Encrypt for wall.ninja

  • systemd service - Auto-restart on failure, proper logging

  • Cloudflare + OPNsense - DNS proxy, DDoS protection, port forwarding

  • Created setup-blog.sh - Automated server provisioning script

  • Created justfile - just deploy command for one-command deployments

  • Cross-compilation setup - cargo-zigbuild + zig for Mac → Linux builds

Performance & Reliability

  • HTTP compression - Gzip compression via tower-http

  • Rate limiting - Implemented with tower-governor using X-Forwarded-For header for proper client identification behind reverse proxy

  • Zero-downtime deploys - systemd automatically restarts service after binary update

Documentation

  • Comprehensive README - Tech stack, project structure, deployment instructions, design credits

  • Cross-compilation docs - Why zig is used and how to set it up

  • Server setup guide - Complete infrastructure documentation

Example Output

Here's an example of what the rendered content looks like:

Headline 2

Headline 3

Headline 4

Headline 5

This is some text with a link. This is some code text.

fn fibonacci(n: u32) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

fn main() {
    let n = 10;
    println!("Fibonacci({}) = {}", n, fibonacci(n));

    // Print the first 20 Fibonacci numbers
    println!("\nFirst 20 Fibonacci numbers:");
    for i in 0..20 {
        println!("F({}) = {}", i, fibonacci(i));
    }
}
HereIsATable
Welcometomyblog
Pleaseenjoyyourstay