WIP go headless CMS
  • Go 83.1%
  • HTML 9.3%
  • CSS 5.1%
  • Shell 1.8%
  • Just 0.5%
  • Other 0.2%
Find a file
2026-06-12 10:13:22 +02:00
.cms justfile: fix broken recipes pointing to nonexistent cmd/server/main.go 2026-06-12 10:13:22 +02:00
cmd Rename module from go-headless-cms to stet 2026-06-12 10:13:22 +02:00
content Initial commit: Go headless CMS 2025-11-29 10:40:59 +01:00
internal Rename module from go-headless-cms to stet 2026-06-12 10:13:22 +02:00
scripts Rename module from go-headless-cms to stet 2026-06-12 10:13:22 +02:00
static/css Add web UI with sidebar layout, htmx pages, and live partials 2026-06-12 10:13:22 +02:00
templates Add web UI with sidebar layout, htmx pages, and live partials 2026-06-12 10:13:22 +02:00
.air.toml Add hot reload support and fix template rendering 2026-02-02 18:47:47 +01:00
.cms.toml justfile: fix broken recipes pointing to nonexistent cmd/server/main.go 2026-06-12 10:13:22 +02:00
.gitignore Add assets endpoints and cross-source full-text search to REST API 2026-06-12 10:13:22 +02:00
.goreleaser.yml Add Goreleaser config and Dockerfile for multi-platform distribution 2026-06-12 10:13:22 +02:00
Dockerfile Rename module from go-headless-cms to stet 2026-06-12 10:13:22 +02:00
go.mod Rename module from go-headless-cms to stet 2026-06-12 10:13:22 +02:00
go.sum Add Cobra CLI with serve and init subcommands, replace cmd/server/main.go 2026-06-12 10:13:22 +02:00
justfile Rename module from go-headless-cms to stet 2026-06-12 10:13:22 +02:00
main.go Rename module from go-headless-cms to stet 2026-06-12 10:13:22 +02:00
README.md Rename module from go-headless-cms to stet 2026-06-12 10:13:22 +02:00
TODO.md Rename module from go-headless-cms to stet 2026-06-12 10:13:22 +02:00

Go Headless CMS

A file-based headless CMS for static site generators — Zola, Hugo, Eleventy, Jekyll, and Astro.

It wraps your existing content directories with a REST API, a browser UI, and a CLI so you can read, write, and search content without leaving your editor.

Features

  • Multi-SSG — auto-detects Zola, Hugo, Eleventy, Jekyll, or Astro. Fallback GenericAdapter handles any Markdown project.
  • REST API — full CRUD for content and assets across sources and collections. List, filter, sort, paginate. Create / update / delete with raw or structured frontmatter.
  • Git integrationstatus, log, diff, commit, pull, push per source (go-git, no system git required). Webhook endpoint for auto-pull on push.
  • CLIstet content|git|schema|config with --source, --output json, sorting, and pagination.
  • Web UI — sidebar layout, Dashboard, Content browser, Editor, Media library, Git panel, Schema viewer, Settings. All htmx-powered — no page reloads.
  • Full-text search — indexed across sources with relevance scoring, title/body/frontmatter snippets, tag filtering, and draft control.
  • Schema inference — scans frontmatter per collection, infers field types, detects required fields. Schema regenerates on pull or on demand.
  • Multi-source — manage multiple repos from one CMS instance.
  • Single binary — templates and static assets embedded with embed.FS. No runtime dependencies (bblot databases are self-contained).
  • Token auth — Bearer token (configurable) protects the API. Session-based auth for the web UI.

Quick Start

One-liner

curl -fsSL https://raw.githubusercontent.com/stet/stet/main/scripts/install.sh | sh

Docker

docker run -p 8080:8080 -v $(pwd):/content ghcr.io/stet/stet:latest

From source

git clone https://github.com/stet/stet
cd stet
just build                     # → bin/cms

Initialize a config

stet init

This writes a .cms.toml with sensible defaults (port 8080, auth off, current directory as the default source).

Start the server

stet serve
# or: stet serve --port 9090 --host 127.0.0.1

Open http://localhost:8080 — you'll see the dashboard.

CLI

All commands accept --source / -s (source ID) and --output json.

Content

# List entries in a collection
stet content list posts --sort date --dir asc -n 20

# Get full content (frontmatter + body)
stet content get posts/hello-world.md --output json

# Create a new entry
echo "## Hello" | stet content new posts/new-post.md

# Edit body (reads from stdin)
cat new-body.md | stet content edit posts/hello-world.md

# Publish / unpublish (toggles draft: false/true)
stet content publish posts/hello-world.md
stet content unpublish posts/hello-world.md

# Delete
stet content delete posts/old-post.md

Git

stet git status
stet git log -n 10
stet git commit -m "Update posts"
stet git pull
stet git push

Schema

# Show inferred schema for all collections
stet schema show

# Show one collection
stet schema show --collection posts

# Regenerate by rescanning
stet schema regen

Config

# Show current config
stet config show

# Set a value
stet config set --key server.port --value 9090
stet config set --key auth.enabled --value true
stet config set --key auth.token --value "my-secret-token"

REST API

Base URL: http://localhost:8080/api/v1

Sources

GET /api/v1/sources

Content

GET    /api/v1/sources/{sid}/collections/{coll}/content
GET    /api/v1/sources/{sid}/collections/{coll}/content/{path}
POST   /api/v1/sources/{sid}/collections/{coll}/content
PUT    /api/v1/sources/{sid}/collections/{coll}/content/{path}
PATCH  /api/v1/sources/{sid}/collections/{coll}/content/{path}
DELETE /api/v1/sources/{sid}/collections/{coll}/content/{path}

Query parameters for list:

Param Default Description
q Free-text filter on title
sort date title, date, mod_time, word_count, path
dir desc asc or desc
page 1 1-based page number
per_page 50 Items per page (max 200)
raw false When true, returns full body + raw frontmatter
drafts only (drafts only) or exclude (published only)

Create content

POST /api/v1/sources/default/collections/posts/content
{
  "path": "posts/new-post.md",
  "body": "## Hello, world!\n\nThis is my first post.",
  "frontmatter": {
    "title": "Hello World",
    "date": "2026-06-12",
    "draft": false,
    "tags": ["intro"]
  },
  "fm_type": "toml"
}

Assets

GET    /api/v1/sources/{sid}/assets
GET    /api/v1/sources/{sid}/assets/{path}
POST   /api/v1/sources/{sid}/assets          (multipart/form-data)
DELETE /api/v1/sources/{sid}/assets/{path}

Upload example:

curl -F "file=@photo.jpg" -F "path=uploads/photo.jpg" \
  http://localhost:8080/api/v1/sources/default/assets
GET /api/v1/search
Param Default Description
q Full-text query
source Limit to source ID
tag Filter by tag
field title, body, fm, or empty (all fields)
drafts only or exclude
limit 50 Max results (max 200)
offset 0 Pagination offset

Response includes relevance-scored results with match snippets and match_on indicating where the hit occurred.

Git

GET  /api/v1/sources/{sid}/git/status
GET  /api/v1/sources/{sid}/git/log?n=50&offset=0
GET  /api/v1/sources/{sid}/git/diff?from=<hash>&to=<hash>
POST /api/v1/sources/{sid}/git/commit   { "message": "...", "author_name": "...", "author_email": "..." }
POST /api/v1/sources/{sid}/git/pull     { "remote": "origin", "branch": "main" }
POST /api/v1/sources/{sid}/git/push     { "remote": "origin", "branch": "main" }
POST /api/v1/git/webhook/{sid}          (forge webhook receiver)

Schema

GET  /api/v1/sources/{sid}/collections/{coll}/schema
POST /api/v1/sources/{sid}/collections/{coll}/schema   (regenerate)

Auth

When auth.enabled = true in .cms.toml, all /api/v1/* routes require authentication. Provide a Bearer token:

curl -H "Authorization: Bearer <token>" http://localhost:8080/api/v1/sources

Configuration (.cms.toml)

[server]
port = 8080
host = "0.0.0.0"

[auth]
enabled = false
token = ""
admin_user = "admin"
admin_pass = "changeme"

[[sources]]
id = "default"
name = "My Blog"
path = "."
ssg = "auto"          # auto | zola | hugo | eleventy | jekyll | astro
fm_type = "toml"      # toml | yaml
branch = "main"
auth = "none"         # none | https | ssh
auto_pull = false
color = "#2563eb"
is_default = true

[schema]
regen_on_pull = true

[ui]
theme = "auto"        # auto | light | dark
editor = "codemirror" # codemirror | plain

Add more [[sources]] blocks for multi-site management.

Directory Structure

.
├── cmd/                    # Cobra CLI: root, serve, init, content, git, schema, config
│   ├── root.go
│   ├── serve.go
│   ├── init.go
│   └── cli.go
├── internal/
│   ├── auth/               # Session management
│   ├── config/             # .cms.toml loading, defaults, migration
│   ├── content/            # Markdown + frontmatter parser (TOML, YAML)
│   ├── git/                # go-git wrapper: status, log, commit, pull, push, diff
│   ├── index/              # bbolt-backed content index + fsnotify live updates
│   ├── schema/             # Schema inference engine + bbolt store
│   ├── server/
│   │   ├── handlers.go     # Page handlers (dashboard, editor, assets, git, etc.)
│   │   └── api/            # Chi REST API router + middleware
│   │       ├── router.go
│   │       ├── content.go
│   │       ├── git.go
│   │       ├── assets.go
│   │       ├── search.go
│   │       └── sources.go
│   ├── source/             # Source types
│   ├── ssg/                # SSG adapters: Zola, Hugo, Eleventy, Jekyll, Astro, Generic
│   └── ui/                 # UI utilities
├── templates/              # Go html/template files (embedded)
├── static/                 # CSS, JS (embedded)
├── scripts/
│   └── install.sh          # curl | sh installer
├── main.go                 # Entrypoint, embed directives
├── justfile                # Build automation
├── Dockerfile              # Multi-stage scratch image
├── .goreleaser.yml         # Multi-platform release config
└── .cms.toml               # Your config (generated by init)

Development

# Run with hot reload
just dev

# Build
just build

# Run tests
just test

# Format & lint
just fmt
just lint

License

MIT