domma-cms
v0.3.0
Published
File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.
Maintainers
Readme
Domma CMS
A flat-file CMS with no database. Pages are Markdown files, config is JSON, and the admin panel is a full SPA. Powered by Fastify on the backend and Domma on the frontend.
Table of Contents
- Quick Start
- Requirements
- Project Structure
- Configuration
- Content
- Admin Panel
- Plugins
- Scripts Reference
- Contributing
- Licence
Quick Start
npx domma-cms my-site
cd my-site
npm run devOpen http://localhost:3050/admin and log in with the credentials you created during setup.
The setup wizard runs automatically after
npm install. To run it again manually:npm run setup.
Requirements
- Node.js 18 or higher
- npm 7 or higher
- pm2 (for production) —
npm install -g pm2
Project Structure
After scaffolding, your project looks like this:
my-site/
├── admin/ # Admin SPA (Domma frontend)
│ ├── index.html
│ └── js/
│ ├── app.js # Router, auth, sidebar init
│ ├── api.js # Authenticated API client (auto token refresh)
│ ├── views/ # Admin view modules
│ └── templates/ # Companion HTML templates
│
├── config/ # All site configuration (JSON)
│ ├── site.json # Title, tagline, theme, SEO, footer
│ ├── navigation.json # Navbar brand, items, variant, position
│ ├── plugins.json # Plugin enabled/disabled state + settings
│ ├── auth.json # JWT expiry, roles, bcrypt rounds
│ ├── content.json # Content directory paths and page defaults
│ └── server.json # Port, host, CORS, upload limits
│
├── content/ # All user content (created on first run)
│ ├── pages/ # Markdown pages (maps to public URLs)
│ ├── media/ # Uploaded files
│ └── users/ # User accounts ({uuid}.json)
│
├── plugins/ # CMS plugins
│ ├── domma-effects/
│ ├── example-analytics/
│ └── form-builder/
│
├── public/ # Public frontend assets (CSS, JS)
├── scripts/ # CLI utilities (setup, reset, seed, etc.)
├── server/ # Fastify backend
│ ├── server.js # Entry point
│ ├── config.js # Config loader (getConfig / saveConfig)
│ ├── middleware/ # Auth (JWT, roles)
│ ├── routes/ # API + public SSR routes
│ ├── services/ # Content, users, plugins, markdown, renderer
│ └── templates/ # Public HTML shell (page.html)
│
├── .env # Secrets — JWT_SECRET, NODE_ENV
├── .env.example # Template for .env
└── package.jsonConfiguration
All configuration lives in config/ as JSON files. They are editable directly or via the admin panel.
config/site.json
Controls the public-facing site identity.
{
"title": "My Site",
"tagline": "Powered by Domma CMS",
"theme": "charcoal-dark",
"seo": {
"defaultTitle": "My Site",
"titleSeparator": " | ",
"defaultDescription": "A site built with Domma CMS"
},
"footer": {
"copyright": "© 2026 My Site. All rights reserved.",
"links": [
{ "text": "Privacy Policy", "url": "/privacy" },
{ "text": "Contact", "url": "/contact" }
]
}
}Available themes: charcoal-dark, charcoal-light, ocean-dark, ocean-light, forest-dark, forest-light,
sunset-dark, sunset-light, royal-dark, royal-light, lemon-dark, lemon-light, silver-dark, silver-light,
grayve, christmas-dark, christmas-light
config/navigation.json
Controls the public navbar.
{
"brand": { "text": "My Site", "logo": null, "url": "/" },
"items": [
{ "text": "Home", "url": "/", "icon": "home" },
{ "text": "About", "url": "/about", "icon": "info" },
{ "text": "Contact", "url": "/contact", "icon": "mail" }
],
"variant": "dark",
"position": "sticky"
}Dropdown menus are supported via nested items arrays on any item.
config/plugins.json
Enables or disables plugins and stores their settings.
{
"form-builder": {
"enabled": true,
"settings": { "smtp": { "host": "localhost" } }
}
}.env
Secrets only — never commit this file.
JWT_SECRET=your-64-char-random-string
NODE_ENV=developmentGenerate a strong secret with: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
Content
Pages are Markdown files in content/pages/. The filename determines the public URL.
| File path | Public URL |
|-------------------------------------|---------------------|
| content/pages/index.md | / |
| content/pages/about.md | /about |
| content/pages/blog/hello-world.md | /blog/hello-world |
| content/pages/services/index.md | /services |
Frontmatter
Every page starts with a YAML frontmatter block:
---
title: My Page
slug: my-page
description: A short description for SEO
layout: default
status: published
sortOrder: 1
showInNav: false
sidebar: false
seo:
title: My Page | My Site
description: A short description for SEO
createdAt: '2026-01-01T00:00:00.000Z'
updatedAt: '2026-01-01T00:00:00.000Z'
---
# My Page
Page content goes here in Markdown.| Field | Description |
|-------------------|---------------------------------------------------------------------------|
| title | Page title (used in <title> tag and admin listing) |
| slug | URL slug (usually matches the filename) |
| description | Short description for SEO |
| layout | Layout preset to use (default or any preset from config/presets.json) |
| status | published or draft — draft pages return 404 on the public site |
| sortOrder | Integer used to order pages in admin listings |
| showInNav | Whether to include this page in auto-generated nav menus |
| sidebar | Whether to show a sidebar on this page |
| seo.title | Overrides the <title> tag |
| seo.description | Overrides the meta description |
Plugin Shortcodes
Some plugins inject content via HTML attributes:
<!-- Form Builder plugin — embed any form by slug -->
<div data-form="contact"></div>
<div data-form="feedback"></div>Forms are created in the admin panel under Plugins → Forms.
Admin Panel
Access the admin panel at http://localhost:3050/admin.
First Login
Run npm run setup (or make setup) to create your admin account, set a site title, and choose a theme. The wizard is
safe to re-run — it skips any steps already completed.
Roles
| Role | Level | Can do |
|--------------|-------|-------------------------------------------------------------|
| admin | 0 | Everything, including user management and plugin config |
| manager | 1 | Manage pages, media, navigation, layouts, and site settings |
| editor | 2 | Create and edit pages and media |
| subscriber | 3 | Read-only access |
Permissions per resource are defined in config/auth.json.
Navigation
The sidebar groups content by role:
- Overview — Dashboard
- Structure — Navigation, Layouts (admin and manager only)
- Content — Pages, Media
- Configuration — Users, Site Settings (admin and manager only)
- Plugins — Plugin manager and plugin settings (admin only)
Plugins
Plugins extend Domma CMS with backend routes, public page injections, and admin panel views.
Bundled Plugins
| Plugin | Description | |--------------------|---------------------------------------------------------------------------------------------------| | Form Builder | Visual form builder — create arbitrary forms, store submissions, trigger email and webhook actions | | Analytics | Basic page view tracking stored as a flat JSON file | | Back to Top | Configurable scroll-to-top button injected into every public page | | Cookie Consent | GDPR cookie consent banner with per-category toggles |
Enable or disable any plugin in the admin panel under Plugins, or directly in config/plugins.json.
Building a Plugin
A plugin is a directory under plugins/ containing three mandatory files:
plugins/my-plugin/
├── plugin.json # Manifest
├── plugin.js # Fastify plugin (backend)
└── config.js # Default settingsplugin.json
{
"name": "my-plugin",
"displayName": "My Plugin",
"version": "1.0.0",
"description": "What this plugin does.",
"author": "Your Name",
"date": "2026-01-01",
"icon": "package",
"admin": {
"sidebar": [
{
"id": "my-plugin",
"text": "My Plugin",
"icon": "package",
"url": "#/plugins/my-plugin",
"section": "#/plugins/my-plugin"
}
],
"routes": [
{
"path": "/plugins/my-plugin",
"view": "plugin-my-plugin-settings",
"title": "My Plugin - Domma CMS"
}
],
"views": {
"plugin-my-plugin-settings": {
"entry": "my-plugin/admin/views/my-plugin-settings.js",
"exportName": "myPluginSettingsView"
}
}
},
"inject": {
"head": "public/inject-head.html",
"bodyEnd": "public/inject-body.html"
}
}All fields except admin and inject are required. If mandatory fields are missing, the plugin is skipped at startup
with a warning — the server never crashes.
plugin.js
A standard Fastify plugin. Use getPluginSettings to read merged settings (defaults + user overrides):
import { getPluginSettings } from '../../server/services/plugins.js';
export default async function myPlugin(fastify, opts) {
fastify.get('/api/plugins/my-plugin/hello', async (req, reply) => {
const settings = getPluginSettings('my-plugin');
return { message: `Hello from ${settings.greeting}` };
});
}config.js
Export a plain object with default settings. These are deep-merged with any user overrides stored in
config/plugins.json:
export default {
greeting: 'My Plugin',
enabled: true
};Admin View
Admin views are ES modules served as static files from /plugins/. Import apiRequest from the core API client for
authenticated requests with automatic token refresh:
import { apiRequest } from '/admin/js/api.js';
export const myPluginSettingsView = {
templateUrl: '/plugins/my-plugin/admin/templates/my-plugin-settings.html',
async onMount($container) {
const settings = await apiRequest('/plugins/my-plugin/settings');
// populate fields from settings…
$container.find('#save-btn').on('click', async () => {
await apiRequest('/plugins/my-plugin/settings', {
method: 'PUT',
body: JSON.stringify({ /* data */ })
});
E.toast('Settings saved.', { type: 'success' });
});
}
};apiRequest(endpoint, options) prepends /api automatically, handles Bearer tokens, and retries once with a
refreshed token on 401 before redirecting to login.
Public Injection
HTML files listed under inject in plugin.json are concatenated into {{headInject}} and {{bodyEndInject}} slots
in server/templates/page.html when the plugin is enabled.
Scripts Reference
| npm script | make target | Description |
|----------------------|-------------------|---------------------------------------------------------------------|
| npm run dev | make dev | Start server with --watch (auto-restart on save) |
| npm start | make start | Start server via pm2 in cluster mode (one process per CPU core) |
| — | make stop | Stop the pm2 process |
| — | make restart | Restart the pm2 process |
| — | make logs | Tail pm2 logs |
| npm install | make install | Install dependencies |
| npm run setup | make setup | Interactive first-run wizard (admin account, theme, title) |
| npm run reset | make reset | Factory reset — wipes content, users, resets config to defaults |
| npm run seed | make seed | Seed sample pages and content |
| npm run fresh | make fresh | Reset + seed in one step |
| npm run copy-domma | make copy-domma | Copy Domma dist from node_modules to public/ |
| — | make check | Syntax-check bin/cli.js |
| — | make scaffold | Dry-run the scaffolder, creates test-site/ locally |
Contributing
Running Locally
git clone https://github.com/pinpointzero73/domma-cms.git
cd domma-cms
npm install
npm run setup
npm run devThe server runs on http://localhost:3050 by default. Change the port in config/server.json.
Conventions
- Backend — Fastify 5, ESM (
"type": "module"). Routes inserver/routes/, services inserver/services/. - Frontend — Domma SPA in
admin/. Views export{ templateUrl, async onMount($container) }. Templates live alongside views inadmin/js/templates/. - HTTP — Always use
apiRequestfromadmin/js/api.js— never rawfetch()— so token refresh is handled automatically. - Storage — Config changes go through
getConfig()/saveConfig()fromserver/config.js. Never write config files directly from routes. - Plugins — Follow the three-file structure. Use
getPluginSettings(name)to read settings — never readconfig/plugins.jsondirectly.
Adding a New Admin View
- Create
admin/js/views/my-view.jsexporting{ templateUrl, onMount } - Create
admin/js/templates/my-view.html - Register the route in
admin/js/app.js - Add a sidebar entry in
admin/js/config/sidebar-config.jswith appropriate role guard
Licence
MIT © Darryl Waterhouse
