koguma
v2.3.10
Published
π» A little CMS with big heart β schema-driven, runs on Cloudflare's free tier
Downloads
3,747
Maintainers
Readme
Koguma gives you a headless CMS with a beautiful admin dashboard, all powered by Cloudflare Workers + D1 + R2. Define your content types in code, and Koguma handles the rest β API routes, admin UI, media storage, and a file-based content/ directory for version-controlled content.
Quickstart
1. Install
bun add koguma
# or
npm install koguma2. Define your content
Create a site.config.ts in your project root:
import { defineConfig, contentType, field } from 'koguma';
export default defineConfig({
siteName: 'My Site',
contentTypes: [
contentType({
id: 'post',
name: 'Blog Post',
displayField: 'title',
fields: {
title: field.text('Title').required(),
slug: field.text('Slug').required(),
body: field.markdown('Body'),
heroImage: field.image('Hero Image'),
published: field.boolean('Published').default(false),
date: field.date('Published Date')
}
})
]
});3. Create your worker
Create a worker.ts:
import { createWorker } from 'koguma/worker';
import config from './site.config';
export default createWorker(config);4. Configure
Create koguma.toml:
name = "my-site"
database_id = "" # filled by `koguma init`That's it β two lines. Everything else is derived by convention:
- D1 database β
my-site-db - R2 bucket β
my-site-media - Worker name β
my-site
5. Deploy
koguma init # Scaffold project, create D1 + R2 on Cloudflare
koguma push # Build + deploy + sync contentYour admin dashboard is at /admin. Your API is at /api/content/:type.
Content Schema
Field Types
| Field | Usage | Stored As |
| -------------------------------- | ---------------------- | ------------------------ |
| field.text(label) | Titles, slugs, URLs | TEXT |
| field.longText(label) | Descriptions, bios | TEXT |
| field.markdown(label) | Rich formatted content | TEXT (markdown string) |
| field.number(label) | Counts, order | number in JSON |
| field.boolean(label) | Toggles | boolean in JSON |
| field.date(label) | Timestamps | string (ISO 8601) |
| field.select(label, {options}) | Dropdowns | string |
| field.url(label) | URLs | string |
| field.email(label) | Email addresses | string |
| field.phone(label) | Phone numbers | string |
| field.color(label) | Hex colours | string |
| field.youtube(label) | YouTube video IDs | string |
| field.instagram(label) | Instagram handles | string |
| field.image(label) | Image from R2 media | string (asset ID) |
| field.images(label) | Array of images | string[] (asset IDs) |
| field.ref(typeId, label) | Link to another entry | string (entry ID) |
| field.refs(typeId, label) | Array of entry links | string[] (entry IDs) |
All fields stored inside a JSON data blob in the entries table β no per-field columns, no migrations.
Content Type Options
contentType({
id: "page",
name: "Page",
displayField: "title",
singleton: true, // Only one entry allowed (e.g. site settings)
fields: { ... },
});CLI Reference
All commands auto-detect your project root by looking for koguma.toml.
| Command | Description |
| ------------------ | ----------------------------------------------------------------- |
| koguma init | Interactive project setup β scaffold, create D1 + R2, set secret |
| koguma dev | Auto-sync content/ β local D1, generate types, start dev server |
| koguma push | Build + deploy + sync content to remote |
| koguma pull | Download remote content + media β local content/ files |
| koguma gen-types | Generate koguma.d.ts typed interfaces |
| koguma tidy | Validate content/ against config, sync dirs, check fields |
Content Directory
Koguma uses a content/ directory for file-based content that lives in your git repo:
content/
βββ post/ # collection β one file per entry
β βββ hello-world.md # slug = "hello-world"
β βββ our-mission.md
β βββ our-mission.heroBody.md # sibling markdown field
βββ landingPage/ # singleton β always index.md
β βββ index.md # slug = content type ID
β βββ heroBody.md # sibling markdown field
β βββ ctaBody.md # sibling markdown field
βββ media/ # local images (synced to R2)
βββ hero-banner.jpgFile Format
Every content file uses YAML frontmatter + markdown body:
---
title: Our Mission
slug: our-mission
heroImage: media-hero-banner
published: true
date: 2026-03-01
---
This body content maps to the **first** `field.markdown()` in the content type.Singletons vs Collections
- Collections (default): Each entry is a file named
{slug}.md - Singletons (
singleton: true): One entry, alwaysindex.md. The slug is auto-set to the content type ID.
Sibling Markdown Fields
When a content type has multiple field.markdown() fields, the first one maps to the file body. Additional markdown fields are stored as sibling files (pure markdown, no frontmatter):
| | Collections | Singletons |
| ------------------- | --------------------- | -------------- |
| Main file | {slug}.md | index.md |
| Sibling pattern | {slug}.{fieldId}.md | {fieldId}.md |
Example singleton with 3 markdown fields:
content/landingPage/
βββ index.md # frontmatter + 1st markdown field body
βββ heroBody.md # 2nd markdown field (pure markdown)
βββ ctaBody.md # 3rd markdown field (pure markdown)Git is your version history. No custom versioning table needed.
API Routes
Public (no auth)
| Method | Route | Description |
| ------ | ------------------------ | --------------------------------------- |
| GET | /api/content/:type | List all entries of a content type |
| GET | /api/content/:type/:id | Get a single entry (with resolved refs) |
| GET | /api/media/:key | Serve a media file from R2 |
Admin (auth required)
| Method | Route | Description |
| -------- | ---------------------- | ----------------------------------- |
| POST | /api/auth/login | Login with { password } |
| POST | /api/auth/logout | Clear session |
| GET | /api/auth/me | Check authentication |
| GET | /api/admin/schema | Content type schemas (for admin UI) |
| GET | /api/admin/:type | List entries |
| GET | /api/admin/:type/:id | Get entry |
| POST | /api/admin/:type | Create entry |
| PUT | /api/admin/:type/:id | Update entry |
| DELETE | /api/admin/:type/:id | Delete entry |
| GET | /api/admin/media | List media assets |
| POST | /api/admin/media | Upload media (multipart form) |
| DELETE | /api/admin/media/:id | Delete media |
References and images are automatically resolved to nested objects in the public API (up to 2 levels deep).
Package Exports
import { defineConfig, contentType, group, field, type Infer } from 'koguma';
import { createWorker } from 'koguma/worker';
import { createClient } from 'koguma/client';
import { Markdown, useEntry, useEntries } from 'koguma/react';
import type { KogumaAsset, EntryReference } from 'koguma/types';Local Development
# Create .dev.vars with your local admin password
echo "KOGUMA_SECRET=your-password" > .dev.vars
# Start dev server (auto-syncs content/, generates types)
koguma dev
# Admin dashboard is at http://localhost:8787/admin
# API is at http://localhost:8787/api/content/:typeArchitecture
Your Project
βββ site.config.ts β Content type definitions
βββ worker.ts β Entry point (imports koguma/worker)
βββ koguma.toml β Project config (name + database_id)
βββ .dev.vars β Local secrets (KOGUMA_SECRET)
βββ content/ β Version-controlled content files
βββ post/
β βββ hello-world.md
βββ siteSettings/
βββ index.md
Koguma (this package)
βββ src/
β βββ config/ β Schema definitions (defineConfig, field types)
β βββ api/ β Hono router (CRUD + media + auth)
β βββ db/ β D1 queries (JSON document store)
β βββ auth/ β HMAC-signed cookie sessions
β βββ media/ β R2 upload/serve/delete
β βββ admin/ β Dashboard HTML shell + JS/CSS bundle
β βββ client/ β Fetch client for consuming the API
β βββ react/ β React hooks + <Markdown> component
βββ admin/ β Vite + React admin dashboard source
βββ cli/ β CLI commands (init, dev, push, pull, gen-types, tidy)License
MIT
