Narlblog
A single-author blog. Rust/Axum API backed by markdown files on disk; Astro/React frontend with a glass-effect Catppuccin theme. KaTeX math, GFM tables, server-side syntax highlighting, draft posts, RSS feed.
backend/ Rust + Axum API (filesystem-backed)
frontend/ Astro + React, SSRs pages and proxies /api/*
data/ Runtime data — posts, uploads, config.json (host volume)
Quick start
cp .env.example .env
# Generate a strong admin token:
echo "ADMIN_TOKEN=$(openssl rand -hex 32)" >> .env
docker compose up --build
# → http://localhost:4321
# → log in at /admin/login with the token from .env
Make sure the data/ host directory is owned by UID 1000 (the backend container's app user):
sudo chown -R 1000:1000 data/
Environment
| Variable | Required | Default | Notes |
|---|---|---|---|
ADMIN_TOKEN |
yes | — | Long random string. Stored as an HttpOnly cookie after login. |
PORT |
no | 3000 |
Backend port. |
DATA_DIR |
no | /app/data |
Where posts/uploads/config live. |
COOKIE_SECURE |
no | true |
Set false only for local HTTP development. |
FRONTEND_ORIGIN |
no | empty | Set to your frontend's URL only if you expose the backend directly. |
RUST_LOG |
no | info |
tracing-subscriber filter. |
PUBLIC_API_URL |
no | http://backend:3000 |
Backend URL the Astro proxy hits server-to-server. |
Local development
Backend:
cd backend
ADMIN_TOKEN=devtoken COOKIE_SECURE=false DATA_DIR=../data cargo run
Frontend (separate terminal, Node ≥ 22.12):
cd frontend
npm install
npm run dev
# → http://localhost:4321 (proxies to PUBLIC_API_URL, default http://backend:3000)
For a fully local stack, set PUBLIC_API_URL=http://localhost:3000 in frontend/.env.
Authoring posts
Posts are markdown files at data/posts/<slug>.md with YAML frontmatter:
---
date: 2026-05-09
summary: Optional summary; falls back to auto-extracted excerpt.
tags:
- rust
- astro
draft: false
---
# My post
Body in **markdown**, with $LaTeX$ via `$…$` / `$$…$$`, GFM tables, and fenced code blocks (```rust, ```ts, …).
Posts with draft: true are hidden from the public list and 404 when accessed by anyone without an admin session. Posts are sorted by date descending on the frontpage.
The web editor at /admin/editor writes the same format and updates atomically (write to .tmp, rename over target).
Uploads
Allowlisted extensions: jpg, jpeg, png, webp, gif, avif, pdf, txt, md, mp3, wav, ogg, mp4, webm, mov. Magic bytes are checked against the extension. SVG and HTML are intentionally rejected — /uploads/* is served as-is, so any active content there would be XSS.
Max upload size: 50 MB.
Backups
The deployed data/ directory is the entire blog. Back it up with whatever you trust — rsync, restic, borg, a sidecar container; nothing fancy is built in.
rsync -av data/ backup-host:/path/to/narlblog-data/
Stack
- Backend: Rust 2024 edition (requires 1.85+), axum 0.8, serde_yaml frontmatter, subtle constant-time auth, infer for upload sniffing
- Frontend: Astro 6, React 19, Tailwind 4, marked + marked-katex-extension + marked-highlight + DOMPurify
- Editor: CodeMirror 6 with vim mode and asset autocomplete