Documentation
Custom Modules
How to add first-party DesertCMS modules with admin settings, public routes, static artifacts, tests, and deployment hooks.
CUSTOM_MODULES.mdDesertCMS modules are first-party features that can be enabled from the admin UI and rebuilt into the public site. Existing examples are Map, Shop, Gallery, Forms, and Documentation.
Use a module when the feature has its own public path, admin settings, generated assets, or database-backed behavior. Use normal page content when the work is only editorial content.
Module Shape
A typical module touches these areas:
lib/DesertCMS/Modules.pmdefines the module card and enable rule.lib/DesertCMS/Settings.pmprovides defaults and whitelists saved settings.lib/DesertCMS/App.pmexposes admin settings routes and dynamic public endpoints when needed.lib/DesertCMS/Renderer.pmwrites static public pages, navigation links, sitemap entries, and cleanup behavior.lib/DesertCMS/DB.pmowns schema changes when the module needs tables.themes/default/assets/site.csscarries public presentation.t/17_modules.tverifies enable, disable, navigation, generated files, and sitemap behavior.
Register The Module
Add a definition in DesertCMS::Modules::definitions:
{
key => 'docs',
label => 'Documentation',
description => 'Adds a public /docs/ markdown knowledge base.',
public_path => '/docs/',
settings_path => '/admin/settings/modules/docs',
enabled => enabled($settings, 'docs'),
}
Then add the enable rule in enabled:
if (($key || '') eq 'docs') {
return _truthy(_setting($settings, 'module_docs_enabled', 0));
}
Default to disabled unless the module is core to every DesertCMS site. Map defaults on; optional modules such as Gallery, Forms, and Documentation default off.
Add Settings
Add defaults in DesertCMS::Settings::all:
module_example_enabled => _config_truthy($config, 'module_example_enabled', 0),
example_title => 'Example',
example_intro => 'A short public description.',
Add the same keys to the allowed list in set_many. Settings not listed there are ignored on save.
For settings that can come from /etc/desertcms.conf, use config defaults first, then allow admin settings to override them through the database.
Add Admin Routes
Module settings routes live under /admin/settings/modules/<module>.
if ($path eq '/admin/settings/modules/example') {
return $self->_require_login($request, sub {
$self->_module_example_settings_page($request, @_);
});
}
if ($path eq '/admin/settings/modules/example/save') {
return $self->_require_post($request, sub {
$self->_module_example_settings_save($request, @_);
});
}
The save handler should:
- Verify CSRF through
_require_post. - Normalize user input before saving.
- Call
DesertCMS::Settings::set_many. - Rebuild public output when the setting changes public behavior.
- Return the same settings page with a short status message.
Keep the form simple. The admin UI is for operators, not developers.
Publish Public Artifacts
Hook the module into DesertCMS::Renderer::rebuild_indexes:
if (DesertCMS::Modules::enabled($site, 'example')) {
_write_example_page($config, $db);
} else {
_remove_example_artifacts($config);
}
A good public module writes predictable static files:
/example/index.html- optional data under
/assets/example.json - related CSS through the theme asset pipeline
- sitemap entries
- navigation links
Cleanup matters. If a module is disabled, remove generated module files so stale public pages do not remain visible.
Dynamic Endpoints
Prefer static public pages. Add dynamic endpoints only when the browser must post or fetch live data, such as comments, ratings, analytics, forms, shop checkout, or webhooks.
Dynamic public routes should:
- Use the existing CGI app and main site config.
- Validate request method.
- Rate limit or token-check public writes.
- Avoid exposing private originals, config secrets, or admin-only data.
- Return compact JSON or a redirect, not a large app shell.
Database Changes
Database migrations belong in DesertCMS::DB. Keep schema ownership explicit and make migrations idempotent.
For a module table:
CREATE TABLE IF NOT EXISTS example_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
created_at INTEGER NOT NULL
)
Do not make module data depend on public generated files as the source of truth. Public files are rebuild artifacts.
Security Checklist
- Public writes need CSRF, signed tokens, rate limits, or IP/user-agent hashes depending on the workflow.
- Admin saves need
_require_postand existing session checks. - Uploaded originals stay outside the public root.
- Generated public files should contain escaped user content.
- Webhook handlers must verify provider signatures before mutating state.
- Module settings should be whitelisted in
DesertCMS::Settings. - Disable cleanup should remove stale public artifacts.
Test Checklist
At minimum, update t/17_modules.t to prove:
- The module defaults to the intended enabled state.
- Enabling it adds navigation and generated public files.
- The generated HTML contains expected content.
- The sitemap includes module URLs.
- Disabling it removes navigation, generated files, and sitemap URLs.
Run:
prove -l t/01_syntax.t t/17_modules.t
On OpenBSD staging, run the same tests before activation.