fubber / mini-cms
Template-first CMS aspect for fubber/mini — inline editing, media library, auto CRUD, and admin panel
Requires
- php: >=8.3
- fubber/mini: ^0.17.0
README
A template-first CMS aspect for fubber/mini. Adds inline editing, a media library, auto CRUD for database models, an admin panel, and an AI assistant — all layered on top of a plain Mini application without changing how the app itself works.
Requirements
- PHP 8.3+
- fubber/mini ^0.17.0
Installation
composer require fubber/mini-cms
Quick start
Create the content directory structure your site needs:
_content/
routes.php # Page definitions
models.php # Data model registrations
site.json # Site name and description
_views/ # Your templates (override CMS defaults)
_static/ # Your CSS, images, etc.
uploads/ # Media uploads (created at runtime)
Define pages
In _content/routes.php, map URL paths to pages:
<?php use MiniCms\Page; return [ '/' => new Page('pages/home', title: 'Home'), '/about' => new Page('pages/about', title: 'About'), ];
Each page points to a view template in _views/. Views are plain PHP using Mini's template inheritance.
Inline-editable content
Use helper functions in your views to create regions the admin can edit:
<?= cms_text('home.json', 'Hero|Heading', 1, 'Welcome', 'h1') ?> <?= cms_html('home.json', 'Hero|Body', 2, '<p>Default content here.</p>') ?> <?= cms_image('home.json', 'Hero|Background', 3, '/default.jpg', 'img', 'Alt text', '16x9') ?>
Content is stored as JSON files in _content/. When the admin is logged in, these regions become editable in-place.
Data models
Register database models for auto CRUD in _content/models.php:
<?php use MiniCms\Entity; use App\Testimonial; return [ 'testimonials' => (new Entity(Testimonial::class, icon: 'bi-chat-quote', pluralTitle: 'Testimonials')) ->withDefaultOrder('sort_order ASC'), ];
The admin panel automatically provides list, create, edit, and detail views. Customize any view by overriding it:
'testimonials' => (new Entity(Testimonial::class, icon: 'bi-chat-quote')) ->withIndexView('admin/data/testimonials/index.php') ->withEditView('admin/data/testimonials/edit.php'),
Entity relationships
When a model has a #[ForeignKey] attribute pointing to another registered entity, the CMS renders the field as a searchable picker in forms and as an inline reference in detail views.
#[ForeignKey(navigation: 'customer')] public int $customer_id;
Admin panel
The admin panel lives under /admin/ and provides:
- Page editor — inline editing with a component sidebar
- Media library — upload, browse, crop, and organize images
- Data management — auto-generated CRUD for registered models
- Site settings — edit site name and metadata
- AI assistant — chat with an AI agent that can read and edit your site files
Access the admin at /admin/ after setting CMS_PASSWORD in your .env file.
Preview navigation
When you are logged in, the page editor renders your site inside a preview
iframe. How that preview navigates is set by navigation in _content/site.json:
{
"name": "My Site",
"navigation": "document"
}
-
"document"(default) — the admin shell rewrites the preview's links to target_top, so each navigation reloads the shell and the editable-field sidebar is rebuilt server-side. This is the behaviour of every MiniCMS site that predates the setting, and omitting the key keeps it. -
"client"— your site handles its own navigation (HTMX, a client router, anything that swaps content without a document load). The shell leaves the preview's DOM alone and waits to be told where it went.
An unrecognised value throws rather than falling back, so a typo surfaces immediately instead of looking like the setting does nothing.
In "client" mode your site takes on two obligations:
// 1. Announce each navigation, so the shell can resync the sidebar. window.parent.postMessage({ type: 'cms-navigated', path: '/about' }, '*'); // 2. Do not swap content while edit mode is active. The shell broadcasts // 'cms-enter-edit' and 'cms-exit-edit' / 'cms-cancel-edit'; between them it // is about to read the contenteditable DOM back, and replacing it discards // the user's unsaved edits.
The shell currently handles cms-navigated by reloading itself at the new path
— correct, but it costs a page load per navigation while logged in. A future
component-list endpoint will let it swap the sidebar in place instead, with no
change required on the site side.
AI assistant
Refreshing the preview from the agent
While the assistant edits templates or styles, the admin's preview iframe still shows the old render. The agent can refresh it itself:
php vendor/fubber/mini-cms/bin/cms-preview reload php vendor/fubber/mini-cms/bin/cms-preview navigate /en/projects
The agent runs detached and has no HTTP session, so it cannot reach the browser
directly. It writes .cms/ai-preview.json instead (atomically, via
write-then-rename), and the already-open response stream forwards the command
as a {"ctl": {...}} frame on its next poll. Those frames deliberately carry
no pos key, so they share the connection without disturbing the byte offset
the client uses to resume after a reconnect.
reload refreshes the iframe in place. navigate moves the whole admin shell,
not just the iframe — the editable-field sidebar is rendered server-side per
page, so re-pointing the iframe alone would leave it describing the previous
page. The shell reopens the assistant drawer and resumes the stream on load, so
the conversation survives the navigation.
Two things it will not do: paths must be same-origin and absolute (//host is
protocol-relative and would take the admin off-site), and commands are dropped
while the user has unsaved inline edits rather than discarding their work.
Commands only reach the browser while a response stream is open — that is, during the agent's own turn.
The CMS includes an AI assistant available as a drawer in the page editor and as a standalone page at /admin/ai/. It uses an adapter pattern — the default implementation (ClaudeCodeAgent) invokes the Claude Code CLI, but other backends can be swapped in by registering a different AgentInterface implementation.
The assistant has full context of your site structure and can read and modify files directly.
How aspects overlay
MiniCMS registers its routes, views, and static assets through Mini's path registry. Your application's files always take priority — if you define _views/partials/header.php, it overrides the CMS default. This lets you customize any part of the admin UI or public-facing templates without forking the package.
Building JS assets
If you need to modify the CMS JavaScript:
cd vendor/fubber/mini-cms npm install npm run build # one-time build npm run watch # rebuild on changes
The bundle outputs to _static/admin/dist/cms.min.js. The built file is committed to the repo, so end users don't need Node.js.
Constraints
- No external CDNs. All assets (Bootstrap, AdminLTE, fonts, icons) are served locally. This is a privacy requirement.
- Filesystem content + git = versioning. All content lives in flat files, so your entire site is version-controlled by default.