- Go 63.5%
- Vue 13.4%
- CSS 10%
- HTML 6.2%
- JavaScript 4.6%
- Other 2.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| adapters | ||
| admin | ||
| cache | ||
| config | ||
| docs | ||
| domain | ||
| i18n | ||
| ports | ||
| render | ||
| static | ||
| templates | ||
| utils | ||
| .gitignore | ||
| admin_embed.go | ||
| admin_embed_test.go | ||
| config.yaml | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| main.go | ||
| README.md | ||
aoi-cms
A small, single-binary blog / personal-site CMS written in Go.
Markdown files in pages/ are the source of truth. The binary scans
them on startup (and on change), keeps a SQLite read-model for fast
listings and search, renders pages through Go's standard
html/template, and ships an embedded Vue 3 admin SPA for everything
you'd rather click than retype.
Fediverse (Mastodon / GoToSocial / Misskey) is a first-class citizen: articles can be announced to a configured account, replies sync back as native comments, and visitors can sign in with their own Fedi account to post.
Highlights
- File-first. Drop a
.mdintopages/and reload — that's the whole workflow. The scanner injects auuidanddatefor you on first sight, atomically. - Hexagonal Go core.
domain/is plain types,ports/are interfaces,adapters/plug in SQLite, the filesystem, the Fediverse client, Gin, and so on. Easy to swap or test. - Hot reload. Editing a template or a content file refreshes the page on the next request — no rebuild, no restart.
- Page cache with cheap invalidation. Rendered HTML is held in a 1000-entry FIFO keyed by slug (+ page number); any content change drops the whole cache.
- Vue 3 admin at
/admin. File tree, Markdown editor with keyboard shortcuts, drag-drop uploads, frontmatter panel, manual Fedi publish, comment moderation, live config edit. Bundle is//go:embed-ed — the binary is self-contained. - Fediverse. Bot account on a Mastodon-compatible instance
(Mastodon, GoToSocial, Pleroma) for per-article announce, comment
sync, star-count sync, optional cover upload. New articles can
auto-announce (
fediverse.auto_publish: true); previously-tracked articles always wait for an explicit admin republish, so the first connection to an establishedpages/tree doesn't flood the timeline. Commenter login is cross-protocol: Mastodon OAuth and Misskey MiAuth both work — Like and reply calls dispatch to the right native API and federate back to the bot's instance, so a Misskey user can comment on a GoToSocial-hosted bot post and it shows up natively for everyone. "Quick Reply" viaauthorize_interactionis the non-logged-in cross-instance escape hatch (Mastodon-side; Misskey users can just sign in). Comment threads are reconstructed by canonical post URL, so nested replies survive the round-trip back through ActivityPub regardless of which instance authored which reply. - Static-feel routing.
skip_in_url, custom slugs that propagate from a section's_index.md, numeric01.filename prefixes for sort order — and 301s for old URLs that pre-date askip_in_urlflip, so your backlinks don't break.
Quick start
Requirements: Go 1.25+, Node 20+ (only if you want to rebuild the admin SPA).
git clone <this repo>
cd aoi-cms
# 1. build the admin SPA (the built dist is embedded into the Go
# binary via //go:embed; skip this step if you don't change it)
cd admin && npm install && npm run build && cd ..
# 2. build the server
go build -o aoi-cms .
# 3. drop some content
mkdir -p pages
cat > pages/hello.md <<'EOF'
---
title: Hello, world
type: post
---
This is my first post.
EOF
# 4. configure and run
cp config.yaml my-config.yaml # edit url/sitename/etc.
./aoi-cms -c my-config.yaml
Open http://localhost:8080 for the site and
http://localhost:8080/admin for the editor.
Flags
| Flag | Default | Purpose |
|---|---|---|
-r <dir> |
. |
Project root. The defaults for the next three are derived from it. |
-c <file> |
<root>/config.yaml |
Config file. |
-p <dir> |
<root>/pages |
Content directory. |
-d <file> |
<root>/database.db |
SQLite database path. |
Configuration
A complete annotated example lives at config.yaml.
The shape is also documented under the runtime keys in
docs/api.md. A few callouts:
codeis a site-wide fallback password forlocked: truepages.static_urlsets the origin shared by every language subdomain for/static/...assets. Without it, a subdomain-per-language deployment (en.site,ja.site) re-downloads CSS/fonts/JS on each language switch — every subdomain is a separate browser-origin with its own HTTP cache. Empty keeps relative/staticpaths (single-origin sites).ip_saltshould be a long random string — rotate it to invalidate every existing star.admin.passwordaccepts either a plain string (handy for local development) or a bcrypt hash ($2a$...) for production.fediverse.owner_acctis the only way to grant admin access via Fediverse OAuth — without it, only the standalone login works.- Leave
fediverse.access_tokenempty on first run: the binary will register an OAuth app and print the authorization URL plus the exactcurlcommand for the token exchange.
Project layout
adapters/
fedi/ # Mastodon / Misskey / generic ActivityPub client
file/ # scanner, watcher, asset uploads
rss/ # RSS / Atom fetcher for type:micro pages
sqlite/ # SQLite read-model (pages, comments, stars, sources)
web/ # Gin router and HTTP handlers (handlers/, htmx/)
admin/ # Vue 3 + Vite admin SPA. Built output is embedded.
cache/ # in-memory FIFO HTML cache
config/ # YAML loader + live `SiteConfig` snapshotting
docs/ # this folder — frontmatter / pages / templates / api
domain/ # plain Article, Comment, MicroPost types
pages/ # your Markdown content (you create this)
ports/ # interfaces the adapters implement
render/ # template engine, paginator, query parser
static/ # CSS, JS, fonts, uploads — served at /static
templates/ # html/template files: bases/, partials/, pages/, sections/
See:
docs/config.md— full config.yaml reference (site, admin, nav, fediverse, i18n).docs/pages.md— directory layout, type inference, URL derivation, the scan loop, multilingual content.docs/frontmatter.md— every frontmatter key, defaults, and inheritance rules.docs/templates.md— TemplateData fields, helper functions, minimal page / section examples, i18n / LocalizedSite.docs/api.md— public, admin, and HTMX endpoints.
Working on the admin SPA
cd admin
npm install
npm run dev # Vite dev server, proxies API calls to :8080
npm run build # writes admin/dist/, which the Go binary embeds
The Go server picks up admin/dist/ at build time via
//go:embed (see admin_embed.go), so you must npm run build
before go build for SPA changes to ship.
License
See LICENSE if present.