NafCMS¶
naf/cms adds content administration to a NAF application: localized articles,
pages, forms, media, navigation, themes, users, permissions and audit history.
Its page, API, content and block registries let plugins extend those workflows.
The host owns configuration, runtime storage and the public webroot.
Use CMS 0.0.3 or newer with PHP 8.3 or 8.4, Composer 2, cURL, DOM, PDO and a PDO SQLite or MySQL driver. Framework 0.2.13+, CLI 0.2.2+, Form 0.2.4+ and MCP 0.2.6+ are required and installed as dependencies. Node.js is needed only to rebuild CMS frontend assets.
Required packages
In an existing NAF application:
composer require naf/cms
Also installs these transitive dependencies: naf/auth, naf/cli, naf/database, naf/form, naf/i18n, naf/mail, naf/mcp, naf/orm, naf/queue, naf/schedule, naf/session, naf/view.
Quick start¶
Create a disposable application, then install the CMS:
composer create-project naf/app my-cms
cd my-cms
composer require naf/cms
Create app/plugins.php with the CMS after its dependencies. This file orders
installed plugins; it does not install them or exclude other installed plugins.
<?php
declare(strict_types=1);
return [
'naf/cli',
'naf/database',
'naf/session',
'naf/auth',
'naf/form',
'naf/view',
'naf/i18n',
'naf/orm',
'naf/queue',
'naf/schedule',
'naf/mail',
'naf/mcp',
'naf/cms',
];
For this SQLite example, replace the disposable application's app/config.php:
<?php
declare(strict_types=1);
return [
'database' => [
'driver' => 'sqlite',
'database' => BASE_PATH . '/storage/nafcms.sqlite',
],
];
Create storage, publish the shipped assets and apply migrations from the host root:
mkdir -p storage
vendor/bin/naf nafcms:assets:publish
vendor/bin/naf nafcms:assets:check
vendor/bin/naf db:migrate up
vendor/bin/naf user:create admin admin@example.com admin
php -S 127.0.0.1:8080 -t public
user:create prompts for the password twice without terminal echo. It creates the
first installation administrator only; create additional users in the protected
administration UI. For unattended setup, use --password-file pointing to a
temporary mode-0600 file, then remove that file. Do not pass passwords as arguments.
Open http://127.0.0.1:8080/admin. Before login, it redirects to /admin/login.
Sign in with the administrator you created to reach the dashboard. The plugin
repository has no front controller and cannot be served directly.
For MySQL/MariaDB, configure DB_DRIVER, DB_HOST, DB_PORT, DB_USERNAME,
DB_PASSWORD and DB_DATABASE instead. db:migrate up applies pending migrations;
install:database may also create the configured database. Use the migration-only
command when infrastructure manages database creation.
Add a protected page¶
In the example application, replace app/routes.php with this registration. In an
existing application, add it alongside the application's other routes.
<?php
declare(strict_types=1);
use Naf\CMS\Enums\Permission;
use Naf\CMS\Page\Page;
use function Naf\CMS\cms;
use function Naf\View\view;
cms()->pages()->add(
Page::create('app.reports')
->setPath('/admin/reports')
->setTitle('Installation report')
->setLayout('admin')
->requirePermission(Permission::SYSTEM_READ)
->setAdminNavigation('Installation report', parent: 'system', icon: 'stats')
->setContent(static fn(): string => view('reports', [
'message' => 'The CMS extension is running.',
])),
);
cms()->pages()->registerRoutes();
Create the host-owned template app/views/reports.phtml:
<?php
use function Naf\View\s;
?>
<h1>Installation report</h1>
<p><?= s($message) ?></p>
After login, visit /admin/reports. It displays the report inside the CMS admin
layout. The page requires system.read; hiding a navigation item alone does not
enforce authorization. A plugin can register the same Page from its bootstrap,
with replace: true for an intentionally idempotent registration.
Page content callbacks can return a string for the page layout or a PSR-7 response.
Use Page::setDataHandler() for a form posted back to a page; data handlers must
return a response. Register JSON/actions through cms()->api() and
Naf\CMS\Api\ApiEndpoint; keep workflow logic in handlers and application services.
Content, blocks and extension points¶
Use the highest existing data-access layer that covers the operation:
| Layer | Purpose |
|---|---|
cms()->articles() or cms()->content(Model::class) |
Domain actions, validation, lifecycle rules and audit entries |
A content API's storage() |
Generic entity CRUD and criteria-based reads |
| A content API's repository, where exposed | Joins, aggregates and named queries |
| The configured NAF PDO connection | Migrations and SQL not covered by the higher layers |
Keep audit entries at the domain-action boundary so storage and repository calls do
not record the same action twice. cms()->contentTypes() registers content APIs.
cms()->blockTypes() describes editable PageBuilder fields; cms()->blocks() maps
saved block types to views. Unknown block types render the placeholder view.
Navigation defaults to the frontend scope. Use cms()->scope(Scope::ADMIN) with
Naf\CMS\Enums\Scope when contributing admin navigation directly. A registered
page can carry its own admin navigation metadata, as in the example above.
The extension examples cover protected pages, content types, blocks, scoped MCP tools, a complete Composer plugin and service replacement. Register integrations explicitly; placing a PHP file in a directory does not activate a CMS feature.
Runtime files and updates¶
Package defaults under vendor/naf/cms stay read-only. Installation-specific
prompts, memory, translation overrides, rate-limit counters and embedding caches
use storage/nafcms by default. Relative cms:runtime_path values resolve below
the host root and reject ..; absolute paths support deliberate external volumes.
Reads prefer the host override and fall back to the package default.
The asset publisher copies the package's CSS, JavaScript, fonts and images into
the host public directory. .nafcms-assets.json records ownership and hashes.
Publishing updates unchanged owned files and rejects host-modified collisions;
removal preserves modified host files. Generated theme CSS is installation data.
Back up the database, runtime files, uploaded media and generated themes before
updating. Then run Composer, nafcms:assets:publish, nafcms:assets:check and
db:migrate up, and verify login and the workflows your installation uses.
nafcms:agents:install creates a CMS project guide only when the host has none.
It preserves user-owned files; the current starter's AGENTS.md therefore makes
the command exit with status 1. Review and combine the
CMS guide template
with that existing guide. --update refreshes only a CMS-managed guide.
Authentication, MCP and local AI¶
CMS login uses naf/auth with a database provider and CMS session adapter.
Page/API permissions and domain privilege checks remain enforced in the CMS.
Anonymous HTML access redirects to login; API denials return JSON with 401 or 403.
Logout is a CSRF-protected POST. Existing legacy sessions can be restored through
the database provider; see the
Auth integration notes.
Verify session revocation separately from password changes. In CMS 0.0.2, resetting a password does not by itself end previously authenticated sessions. Incident response must also invalidate those sessions in the configured backend. Test with a second browser session and check that its next protected request requires authentication.
MCP authentication is enabled with file-backed tokens by default. Create tokens
with vendor/bin/naf mcp:token:create and grant only the scopes the client needs.
Tools use the executable registry; cms_system_health, cms_queue_status and
cms_article_status_counts expose concrete read capabilities. See MCP tools.
The optional local assistant is disabled by default and uses browser-local Ollama. Its selected tools come from tool metadata, permissions and scopes. An optional embedding model helps rank tools; failure falls back to keyword routing. The first preview does not persist remote-provider API credentials. Confirmation dialogs provide guidance; server-side permissions and scopes enforce access.
Server-side webhooks require public HTTPS targets. The outbound policy validates and pins resolved addresses and rechecks redirects. Local development HTTP requires both an explicit opt-in and an exact allowed hostname. Server-side Ollama embeddings accept only loopback hosts and configured ports.
Configuration¶
Add overrides to the array returned by host app/config.php. These are the CMS
and AI keys; the generated configuration reference
also lists shared application, database, session, view, schedule and MCP defaults.
| Key | Type | Default | Effect |
|---|---|---|---|
cms:runtime_path |
string | storage/nafcms |
Host-owned runtime root |
cms:languages |
string list | ['en', 'de'] |
Initial content languages |
cms:admin:accent_colors |
map | Colors by admin area | Admin navigation accents; see generated defaults |
cms:media:storage_path |
string | {BASE_PATH}/storage/media |
Uploaded media storage |
cms:media:public_url |
string | /media |
Media URL prefix |
cms:media:max_upload_size |
integer | 26214400 |
Maximum upload bytes |
cms:media:allowed_extensions |
map | Shipped extension/MIME allow-list | Both extension and MIME must match; SVG is excluded |
cms:outbound_http:webhooks:allow_development_http |
boolean | false |
Enables the explicit local HTTP exception |
cms:outbound_http:webhooks:development_hosts |
string list | [] |
Exact hostnames allowed by that exception |
cms:outbound_http:webhooks:timeout |
number | 8.0 |
Request timeout in seconds |
cms:outbound_http:webhooks:max_response_bytes |
integer | 65536 |
Webhook response limit |
cms:outbound_http:ollama:allowed_ports |
integer list | [11434] |
Allowed loopback embedding ports |
cms:outbound_http:ollama:max_response_bytes |
integer | 1048576 |
Embedding response limit |
ai:enabled |
string | '0' |
Local assistant enabled flag |
ai:type |
string | local |
Local Ollama provider; remote mode is reserved |
ai:url |
string | http://localhost:11434 |
Ollama endpoint |
ai:model |
string | llama3.1:8b |
Chat model; choose one supporting tools |
ai:embedding_model |
string | empty | Optional semantic tool-ranking model |
ai:routing_debug |
boolean | false |
Allows capped query text in routing diagnostics |
ai:instructions |
string | empty | Installation-specific assistant guidance |
Session proxy trust is disabled by default. If TLS terminates at a proxy, configure the session plugin's exact trusted proxy addresses together with its trust flag; see Sessions. Keep secrets in the host environment and runtime files outside the public webroot. The PHP built-in server is for local development.
API details¶
Import helpers explicitly from Naf\CMS:
use function Naf\CMS\cms;
$pages = cms()->pages();
$apis = cms()->api();
$contentTypes = cms()->contentTypes();
$blocks = cms()->blockTypes();
The facade also exposes settings, languages, navigation, themes, visualizations
and the content APIs. The generated function index
contains helper signatures. Use Naf\Auth\auth() for authentication;
Naf\CMS\user() exposes the authenticated CMS user or null. Test-only overrides
and resets are not authentication mechanisms.
The CLI reference lists installation and maintenance
commands, arguments and options. From a configured contributor checkout,
vendor/bin/naf docs:check verifies registry-derived reference blocks and
vendor/bin/naf integrity:check checks the CMS registrations and resources.
If the result is different¶
| Symptom | Check |
|---|---|
| Bootstrap reports a missing dependency helper | Put CMS after its installed dependencies in app/plugins.php. Run commands from the host. |
| Database or migration command fails | Check the PDO driver, configured database and host storage permissions before db:migrate up. |
| The admin page has missing styles or scripts | Run nafcms:assets:publish and nafcms:assets:check; review modified-file collisions. |
/admin redirects to login |
This is the expected anonymous response. Create the first admin and sign in. |
user:create refuses another user |
It bootstraps the first admin only. Use the authenticated user administration afterwards. |
| The agent guide installer exits with 1 | An existing user-owned guide was preserved. Combine the CMS template with it manually. |
| A custom page remains unavailable | Register the page and its routes, check its path/name, then verify the required permission. |
| Embedding selection fails | Check the loopback endpoint and allowed port; keyword routing remains the fallback. |
Preview compatibility
Version 0.0.1 is the first CMS preview. Sitebuilder payloads, extension APIs and
storage shapes may change before 0.1. The builder accepts builderVersion: 1,
with bounded payload size, block counts and nesting. It has no undo/redo,
collaborative editing or guaranteed autosave recovery. Local AI is Ollama-only,
custom theme JavaScript is disabled, and PHP 8.5 is a compatibility target.
Version 0.0.1 omitted template-only Tailwind utilities from the shipped CSS. Upgrade to 0.0.2 or newer and republish/check assets before using that installation.
Related pages¶
- Plugin development for Composer discovery and extension boot order.
- Authentication, forms and sessions.
- Database and migrations and ORM.
- Queues, scheduling and MCP tools.
- Release notes and security reporting.