A zero-dependency C++20 blog engine. One binary, one content directory, one command.
Loom reads markdown, renders HTML, and serves it over HTTP — no frameworks, no build tools, no JavaScript bundlers. The HTTP server, markdown parser, router, and template engine are all written from scratch.
make && ./loom content/
That's it. Your blog is at localhost:8080.
The content/ directory in this repo is a working example site — it's what you see at loomblog.com. Use it to try out themes, explore the config, or copy it as a starting point.
curl -L -o loom https://github.com/1ay1/loom/releases/latest/download/loom
chmod +x loom
./loom --git https://github.com/1ay1/loom.git main contentOpen http://localhost:8080. That's it.
Every static site generator needs a build step, a deploy pipeline, and a runtime. Loom doesn't. It's a single binary that reads markdown and serves HTML. Hot reload is built in. Push a commit, the site updates.
- Epoll-based HTTP server — non-blocking I/O, TCP_NODELAY, keep-alive, trie-based router
- Hand-written markdown parser — headings, bold/italic, code blocks, tables, footnotes, task lists, strikethrough, images, reference links, raw HTML passthrough
- Smart typography — curly quotes, em/en dashes, ellipsis — applied automatically during rendering
- Pre-rendered cache — the entire site lives in memory, atomically swapped on content changes
- Gzip + ETag — compressed responses with HTTP 304 support out of the box
- HTML minification — automatic, zero config
- Hot reload — inotify-based filesystem watching with 500ms debounce, or git polling for commit-driven updates
- 6 built-in themes — default, terminal, nord, gruvbox, rose, hacker — all with light/dark mode
- View Transitions API — smooth crossfade between pages (browser-native, progressive enhancement)
- Cmd+K command palette — instant fuzzy search and navigation from any page
- Keyboard navigation —
j/kto browse posts,/to search,Escto close - Sidenotes — Tufte-style margin notes on wide screens, toggleable inline on mobile
- Code blocks — copy button, optional filename tabs (
title="main.rs"), language hints - Image zoom — click any image to expand fullscreen,
Escto close - Active TOC — table of contents highlights the current section as you scroll
- Reading position memory — remembers where you left off, offers to resume on revisit
- Post staleness notice — subtle banner on posts older than 18 months
- Post connections graph — SVG visualization of tag-based relationships on the archives page
- RSS, sitemap, robots.txt — generated automatically
- Tags, series, archives — first-class content organization with dedicated pages
- Sidebar widgets — recent posts, tag cloud, about text
- Open Graph / SEO metadata — og:tags, canonical URLs, structured data
- Git source — serve content directly from a git branch, no checkout needed
- Strong types —
Slug,Tag,Title,PostId,Content— no stringly-typed domain logic - C++20 concepts —
ContentSource,WatchPolicy,Reloadable— the type system enforces contracts
# build
make
# try the included example site immediately
./loom content/Open http://localhost:8080. You're looking at a live blog — edit any file in content/ and the site rebuilds instantly.
To start your own site, copy the example or create from scratch:
# option 1: copy the example as a base
cp -r content/ myblog/
# option 2: start from scratch
mkdir -p myblog/posts myblog/pages
# then run
./loom myblog/# serve from a git repo instead of the filesystem
./loom --git /path/to/repo main contentcontent/
├── site.conf # site config
├── posts/
│ ├── hello-world.md
│ └── my-second-post.md
├── pages/
│ └── about.md
├── images/ # static assets, served as-is
│ └── cover.png
└── theme/
└── style.css # optional, overrides everything
Any file that isn't markdown or site.conf is served as a static asset. Put images, fonts, or downloads anywhere in content/ and reference them by path (e.g. ).
---
title: Why epoll beats select
date: 2024-03-10
slug: epoll-vs-select
tags: linux, networking, performance
draft: false
excerpt: Custom excerpt for social cards and listings
image: /images/epoll-cover.png
---
Your markdown here.Straight quotes become curly, dashes become proper typographic characters, and triple dots become ellipsis — all automatically:
| You write | Rendered as |
|---|---|
"hello" |
"hello" |
it's |
it's |
-- |
en dash |
--- |
em dash |
... |
ellipsis |
Add a filename tab to any fenced code block:
```rust title="main.rs"
fn main() {
println!("Hello");
}
```A copy button appears on hover for all code blocks.
Footnotes render as Tufte-style margin notes on wide screens and toggle inline on mobile:
Something noteworthy[^1] in the text.
[^1]: This appears in the margin on desktop.Series are defined by directory structure — create a subfolder inside posts/:
posts/
├── hello-world.md # standalone post
├── systems-programming/ # ← series name
│ ├── epoll-vs-select.md
│ └── tcp-nodelay.md
└── type-safety/ # ← another series
├── strong-types.md
└── phantom-types.md
Posts within a series are ordered by publish date (oldest first). No frontmatter needed.
---
title: About
slug: about
---
Page content.site.conf uses key = value format:
title = My Blog
description = Thoughts on software engineering
author = Jane Doe
base_url = https://example.com
# Navigation bar
nav = Home:/, Archives:/archives, About:/about
# Theme (5 built-in — see Themes section below)
theme = nord
# Override individual theme variables
theme_font_size = 16px
theme_max_width = 800px
# Sidebar
sidebar_widgets = recent_posts, tag_cloud, about
sidebar_recent_count = 5
sidebar_about = Thoughts on software engineering, systems, and type theory.
sidebar_position = right
# Layout
header_style = default
post_list_style = cards
show_description = true
show_theme_toggle = true
show_post_dates = true
show_post_tags = true
show_excerpts = true
show_reading_time = true
date_format = %Y-%m-%d
# Footer
footer_copyright = © 2024 Jane Doe
footer_links = GitHub:https://github.com/jane, RSS:/feed.xml
# Inject custom CSS or HTML
custom_css = body { letter-spacing: 0.02em; }
custom_head_html = <link rel="icon" href="/favicon.ico">
| Key | Action |
|---|---|
Ctrl+K / Cmd+K |
Open command palette (fuzzy search + navigate) |
j / k |
Move between posts in listings |
Enter |
Open focused post |
/ |
Focus search input |
Esc |
Close palette / image zoom |
| Path | Description |
|---|---|
/ |
Post index |
/post/:slug |
Single post |
/tag/:slug |
Posts by tag |
/tags |
All tags |
/archives |
Posts by year |
/series |
All series |
/series/:name |
Posts in series |
/:slug |
Static page |
/feed.xml |
RSS 2.0 |
/sitemap.xml |
XML sitemap |
/robots.txt |
Robots |
All 6 themes ship with light and dark variants. The toggle is automatic (respects prefers-color-scheme, with manual override persisted to localStorage).
| Theme | Font | Vibe |
|---|---|---|
default |
System sans-serif | Clean, neutral grays, blue accent |
terminal |
Monospace | Dark hacker aesthetic, green accent, sharp corners |
nord |
Inter / sans-serif | Arctic color palette, muted frost blues |
gruvbox |
System sans-serif | Retro groove, warm earthy contrast, orange accent |
rose |
System sans-serif | Soft pinks, elegant, magenta accent, pill-shaped elements |
hacker |
Courier New | CRT phosphor green-on-black, scanlines, blinking cursor, prompt-style UI |
Themes go beyond colors — they define structural choices like corner rounding, tag styles, link decoration, and more. The theme DSL makes it easy to create new themes purely in C++ with type-safe CSS helpers.
Override any theme with content/theme/style.css — it replaces the built-in CSS entirely.
Serve content from any git branch without touching the working tree:
# local repo, main branch, content in "content/" subdirectory
./loom --git . main content
# bare repo, custom branch
./loom --git /srv/blog.git production
# public GitHub remote — clones bare automatically
./loom --git https://github.com/you/blog.git main contentThe git source uses git show and git ls-tree to read blobs directly — no checkout, no temp files. Hot reload polls for new commits and rebuilds automatically.
Post dates fall back to the first commit that introduced the file (git log --diff-filter=A). Modified timestamps use the last commit date. Both survive clones and CI rebuilds — unlike filesystem mtime.
When using a public GitHub remote, Loom redirects static asset requests to raw.githubusercontent.com instead of piping the bytes through your server:
GET /images/cover.png
→ 302 Location: https://raw.githubusercontent.com/you/blog/refs/heads/main/content/images/cover.png
Write image paths the same way in all modes — /images/cover.png works on filesystem, local git, and remote git. The redirect is automatic.
Request → Epoll → Router → AtomicCache snapshot → Gzip → Response
↑
HotReloader → build_cache() → atomic swap
↑
InotifyWatcher | GitWatcher
The site is pre-rendered into an immutable SiteCache struct on startup. Every request grabs a shared_ptr snapshot — zero contention on the read path. When content changes, a new cache is built and swapped atomically. In-flight requests continue serving from the old snapshot.
make # g++, C++20, -O2
make cleanRequirements: Linux, g++ with C++20 support, zlib (-lz).
No cmake. No conan. No vcpkg. No submodules.
src/
├── main.cpp # entry point, routing, cache orchestration
├── http/ # epoll server, trie router, request/response
├── content/ # filesystem + git content sources
├── render/ # HTML rendering, themes, sidebar, layout
├── util/ # markdown parser, config parser, gzip, minify, git
└── reload/ # inotify watcher, git watcher, hot reloader
include/loom/
├── core/ # strong types (Slug, Tag, Title, Content, PostId)
├── domain/ # Site, Post, Page, Navigation, Theme, Footer
├── engine/ # BlogEngine, site_builder
└── ... # mirrors src/ structure
MIT
