riley_cms
A minimal, self-hosted headless CMS for personal blogs. Rust, no database, no GUI.
Git is your content database. S3/R2 is your asset database. riley_cms is the stateless glue that serves it.
Philosophy
- Git is the database for content - version controlled, portable, yours forever
- S3/R2 is the database for assets - cheap, fast, globally distributed
- The API is stateless glue - easy to deploy, easy to scale
- Opinionated by design - less config, less bikeshedding
- Headless - you control the frontend, riley_cms serves JSON
Quick Start
Install
Or build from source:
Initialize Content
This creates an example content structure:
my-blog/
├── riley_cms.toml
└── content/
├── hello-world/
│ ├── config.toml
│ └── content.mdx
└── my-series/
├── series.toml
├── part-one/
│ ├── config.toml
│ └── content.mdx
└── part-two/
├── config.toml
└── content.mdx
Configure
Edit riley_cms.toml:
[]
= "."
= "content"
[]
= "s3"
= "my-assets"
= "auto"
= "https://xxx.r2.cloudflarestorage.com"
= "https://assets.example.com"
[]
= "0.0.0.0"
= 8080
= ["https://mysite.com"]
Run
Content Structure
Posts live in directories with config.toml + content.mdx:
content/
├── my-post/
│ ├── config.toml
│ └── content.mdx
└── my-series/
├── series.toml # Makes this a series
├── getting-started/
│ ├── config.toml
│ └── content.mdx
└── advanced-topics/
├── config.toml
└── content.mdx
Post Config
= "My Post Title"
= "Optional subtitle"
= "A short preview for listings..."
= "https://assets.example.com/preview.jpg"
= ["rust", "programming"]
= 2025-01-15T00:00:00Z # None = draft, future = scheduled
= 1 # For series posts
Series Config
= "My Series"
= "Learn something cool"
= "https://assets.example.com/series.jpg"
= 2025-01-15T00:00:00Z
API
| Endpoint | Description |
|---|---|
GET /posts |
List all live posts |
GET /posts/:slug |
Get a single post with content |
GET /posts/:slug/raw |
Get raw MDX content only |
GET /series |
List all live series |
GET /series/:slug |
Get series with ordered posts |
GET /assets |
List assets in bucket |
GET /health |
Health check |
* /git/{*path} |
Git Smart HTTP (requires Basic Auth) |
Query Parameters
?include_drafts=true- Include unpublished posts (requires auth)?include_scheduled=true- Include future-dated posts (requires auth)?limit=N- Limit results (default: 50)?offset=N- Skip results for pagination
Authentication
riley_cms supports two authentication mechanisms:
API Token (Bearer)
For accessing drafts and scheduled content via the API:
Configure in riley_cms.toml:
[]
= "env:API_TOKEN" # Read from environment variable
# or
= "your-literal-token"
Git Token (Basic Auth)
For pushing content via Git over HTTP:
Configure in riley_cms.toml:
[]
= "env:GIT_AUTH_TOKEN"
Git Server
riley_cms can serve your content repository over HTTP, allowing you to push content updates directly to the server.
Setup
- Initialize a bare git repo in your content directory (or use an existing one)
- Configure the
git_tokenin your config - Add the remote to your local clone:
# On your local machine
Endpoints
| Endpoint | Description |
|---|---|
GET /git/{*path} |
Git read operations (fetch/clone) |
POST /git/{*path} |
Git write operations (push) |
After a successful push, riley_cms automatically:
- Refreshes the content cache
- Fires any configured webhooks
Response Example
CLI
Crates
| Crate | Description |
|---|---|
riley-cms-core |
Core library - embed in your own apps |
riley-cms-api |
Axum HTTP server |
riley-cms-cli |
CLI binary |
Using riley-cms-core as a Library
use ;
async
Deployment
Docker
FROM rust:1.88 AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y git ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/riley_cms /usr/local/bin/
VOLUME /data
EXPOSE 8080
CMD ["riley_cms", "serve"]
Docker Compose
services:
riley_cms:
build: .
volumes:
- riley_cms_data:/data
- ./riley_cms.toml:/etc/riley_cms/config.toml:ro
environment:
- GIT_AUTH_TOKEN=${GIT_AUTH_TOKEN}
- API_TOKEN=${API_TOKEN}
- AWS_ACCESS_KEY_ID=${R2_ACCESS_KEY_ID}
- AWS_SECRET_ACCESS_KEY=${R2_SECRET_ACCESS_KEY}
ports:
- "8080:8080"
restart: unless-stopped
volumes:
riley_cms_data:
Configuration
Config is loaded from (first match wins):
--config <path>flagRILEY_CMS_CONFIGenv varriley_cms.tomlin current directory- Walk up ancestors for
riley_cms.toml ~/.config/riley_cms/config.toml/etc/riley_cms/config.toml
See riley_cms.example.toml for all options.
License
MIT