blog.bythewood.me
Personal blog, served by a single Rust axum binary. Self-contained: posts, templates, Vite-built static assets, and the binary all live here. Markdown content in content/posts/, no database.
Features
- Markdown posts with YAML frontmatter (title, slug, date, publish_date, tags, description, cover_image)
- Server-rendered tag and year archives
- Server-rendered + live JSON search (
/search/and/search/live/) - Per-post PDF export via embedded Typst (no chromium subprocess)
- Per-post raw markdown download
- Dynamic OG image generation per post
- Single-binary deploy via
git push server master
Stack
| Concern | Crate / Tool |
|---|---|
| Web framework | axum + tokio |
| Template engine | minijinja |
| Markdown | comrak |
| embedded Typst (no chromium) | |
| Static assets | Vite + Bun |
Crate selection rationale: axum is the most-pulled async framework, minijinja is the only Rust engine that accepts upstream Jinja2 syntax, comrak’s partial-formatter story is the closest match to Mistune’s renderer-override pattern.
System dependencies
Local dev needs all of these on your PATH:
| Tool | Why | Version |
|---|---|---|
rustc / cargo | Build the axum binary | 2021 edition, current stable is fine (1.70+) |
bun | Frontend deps + Vite | 1.x |
make | Run the dev/build targets | any |
The Docker build (see Dockerfile) reproduces this on rust:alpine + alpine:3.23. If you only care about Docker, you do not need any of the above on the host. Runtime image installs font-jetbrains-mono, ttf-dejavu, ttf-liberation, and fontconfig so the embedded Typst renderer can find body sans, mono, and fallback fonts.
Quickstart
cp samplefiles/env.sample .env
make
make (alias make run) installs frontend deps if needed, then runs Vite watch and cargo run concurrently on port 8000. Visit http://localhost:8000.
Configuration
All config comes from .env (loaded via dotenvy):
| Variable | Required | Purpose |
|---|---|---|
PORT | no (default 8000) | HTTP listen port |
BLOG_ROOT | no | Override the project root (where templates/, dist/, and content/ are read from) |
Make targets
| Target | What it does |
|---|---|
make run (default) | Vite watch + cargo run on port 8000 |
make build | Vite assets + release binary (target/release/blog) |
make start | Run the release binary (after make build) |
make bench | oha load test sweep across the main routes. Compares against a Flask server on port 8002 (the original blog.bythewood.me) if running |
make push | git push to every configured remote |
make clean | Remove target/, dist/, and frontend/node_modules/ |
There are no tests or linters configured.
Layout
blog.bythewood.me/
├── Cargo.toml, Cargo.lock # rust deps
├── Makefile, README.md, bench/ # top-level
├── src/ # rust source
│ ├── main.rs # tiny entry: server boot
│ ├── app.rs # AppState + Router assembly
│ ├── render.rs # render_html helper
│ ├── middleware.rs # request log
│ ├── routes/ # blog, post, search, seo, errors
│ ├── posts.rs # frontmatter + post loading
│ ├── markdown.rs # comrak custom renderer
│ ├── templates.rs # minijinja env, url_for, vite_asset, Jinja2-compat formatter
│ └── pdf.rs # embedded Typst renderer
├── templates/ # jinja2 source (minijinja-compatible) + blog_post.typ
├── content/ # markdown source
│ ├── posts/ # markdown posts with YAML frontmatter
│ └── images/ # served at /content/images/
├── frontend/ # JS pipeline (package.json, vite.config.js, static_src/)
├── dist/ # vite build output (gitignored, served at /static/)
├── target/ # cargo build output (gitignored)
└── samplefiles/ # Caddyfile.sample, env.sample, post-receive.sample
The binary reads templates/, dist/, and content/ from the current working directory. Override with BLOG_ROOT=<path>.
Deploy
Production runs on Docker. The standard flow is git push server master to a remote whose post-receive hook runs docker compose up --build --detach. Sample files in samplefiles/:
Caddyfile.sample: reverse proxy with TLSenv.sample: the.envshown abovepost-receive.sample: the git hook
See CLAUDE.md for the full architecture rundown, route table, and PDF pipeline details.
Caveats
- Posts loaded once at startup. Add a post, restart the process.