# Domma CMS

[![Node ≥24](https://img.shields.io/badge/node-%3E%3D24-brightgreen)](https://nodejs.org)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![npm](https://img.shields.io/npm/v/domma-cms)](https://www.npmjs.com/package/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](https://fastify.dev) on the backend and [Domma](https://npmjs.com/package/domma-js) on the frontend.

---

## Table of Contents

- [Quick Start](#quick-start)
- [Requirements](#requirements)
- [Project Structure](#project-structure)
- [Configuration](#configuration)
- [Content](#content)
- [Admin Panel](#admin-panel)
- [Built-in Features](#built-in-features)
- [Plugins](#plugins)
    - [Bundled Plugins](#bundled-plugins)
    - [Building a Plugin](#building-a-plugin)
- [Scripts Reference](#scripts-reference)
- [Contributing](#contributing)
- [Licence](#licence)

---

## Quick Start

```bash
npx domma-cms my-site
cd my-site
npm run dev
```

Open **http://localhost:4096/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** 24 or higher
- **npm** 11 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/
│   ├── 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.json
```

---

## Configuration

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.

```json
{
    "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`, `christmas-dark`, `christmas-light`, `dreamy-dark`,
`dreamy-light`, `forest-dark`, `forest-light`, `grayve-dark`, `grayve-light`, `lemon-dark`, `lemon-light`, `mint-dark`,
`mint-light`, `ocean-dark`, `ocean-light`, `royal-dark`, `royal-light`, `silver-dark`, `silver-light`, `sunset-dark`,
`sunset-light`, `unicorn-dark`, `unicorn-light`, `wedding-dark`, `wedding-light`

### `config/navigation.json`

Controls the public navbar.

```json
{
    "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.

```json
{
    "form-builder": {
        "enabled": true,
        "settings": { "smtp": { "host": "localhost" } }
    }
}
```

### `.env`

Secrets only - never commit this file.

```
JWT_SECRET=your-64-char-random-string
NODE_ENV=development
```

Generate 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:

```yaml
---
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:

```html
<!-- 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:4096/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)*

---

## Built-in Features

These features are part of the CMS core - no plugins required. They are configured via the admin panel under
**Site Settings** or through `config/site.json`.

| Feature            | Description                                                                                                                                                       |
|--------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Search**         | Full-text search over the site's pages - navbar icon or floating button, Cmd+K shortcut, weighted results (`config/search.json`, admin: **System > Search**)      |
| **Effects**        | Scroll-reveal animations on page elements (`[data-fx]`) and row shortcodes (`[data-reveal]`) via IntersectionObserver                                             |
| **Celebrations**   | Seasonal particle effects that activate automatically by date - Christmas, Halloween, Valentine's, Guy Fawkes, St Patrick's, St Andrew's, St David's, St George's |
| **Back to Top**    | Configurable scroll-to-top button with position, offset, and label options                                                                                        |
| **Cookie Consent** | GDPR cookie consent banner with per-category toggles (necessary, functional, analytics, marketing)                                                                |
| **Auto Day/Night** | Automatic theme switching between light and dark variants based on time of day                                                                                    |

---

## Plugins

Plugins extend Domma CMS with backend routes, public page injections, and admin panel views.

### Bundled Plugins

| Plugin             | Description                                                                      |
|--------------------|----------------------------------------------------------------------------------|
| **Analytics**      | Basic page view analytics - tracks hits per page using a simple JSON store       |
| **Contacts**       | Contact manager with groups, favourites, search, and import/export               |
| **Demo Viewer**    | Embeds interactive Domma JS demos via iframe shortcode                           |
| **Docs**           | Document editor with folders, version history, templates, and find & replace     |
| **Garage**         | UK vehicle management with DVLA API lookup, save vehicles, and search history    |
| **Notes**          | Rich note-taking with categories, search, and markdown content                   |
| **Theme Switcher** | Floating disc icon with colour theme dots for switching between all Domma themes |
| **Todo**           | Personal task manager with priorities and status tracking                        |

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 settings
```

#### `plugin.json`

```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):

```js
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`:

```js
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:

```js
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

```bash
git clone https://github.com/pinpointzero73/domma-cms.git
cd domma-cms
npm install
npm run setup
npm run dev
```

The server runs on **http://localhost:4096** by default. Change the port in `config/server.json`.

### Conventions

- **Backend** - Fastify 5, ESM (`"type": "module"`). Routes in `server/routes/`, services in `server/services/`.
- **Frontend** - Domma SPA in `admin/`. Views export `{ templateUrl, async onMount($container) }`. Templates live
  alongside views in `admin/js/templates/`.
- **HTTP** - Always use `apiRequest` from `admin/js/api.js` - never raw `fetch()` - so token refresh is handled
  automatically.
- **Storage** - Config changes go through `getConfig()` / `saveConfig()` from `server/config.js`. Never write config
  files directly from routes.
- **Plugins** - Follow the three-file structure. Use `getPluginSettings(name)` to read settings - never read
  `config/plugins.json` directly.

### Adding a New Admin View

1. Create `admin/js/views/my-view.js` exporting `{ templateUrl, onMount }`
2. Create `admin/js/templates/my-view.html`
3. Register the route in `admin/js/app.js`
4. Add a sidebar entry in `admin/js/config/sidebar-config.js` with appropriate role guard

---

## Licence

MIT © Darryl Waterhouse
