This guide explains how a content/marketing administrator uses the ERPat CMS to publish public pages and blog posts, build pages from reusable components, manage media and taxonomy, moderate engagement, and safely take over the public homepage or a module landing page.
Safe-by-default posture. Everything that could change the live public site ships off. Enabling the module never alters your existing homepage or Community page; AkbAI comment scanning, guest commenting, Lighthouse and every route override start disabled, and every sensitive action is audit-logged and reversible. You turn on each capability deliberately, one switch at a time.
1. Enabling the module
The CMS is an HMVC module. Until it is enabled, none of its admin screens, public
routes (/blog, /page/...), or APIs respond.
- Sign in as an Admin (or a user holding the CMS permission).
- Go to Settings → Manage Modules.
- Enable CMS (this writes the master toggle
settings.module_content = 1). - Run the module migrations if this is a fresh install:
php erpat migrate:modules(creates the 23cms_*tables, tracked inmigrations_content). The migration also seeds the defaultcms_settings.
Once enabled, a new CMS group appears in the left sidebar.
There are two layers of "on":
| Layer | Where | Default | Effect |
|---|---|---|---|
| Module master toggle | Settings → Manage Modules (module_content) |
off | The whole module + admin + APIs |
| Public surface flag | CMS → Settings (cms.enabled) |
on (once module on) | The public /blog, /page/... routes |
So even with the module on, the public site only responds while cms.enabled is
true — and assigned homepage/module takeovers stay off until you explicitly turn
on their feature flags (Section 11).
2. The CMS menu
The sidebar CMS group (gated per-permission) contains:
- Dashboard — counts (published pages/posts, drafts, scheduled, pending comments, SEO needs-review, Lighthouse needs-review), recent content, and the state of each safety flag.
- Pages — static / landing pages.
- Blog Posts — time-based articles.
- Components — the installed component Library (code packages) and your Reusable Components (saved configurations).
- Templates — starter block documents for new pages/posts.
- Media Library — uploaded images with alt text.
- Categories / Tags — taxonomy.
- Comments — moderation queue (with AkbAI verdict badges).
- Reactions — engagement view.
- User Bans — temporary CMS commenter bans.
- Route Assignments — assign the homepage / Community page to a CMS page.
- SEO — analyzer + per-page SEO scores.
- Redirects — source → target redirect rules.
- Analytics — view/engagement metrics.
- API & Integrations — Client + End-User API status and OpenAPI links.
- Settings — feature flags, moderation mode, AkbAI, ban thresholds.
A "SEO & Lighthouse" working area lives under SEO (the SEO list, analyzer and Lighthouse runs). Each item is hidden if you lack its permission.
3. Creating pages and posts
CMS → Pages → Add (or Blog Posts → Add) opens the full-page editor.
Common fields:
- Title (required) and Slug (auto-derived from the title; you can override
it). Slugs are normalized, checked against a reserved-slug list (
admin,api,blog,community, ... — seeconfig/cms.php), and made unique within the content type. A reserved or duplicate slug is rejected with a clear message. - Excerpt, Featured image (from the Media Library), Template.
- Visibility —
public,private, ornoindex(MVP set).privatecontent never renders publicly;noindexrenders but is told to search engines to not index, and is excluded from the sitemap. - Posts only: Categories and Tags.
A new item is saved as a draft. Publishing is a separate, permission-gated action (Section 4). Saving writes a content row, its SEO/meta satellite, taxonomy links, and an immutable revision snapshot, and records an audit event.
4. The publish lifecycle
From the editor you transition status via the lifecycle actions
(Content_admin::update_status):
| Action | Status set | Requires |
|---|---|---|
| Publish | published (stamps published_at) |
content_publish |
| Unpublish | unpublished |
content_publish |
| Schedule | scheduled (stores scheduled_at) |
content_publish |
| Trash | trashed (soft delete) |
content_delete |
| Restore (from trash) | back to draft |
content (base) |
Scheduled items go live automatically: the cron job
content_publish_scheduled runs every 5 minutes, promotes any scheduled item
whose time has arrived, snapshots the published revision, and audits the event.
Only published content with public/unlisted visibility ever renders on the
public site. If a published page is later unpublished or deleted, the public route
behaves exactly like a missing page (404) — there is no stale render.
5. The section builder + reusable components
CMS pages are not free-form HTML. They are composed of whitelisted, server-rendered components configured with JSON props. This keeps pages safe, searchable, cacheable and accessible.
There are two layers (the distinction matters):
- Component definitions (the Library) — the code packages that define a
component type (its renderer view + field schema). These are developer-authored
filesystem packages under
modules/Content/components/{key}/; you cannot create executable components from the UI. The bundled set includes Hero, Feature Grid, Module Grid, Stats Strip, CTA Banner, FAQ, Testimonial, Rich Text, Image + Text, Blog List, Community Preview and Tools Preview. - Reusable component instances — saved configurations (props) of a definition that you can reuse across pages (e.g. a "Book a Demo" CTA). These live in CMS → Components → Reusable Components.
In the editor's builder you:
- Add a section and pick a component type from the picker.
- Fill in its fields (text, rich text, URLs, media, selects, repeaters...). Each field type is validated and sanitized on save.
- Reorder, duplicate, hide/show sections.
- Insert a reusable component, or detach a copy to edit it on one page only.
- Preview the page through the same public renderer.
Rich-text fields accept only an allowlisted set of tags (p, strong, em,
ul, ol, li, a, h2–h4, blockquote, br) and attributes; scripts,
inline styles, event handlers and javascript: URLs are stripped automatically.
Component versions, upgrade, rollback
Each definition is versioned per-component (Tools-Hub style). In CMS → Components → Library each row shows the current version, a Pinned badge if an operator rolled it back, and an Update available badge.
- Sync (
content_components_sync) — re-reads the filesystem manifests and upserts the definitions + version history. (CLI:php erpat content:components:sync.) - Upgrade — re-syncs and clears any pin so the component renders its newest version.
- Rollback — pins the definition to a prior recorded version. Rollback never deletes forward history.
6. Media
CMS → Media Library lets you upload images (JPEG/PNG/WebP/GIF, ≤ 10 MB by default), edit title / alt text / caption, and delete. Uploads are validated by server-side MIME inspection (not just the extension), get sanitized filenames and a safe storage path, and image variants (thumbnail/medium/large/og) are generated for public pages. Always provide alt text for accessibility and SEO. The featured/OG image picker searches this library.
7. Taxonomy
CMS → Categories and CMS → Tags are modal CRUD screens (gated by
content_taxonomy). Each has a name, auto/overridable slug (reserved + unique),
description and SEO fields. Categories support a parent. Public archives live at
/category/{slug} and /tag/{slug} and list a category's/tag's published posts.
8. The content timeline + rollback
Every save and lifecycle change appends an immutable revision snapshot
(cms_revisions). In the editor, the Timeline panel lists revisions (created,
edited, published, scheduled, rolled back, ...). You can:
- Diff two revisions (field/block-level).
- Roll back to a prior revision (requires
content_rollback).
Rollback is non-destructive and forward-only: it writes a new revision restoring the older state rather than rewriting history, and it does not change the live page — it produces a draft you re-publish. The publish cron and audit log record every transition.
9. Preview links
For drafts, scheduled items and specific revisions, use Preview to mint a
signed, expiring, read-only link (/cms/preview/{token}). Preview tokens:
- are tied to the requesting user + content (and optional revision),
- expire after
cms.preview_token_minutes(default 60), - render through the same public renderer as live pages, and
- always emit
robots: noindex, nofollowso a previewed draft is never indexable.
10. SEO panel + analyzer
The editor's SEO panel edits the meta satellite: SEO title, meta description, canonical URL, Open Graph (title/description/image), Twitter card, robots directive, focus keyword, schema type, and sitemap inclusion/priority/change-frequency.
CMS → SEO runs the analyzer (seo_analyze), which scores content against a
checklist (title present/length, meta description, slug readability, H1
uniqueness, images have alt text, canonical present, noindex-on-published warning,
word count, internal links, etc.) and stores a 0–100 SEO score:
90–100 Good
70–89 Needs Review
0–69 Needs Work
The SEO score is an internal checklist only — it does not guarantee search ranking. The publish-warning threshold (
cms.seo_score_publish_warning, default 70) drives the dashboard "SEO needs review" count.
The public renderer emits correct <head> SEO (title, description, canonical, OG,
Twitter, structured data) on first paint, and CMS → SEO also exposes the XML
sitemap and RSS/JSON feeds (served at /sitemap-cms.xml, /feed-cms.xml,
/feed-cms.json). The sitemap excludes drafts, private and noindex content.
11. Comments moderation, AkbAI + bans
CMS → Comments is the moderation queue. Each comment carries both a workflow
status (pending/approved/rejected/spam/hidden/deleted) and an AkbAI verdict badge
(ai_severity clean/flag/block, ai_score, ai_provider). The public-visibility
invariant holds throughout: a comment is shown publicly only when it is
approved on both axes.
From the queue you can approve, reject, hide, mark spam, or delete (gated by
content_comments_moderate). Overriding an AkbAI verdict requires the extra
content_comments_override_ai permission.
AkbAI comment safety (off by default)
When cms.akbai_comment_scan_enabled is on, every submitted comment is scanned by
the AkbAI Hermes classifier before it can become public. Key properties:
- Prompt-injection safe. The comment is sent as data only (a
role:usermessage) behind a fixedrole:systemclassifier prompt; a comment can never rewrite the classifier's task, and injection attempts raise the risk score. - Never blocks submission. If AkbAI is down/slow/unconfigured, the scan degrades to a deterministic local heuristic, and ultimately the comment is simply held as pending for the human queue — submission never fails.
- Moderation mode (
cms.comment_moderation_mode):advisory(default — never hard-blocks; suspicious comments are flagged/held),strict(clearly prohibited content is blocked), oroff(no AI; everything queues for humans). - An optional AI auto-reply (
cms.akbai_comment_reply_enabled, off) is a separate constrained call given only a topic summary (never the raw comment), and the generated reply is itself re-scanned before posting.
Held comments are also picked up out-of-band by the content_scan_comments cron
job (every 2 minutes) when inline scanning is off or AkbAI was unavailable.
Temporary bans
CMS → User Bans lists active bans. Repeat offenders (configurable via
cms.comment_ban_threshold) can be temporarily banned for commenting and/or
reacting. Bans:
- key on the logged-in
user_idor, for guests, an HMAC visitor hash (never a bare IP), with a default duration ofcms.comment_ban_default_hours(24h), clamped tocms.comment_ban_max_days(30d), - block new comments/reactions on web and on the End-User API (with a clear "banned" message), and
- expire automatically via the
content_expire_banscron (hourly); you can also lift a ban manually (content_bans).
12. Route assignments (homepage / Community override)
CMS → Route Assignments lets you replace the live homepage (/) or a module
landing (e.g. /community) with a published CMS page — without touching the web
server or system controllers. This is the highest-impact action in the CMS, so it
is deliberately a multi-step, fail-safe flow.
The safety / confirmation flow
Replacing a live route requires all of the following to be true at once (the resolver re-checks them on every request):
- Master flag
cms.route_overrides_enabledis on. - The route-specific flag is on:
site.home→cms.homepage_override_enabledmodule.*(e.g. Community) →cms.module_overrides_enabled
- The route has an active assignment row.
- That row points at a CMS page that is published right now.
The screen separates two distinct steps so you cannot go live by accident:
- Assign (
content_routes_assign) — point a route at a published page. This does not go live by itself. - Activate (
content_routes_activate) — flip the assignment to active. It only renders if the flags above are on and the page is published.
The list shows, per route, the assigned page, whether it is published, the governing feature flag, and whether the override is live right now. Until you flip the flags and activate, the existing system homepage / Community controller remains the live fallback.
Rollback
Each assignment keeps a snapshot of its previous state. Roll back
(assignment_rollback, requires content_routes_activate) restores the prior
assignment, and Deactivate instantly returns the system default to live. Every
assign / activate / deactivate / rollback is audit-logged
(cms.route_assignment.*).
Preview an assigned homepage before activating it via the route's Preview action — it renders the candidate page (even a draft) with
noindex.
13. Redirects
CMS → Redirects manages source → target rules (301/302/307/308). When a
public CMS slug changes, add a redirect so old links keep working. The public 404
handler consults the redirect engine first: a matching source path 301/302s the
visitor to its target (internal/looping targets are refused) before any 404 view
renders. Each rule tracks hit count and last-hit time.
14. Analytics
CMS → Analytics shows views and engagement (unique views, reactions, comments,
shares, CTA clicks) per content and per day. Public page views are recorded by a
throttled, privacy-preserving event log (no raw IPs — only hashes) and rolled up
nightly by the content_aggregate_analytics_daily cron (00:15 daily) into
cms_analytics_daily.
15. Lighthouse
CMS → SEO → Lighthouse queues asynchronous Lighthouse / PageSpeed Insights
tests for a published URL (content_lighthouse). Lighthouse is off by default
(cms.lighthouse_enabled) and never runs during a save/publish. When enabled and a
PageSpeed API key is configured, the content_run_lighthouse_queue cron (every 10
minutes) processes the queue and stores performance/accessibility/best-practices/SEO
scores plus Core Web Vitals (LCP, INP, CLS). Targets: LCP ≤ 2.5 s, INP ≤ 200 ms,
CLS ≤ 0.1. Without a key, you can record manual results. Failures are stored safely
and surface as "Lighthouse needs review" on the dashboard.
16. Settings / feature flags
CMS → Settings (content_settings) edits the runtime flags in cms_settings.
The safety-relevant defaults:
| Setting | Default | Meaning |
|---|---|---|
cms.enabled |
on | Public CMS surface (/blog, /page/...) |
cms.route_overrides_enabled |
off | Master kill-switch for homepage/module takeover |
cms.homepage_override_enabled |
off | Allow / to be a CMS page |
cms.module_overrides_enabled |
off | Allow /community etc. to be a CMS page |
cms.root_level_pages_enabled |
off | Root-level pretty page slugs |
cms.comments_enabled |
on | Site-wide commenting |
cms.guest_comments_enabled |
off | Allow logged-out visitors to comment |
cms.reactions_enabled |
on | Site-wide reactions |
cms.lighthouse_enabled |
off | Lighthouse queue |
cms.comment_moderation_mode |
advisory | AkbAI mode (advisory/strict/off) |
cms.akbai_comment_scan_enabled |
off | AkbAI pre-publish comment scan |
cms.akbai_comment_reply_enabled |
off | Optional AI auto-reply |
cms.akbai_spam_block_threshold |
0.75 | Block score threshold |
cms.comment_ban_threshold |
3 | Offenses before a ban |
cms.comment_ban_default_hours |
24 | Default ban length |
cms.comment_ban_max_days |
30 | Max ban length (clamp) |
cms.seo_score_publish_warning |
70 | "Needs review" threshold |
cms.preview_token_minutes |
60 | Preview link lifetime |
Changing a setting is audit-logged. The safe-by-default flags above mean a fresh install never changes your live site until you opt in.
17. Where to go next
- Architecture, the component package convention, the rendering pipeline and the
dual API: see
DEVELOPER_GUIDE.md. - The full security posture (escaping, RBAC map, CSRF vs bearer, AkbAI defense,
bans, primary-DB isolation, audit logging): see
SECURITY.md. - What was built, the 23 tables, routes, permissions, testing and rollback: see
../specs/IMPLEMENTATION_SUMMARY.md.