A tiny dependency-free Zig blog engine.
- What It Is
- Design
- Features
- Requirements
- Contributing
- Build
- Production Docker Compose
- Production Bare Metal
- Configuration
- Agent Post API
- Hermes Skill
- Markdown
Evilblog is a small blog engine written in Zig 0.16.0
It is inspired by Lamer News, the blog/news engine by Salvatore Sanfilippo, @antirez.
The extra idea is agent-friendly writing: posts are plain Markdown, routes are explicit, and the project structure is documented so an agent can be delegated to draft or write a blog post without needing a JavaScript stack.
- No package-manager dependencies:
build.zig.zonhas.dependencies = .{}. - SQLite is vendored and compiled into the executable.
- Redis is optional and used only as a best-effort cache.
- HTML is rendered server-side.
- CSS and small browser scripts are embedded at build time.
- Post bodies are stored as restricted Markdown, not raw HTML.
- The app runs as a single native binary.
- ReleaseSmall binaries are under 5 MB each.
SQLite is not a runtime dependency because it is compiled in from vendor/sqlite.
- Public post list and single post pages.
- Admin-only post creation and editing.
- Drafts.
- Anonymous comments with nested replies.
- RSS feed.
- Upvotes for signed-in users.
- Optional Redis cache.
- Optional token-authenticated post API for agents.
- Donate page with optional README-backed
about mesection. - Built-in social metadata and default Open Graph image.
- Zig 0.16.0
SESSION_SECRETwith at least 32 bytes- Redis only if you want cache
Zig does not load .env files by itself. Export environment variables, prefix the run command, or use Docker Compose with .env.prod.
Run tests:
zig build testGenerate a local development session secret:
make session-secretStart the development server:
SESSION_SECRET=0123456789abcdef0123456789abcdef zig build runOpen:
http://127.0.0.1:8080
On first startup with an empty users table, Evilblog creates an admin user, prints a one-time password to the console, and forces a password change before admin routes can be used.
zig build test
zig build -Doptimize=ReleaseSmallThe binary is written to:
./zig-out/bin/evilblogFor a faster optimized build instead of the smallest one:
zig build -Doptimize=ReleaseFastCreate the production env file from the example and set real secrets:
cp .env.prod.example .env.prodGenerate SESSION_SECRET with make session-secret and put it in .env.prod.
Start Evilblog and Redis:
make upmake up tags the Docker image with the current Git version.
To run the same stack behind Traefik (HTTP → HTTPS with Let's Encrypt) instead of exposing Evilblog directly:
TRAEFIK_HOST=blog.example.com LETSENCRYPT_EMAIL=admin@example.com docker compose --env-file .env.prod -f docker-compose.traefik.yml up --builddocker-compose.traefik.yml starts Traefik with:
- HTTP on
TRAEFIK_HTTP_PORT(default80), redirecting to HTTPS. - HTTPS on
TRAEFIK_HTTPS_PORT(default443) with a Let's Encrypt certificate for theTRAEFIK_HOST. SetLETSENCRYPT_EMAILto receive expiry notices. - Evilblog's app port is internal to the Docker network; only Traefik is exposed.
The compose file overrides REDIS_HOST=redis so the app reaches Redis on the
Docker network. Keep REDIS_USERNAME and REDIS_PASSWORD in .env.prod; leave
both empty for an unauthenticated Redis.
Build the binary or download it from the release page:
zig build -Doptimize=ReleaseSmallRun the compiled binary from wherever you install it:
SESSION_SECRET=<VALUE> ./zig-out/bin/evilblogFor an installed copy, use its real path:
SESSION_SECRET=<VALUE> /opt/evilblog/evilblogBy default, SQLite is stored at evilblog.sqlite3 in the working directory. Set
SQLITE_PATH if you want the database somewhere else.
Redis is optional. To use it, set redis_host and redis_port in evilblog.zon,
or override them at runtime with REDIS_HOST and REDIS_PORT. If Redis requires
auth, set REDIS_USERNAME and REDIS_PASSWORD in the environment.
Most public settings live in evilblog.zon:
.{
.log_level = .info,
.site_title = "evilblog",
.site_logo_light = "/statics/evilblog-logo-light.png",
.site_logo_dark = "/statics/evilblog-logo.png",
.site_base_url = "https://example.com",
.redis_host = "127.0.0.1",
.redis_port = 6379,
.api_gateway_enabled = false,
.api_token = "",
.donate_paypal_url = "https://www.paypal.com/donate",
.donate_kofi_url = "https://ko-fi.com/example",
.donate_bitcoin_url = "bitcoin:bc1qexample",
.donate_about_readme_url = "https://raw.githubusercontent.com/user/user/refs/heads/main/README.md",
.donate_about_profile_image_url = "https://avatars.githubusercontent.com/u/19678157?v=4",
.footer_text = "evilblog",
}Useful environment variables:
BLOG_HOST, default127.0.0.1BLOG_PORT, default8080SQLITE_PATH, defaultevilblog.sqlite3REDIS_HOST, optional override forredis_hostinevilblog.zonREDIS_PORT, optional override forredis_portinevilblog.zonREDIS_USERNAME, optional Redis ACL usernameREDIS_PASSWORD, optional Redis passwordSESSION_SECRET, requiredAPI_TOKEN, optional override for the agent post API tokenSITE_BASE_URL, used for canonical URLs, RSS, and social metadata
Enable the API in evilblog.zon and set a token:
.api_gateway_enabled = true,
.api_token = "replace-with-a-long-random-token",Then agents can read, create, and update posts using:
Authorization: Bearer <api_token>Endpoints:
GET /api/posts
POST /api/posts
PATCH /api/posts/<id>Create request body:
{
"title": "Post title",
"body": "Post body in Markdown",
"excerpt": "Optional meta description",
"og_image": "/statics/og-default.png",
"tags": "zig,sqlite",
"status": "draft"
}PATCH /api/posts/<id> accepts any subset of title, slug, body, excerpt, og_image, tags, and status.
This repository includes a Hermes skill under skills/ for delegating blog post creation and updates to an agent.
To install it, give Hermes this repository and tell it:
Read the skill in this repository under /skills and install it into yourself.
After installing the skill, configure Hermes with EVILBLOG_API_KEY and EVILBLOG_API_URL in its environment. The key must match api_token in evilblog.zon or the API_TOKEN environment variable used by Evilblog.
Posts are written in a small safe Markdown subset:
- paragraphs and line breaks
#,##,###headings**bold**,_italic_, and inline code- links and bare URLs
- images by URL
- fenced code blocks
- simple ordered and unordered lists
Raw HTML is escaped.
MIT. See LICENSE.md.