Plugins
Extend Domma CMS with custom routes, admin views, hooks, and public injections

Architecture (3 Mandatory Files)

A plugin is a self-contained directory inside plugins/ consisting of exactly three required files. Each file has a distinct responsibility: the manifest declares metadata, the server file registers backend logic, and the admin file provides the frontend integration.

plugins/
└── my-plugin/
    ├── plugin.json     # Manifest — name, slug, description, version, author
    ├── server.js       # Backend: Fastify routes, hooks
    └── admin.js        # Frontend: admin views, sidebar entries

Enable or disable any plugin by editing config/plugins.json. The CMS will not load a plugin unless its entry is present and enabled is set to true.

[
  { "slug": "my-plugin", "enabled": true }
]

Disabling a plugin ("enabled": false) is non-destructive — the directory and all its data remain intact.

Plugin Manifest (plugin.json)

The manifest is the single source of truth for plugin identity and capabilities. The CMS reads it before loading any code, so mismatches here will prevent the plugin from mounting.

{
  "name": "My Plugin",
  "slug": "my-plugin",
  "version": "1.0.0",
  "description": "Does something useful",
  "author": "Your Name",
  "minCmsVersion": "0.5.0",
  "permissions": ["pages:read", "collections:read"],
  "public": {
    "injectHead": "head.html",
    "injectBody": "body.html"
  },
  "adminNav": {
    "text": "My Plugin",
    "icon": "package",
    "view": "my-plugin"
  }
}

Key fields:

  • slug — Must match the directory name and the entry in config/plugins.json. Lowercase, hyphenated.
  • minCmsVersion — The CMS will refuse to load the plugin if the running version is older. Use semantic versioning.
  • permissions — Declares which CMS data scopes the plugin accesses. Shown to the user at install time.
  • public.injectHead / public.injectBody — Paths to HTML snippets for server-side injection into public pages (see Section 6).
  • adminNav — Registers a sidebar entry. The view key must match a view name exported by admin.js.

Backend Plugin (server.js)

The server.js file exports a standard Fastify plugin function. The CMS passes an opts object containing a cms property which exposes the full CMS service layer — content, collections, media, users, and hooks.

export default async function(fastify, opts) {
    const { cms } = opts;  // Access to CMS service layer

    // Register Fastify routes
    fastify.get('/api/my-plugin/data', async (request, reply) => {
        // Access CMS services:
        const pages = await cms.content.getAll();
        return { pages };
    });

    fastify.post('/api/my-plugin/action', {
        preHandler: [fastify.authenticate]  // Require auth
    }, async (request, reply) => {
        // ...
    });
}

Notes:

  • Plugin routes are automatically scoped and registered by the CMS — you do not need to call fastify.register() yourself.
  • Use fastify.authenticate as a preHandler to restrict routes to logged-in admin users.
  • Access any CMS service via cms: cms.content, cms.collections, cms.media, cms.users, cms.hooks.
  • The function must be async and the default export must be the plugin function directly (not an object).

Admin Integration (admin.js)

The admin.js file exports a view definition that the Domma CMS admin SPA registers at runtime. Views are mounted inside the admin shell — the full Domma API is available in onMount.

export default {
    views: {
        'my-plugin': {
            title: 'My Plugin',
            template: `
                <div class="container py-6">
                    <h1>My Plugin</h1>
                    <!-- Domma components work here -->
                </div>
            `,
            onMount($el) {
                H.get('/api/my-plugin/data').then(data => {
                    T.create('#my-table', { data: data.pages, columns: [...] });
                });
            }
        }
    },
    sidebar: [
        { text: 'My Plugin', icon: 'package', view: 'my-plugin' }
    ]
}

Available Domma APIs inside onMount:

  • H.get(), H.post(), H.put(), H.delete() — HTTP requests to your plugin's backend routes
  • T.create() — DataTable with sorting, filtering, pagination
  • E.modal(), E.toast(), E.confirm() — UI components
  • F.create() — Blueprint-driven form generation
  • M.create() — Reactive models for state management
  • $el — Domma-wrapped container element for scoped DOM operations

The view key ('my-plugin') must match the adminNav.view value declared in plugin.json.

Hook System

Plugins can intercept CMS lifecycle events by registering hook callbacks via cms.hooks.on(). Hooks allow plugins to modify content before rendering, react to form submissions, register custom shortcodes, and sanitise HTML output — all without patching core CMS code.

// In server.js
cms.hooks.on('page:before-render', (page) => {
    // Modify page before rendering
    page.content += '\n<!-- rendered by my-plugin -->';
    return page;
});

cms.hooks.on('form:submission', async (submission) => {
    // React to form submissions
    await sendToExternalCRM(submission);
});

// Register a custom shortcode
cms.hooks.registerShortcode('my-widget', (attrs) => {
    return `<div class="my-widget">${attrs.content}</div>`;
});

// Sanitize hook (modify HTML after rendering)
cms.hooks.on('html:sanitize', (html) => html.replace(/foo/g, 'bar'));

Available events:

Event Triggered when Payload
page:before-render A public page is about to be rendered Page object (mutable)
page:after-render A public page has finished rendering Rendered HTML string
form:submission A CMS form receives a submission Submission data object
media:upload A file is uploaded via the media manager File metadata object
collection:entry-save A collection entry is created or updated Entry object with collection name
user:login An admin user logs in User object (read-only)
user:register A new admin user is registered New user object
html:sanitize After HTML rendering, before output HTML string (return modified string)

Hooks that return a value replace the original payload. Async hooks are awaited by the CMS before proceeding. Multiple plugins may register the same event — they run in registration order.

Public Injection (Head/Body Slots)

Plugins can inject HTML into every public page without touching the theme templates. Two injection slots are available, both declared in plugin.json under the public key and resolved relative to the plugin directory.

injectHead — inserted in <head>

Use for CSS, meta tags, preload hints, fonts, and anything that must be parsed before the page renders.

<!-- head.html -->
<link rel="stylesheet" href="/plugins/my-plugin/style.css">
<meta name="my-plugin-version" content="1.0.0">

injectBody — inserted before </body>

Use for JavaScript, analytics snippets, chat widgets, or anything that should load after page content.

<!-- body.html -->
<script src="/plugins/my-plugin/tracker.js"></script>
Server-side injection — both slots are rendered by the CMS server before the response is sent. The injected content is always present in the initial HTML, making it compatible with SEO crawlers and non-JavaScript environments.

Injections are only active when the plugin is enabled. Disabling the plugin immediately removes all injections from public pages without requiring a restart.

Bundled Plugins

Domma CMS ships with a set of ready-to-enable plugins. They are installed but disabled by default — add the relevant entry to config/plugins.json to activate any of them.

domma-effects

Adds Domma's full visual effects library to public pages — breathe, reveal, scribe, scramble, counter, ripple, and more. Configure which effects are active and their default options via the plugin settings view in admin.

analytics

Simple self-hosted analytics: page views, unique visitor counts, top pages, referrers, and device breakdown. All data stays on your server — no third-party tracking. A dashboard view is added to the admin sidebar.

theme-roller

Visual theme customisation tool accessible from the admin toolbar. Live preview of CSS variable overrides, theme selection, and one-click export. Changes can be saved as a custom theme file or applied directly to the site config.

cookie-consent

GDPR-compliant cookie consent banner with configurable consent categories (necessary, analytics, marketing), customisable copy, and a link to your privacy policy. Consent state is stored in localStorage and respected across sessions.

back-to-top

Injects a configurable back-to-top button into all public pages. Appears after the user scrolls past a configurable threshold. Smooth scroll behaviour, position, and appearance are all configurable via plugin.json.

Enable any bundled plugin by adding its slug to config/plugins.json:

[
  { "slug": "analytics",      "enabled": true },
  { "slug": "cookie-consent", "enabled": true },
  { "slug": "back-to-top",    "enabled": true }
]

Building a Plugin (Step-by-Step)

Follow these steps to build and register a minimal working plugin — a simple hit counter with an admin view and an increment button.

  1. Create the directory
    mkdir plugins/my-counter
  2. Write plugin.json — name, slug, version, description
    {
      "name": "My Counter",
      "slug": "my-counter",
      "version": "1.0.0",
      "description": "A simple hit counter",
      "author": "Your Name",
      "adminNav": {
        "text": "Counter",
        "icon": "hash",
        "view": "my-counter"
      }
    }
  3. Write server.js — register GET /api/my-counter/count returning a stored count
    export default async function(fastify, opts) {
        const { cms } = opts;
        let count = 0;
    
        fastify.get('/api/my-counter/count', async () => ({ count }));
    
        fastify.post('/api/my-counter/increment', {
            preHandler: [fastify.authenticate]
        }, async () => {
            count += 1;
            return { count };
        });
    }
  4. Write admin.js — view with a counter display and increment button using Domma UI
    export default {
        views: {
            'my-counter': {
                title: 'Counter',
                template: `
                    <div class="container py-6">
                        <h1 class="mb-4">Hit Counter</h1>
                        <p class="text-xl mb-4">Count: <strong id="count-display">...</strong></p>
                        <button id="increment-btn" class="btn btn-primary">Increment</button>
                    </div>
                `,
                onMount($el) {
                    H.get('/api/my-counter/count').then(data => {
                        $el.find('#count-display').text(data.count);
                    });
    
                    $el.find('#increment-btn').on('click', () => {
                        H.post('/api/my-counter/increment').then(data => {
                            $el.find('#count-display').text(data.count);
                            E.toast('Counter incremented', { type: 'success' });
                        });
                    });
                }
            }
        },
        sidebar: [
            { text: 'Counter', icon: 'hash', view: 'my-counter' }
        ]
    }
  5. Register the plugin — add to config/plugins.json
    [
      { "slug": "my-counter", "enabled": true }
    ]
  6. Start the server
    npm run dev
  7. View in admin — the Counter sidebar entry appears automatically. Navigate to it to see the counter and increment button.
  8. Public injection (optional) — create head.html and/or body.html in the plugin directory and declare them under the public key in plugin.json to inject content into every public page.