A static site generator designed for AI agents. Pure HTML/CSS output, zero runtime dependencies.
AI agents writing blog posts shouldn't manually copy-paste HTML templates. They should call one function and get consistent, themed output.
- Markdown to HTML - Write content in markdown, get styled HTML
- Theme enforcement - One source of truth for styling
- Auto-index - Home page and posts listing update automatically
- Plugin system - Hooks for custom behavior
- RSS feed - Auto-generated
- Static output - Deploy anywhere (Pi, shared hosting, S3, Cloudflare Pages)
# Create a new post
python3 cms.py --init "My First Post"
# Build everything
python3 cms.py
# Deploy to server
python3 cms.py --deploy
# Push to git
python3 cms.py --git
# All at once
python3 cms.py --deploy --git
# Output is in ./output/ - deploy it anywhereNote: cms.py is a wrapper that loads the auth key from ~/.openclaw/vault/secrets.json automatically. For direct use:
CMS_AUTH_KEY=your_key python3 build.py --init "Title"agent-cms/
├── config.json # Site configuration
├── build.py # Build script
├── content/posts/ # Markdown posts
│ └── 2026-04-13-slug.md
├── themes/default/ # Theme templates
│ ├── post.html
│ ├── index.html
│ └── style.css
├── plugins/ # Optional plugins
│ └── example.py
└── output/ # Generated static files
---
title: My Post Title
date: 2026-04-13
description: Optional meta description
---
# My Post Title
Content here...def pre_render(post_data):
"""Modify content before HTML conversion."""
return post_data
def post_render(html):
"""Modify final HTML output."""
return html
def on_publish(post_info):
"""Webhook/notification after publish."""
passFor programmatic use, the build script can be called directly:
from build import build_post, parse_post, load_config
config = load_config()
post = parse_post('content/posts/2026-04-13-my-post.md')
result = build_post(post, config)
# result: {'title': '...', 'url': '/posts/2026-04-13-my-post.html'}Agent CMS uses authentication to ensure only authorized agents can create/modify content:
# Generate or show the auth key
python3 build.py --show-key
# Output: CMS_AUTH_KEY=abc123...Store this key securely. The agent uses it via environment variable:
# For authenticated builds
CMS_AUTH_KEY=abc123... python3 build.py
# For creating posts
CMS_AUTH_KEY=abc123... python3 build.py --init "New Post"In config.json:
{
"auth": {
"key_file": ".cms_auth",
"require_auth": true
}
}Set require_auth to false for development/testing.
- Stored in
.cms_auth(not in git) - Mode 600 (owner read/write only)
- 32-byte cryptographically secure random token
Output is pure static files. Deploy to:
- Raspberry Pi with nginx
- Shared hosting (just upload)
- GitHub Pages
- Cloudflare Pages
- Netlify
- S3 + CloudFront
- Any web server
Configure in config.json:
{
"deploy": {
"method": "rsync",
"host": "yourserver.com",
"user": "youruser",
"path": "/var/www/blog/",
"key_file": "/path/to/ssh/key"
}
}Then deploy:
# Build and deploy in one command
CMS_AUTH_KEY=... python3 build.py --deployThe --deploy flag:
- Builds all posts
- Syncs output to remote server via rsync
- Deletes old files on server
- Runs
on_publishplugin hooks
Maintain the repo on GitHub:
# Build, commit, and push to git
CMS_AUTH_KEY=... python3 build.py --gitThe --git flag:
- Builds all posts
- Commits new/modified content with auto-generated message
- Pushes to remote origin
# Build, deploy, and push to git
CMS_AUTH_KEY=... python3 build.py --deploy --gitIf the CMS is installed on the same server as nginx:
# Build directly to deploy.path (e.g., /var/www/blog)
CMS_AUTH_KEY=... python3 build.py --localThe --local flag:
- Builds all posts
- Writes directly to
deploy.pathinstead of localoutput_dir - No rsync needed - files are already in place
Use --local when:
- CMS is on the same server as nginx
- You want to build directly in
/var/www/<site> - No remote deployment needed
MIT