Changelog
Everything that's changed.
Release notes for Contensio, pulled live from GitHub. Click any release to expand its details.
What's New in v2.4.0
Core Changes
License key rotation support
- LicenseService now supports multiple Ed25519 public keys (PUBLIC_KEYS array) for graceful key rotation
- Verifier tries each registered key in order until one succeeds
- Allows deploying a CMS update with both old and new keys, then reissuing licenses at your own pace
License refresh from admin
- WhitelabelController::refreshLicense() calls contensio.com/api/license/refresh to get a fresh key
- One-click refresh from Settings > White Label - no manual copy/paste needed on yearly renewal
- Verifies the new key before storing it
New Plugins Released (v1.0.0)
This release coincides with 7 new plugins pushed to the Contensio organization:
| Plugin | Description |
|---|---|
| plugin-questions | Community Q&A with voting, accepted answers, tags, reputation |
| plugin-glossary | A-Z glossary with multilingual support and auto-linking in post content |
| plugin-live-chat | Built-in live chat widget with polling, canned responses, business hours |
| plugin-tickets | Support ticket system with categories, priorities, agent assignment |
| plugin-forms | Visual form builder with 16 field types, block type embed, CSV export |
| plugin-donations | Stripe + PayPal donations with campaigns, goal thermometer, donor wall, widget |
Total plugins: 40 built out of 67 planned.
Contensio PRO (contensio.com)
License key purchase system built on contensio.com:
- Yearly subscription via Stripe (single domain + agency packs for 10/50 domains)
- Ed25519-signed keys generated server-side
- Auto-renewal via webhook (keys regenerated with fresh expiry on invoice.paid)
- License refresh API endpoint for CMS installations
- 1-month grace period on key expiry
v2.3.0 - Plugin Infrastructure: Shared Tables
Two new core tables and their Eloquent base classes give plugins a standard place to store their data without shipping their own migrations. This eliminates per-plugin tables for simple use cases and ensures clean uninstall (all plugin-owned rows are identified by the plugin column and can be purged in one pass).
New tables
contensio_meta - Polymorphic key-value store. Attaches arbitrary metadata to any model (Post, User, Term, ...). Replaces the pattern of each plugin creating its own {thing}_meta table.
Columns: id, metable_type, metable_id, meta_key, meta_value (LONGTEXT), plugin (nullable), timestamps.
contensio_plugin_entries - Generic items table. Stores any flat or lightly-nested list of plugin-managed items. The data JSON column carries plugin-specific fields so no additional migration is needed when a plugin adds new fields.
Columns: id, plugin, type, title, slug, content (LONGTEXT), data (JSON), status, sort_order, parent_id (nullable), post_id (nullable), user_id (nullable), timestamps.
Plugin ownership in settings
A new nullable plugin column has been added to the settings table. This tracks which plugin owns each settings row, making uninstall cleanup trivial: DELETE FROM settings WHERE plugin = 'vendor/plugin-name'. Existing plugin_options and theme_options rows are backfilled automatically.
New models
Contensio\Models\Meta - Eloquent model for contensio_meta. Includes getValue, setValue, and increment static helpers and forPlugin / forKey scopes.
Contensio\Models\PluginEntry - Base Eloquent model for contensio_plugin_entries. Plugins extend this class and add a global scope to filter by their plugin + type values. Includes forPlugin, ofType, active, forPost, and ordered scopes, plus children() / parent() self-referential relationships.
New files
database/migrations/2026_04_20_000001_add_plugin_column_to_settings_table.phpdatabase/migrations/2026_04_20_000002_create_contensio_meta_table.phpdatabase/migrations/2026_04_20_000003_create_contensio_plugin_entries_table.phpsrc/Models/Meta.phpsrc/Models/PluginEntry.php
Changed files
composer.json- version bumped to 2.3.0src/ContensioServiceProvider.php- VERSION constant bumped to 2.3.0
Plugin Update System
Contensio now detects and applies plugin updates automatically — no CLI required.
New features
- Automatic update detection — checks enabled plugins daily in the background via the
contensio:check-plugin-updatesscheduled command - GitHub-based updates — plugins with a
repositoryfield inplugin.jsonare checked against the GitHub releases API; ZIP download URL is assembled automatically - Custom update server — plugins with an
update_urlfield receive a POST with the current version and consume a{ version, download_url, changelog_url }response contensio/plugin-update-infofilter — paid and license-gated plugins self-report update availability via a filter callback; full control over the license check and download URL generation- One-click update in the admin Plugins panel — amber update notice with a changelog link and an Update button that downloads, extracts, and migrates in one step
contensio:check-plugin-updates --force— bypass the 24-hour cache for an immediate checkcontensio/admin/content-row-badgesrender hook — new injection point in content and posts listing rows for plugin-injected status badges
New files
src/Services/PluginUpdateChecker.phpsrc/Console/Commands/CheckPluginUpdatesCommand.php
Changed files
src/ContensioServiceProvider.php— register command and daily schedulesrc/Http/Controllers/Admin/PluginController.php— index passes update info to view; new update() actionroutes/web.php— POST /plugins/update routeresources/views/admin/plugins/index.blade.php— update badge and Update button per cardresources/views/admin/content/index.blade.php— content-row-badges hook callresources/views/admin/posts/index.blade.php— same hook call
What's new
Content Approval Workflow
A complete editorial review system built into core — no plugin required.
- Authors submit drafts for review instead of publishing directly
- Two rejection types: soft rejection (revise and resubmit) and hard rejection (permanent)
- Reviewer notes required on rejection — shown in the author email and in the edit screen sidebar
- Email notifications: all reviewers notified on submission; author notified with decision
- Review queue at
/account/reviewswith sidebar badge showing pending count - Dashboard widget — pending items shown for reviewers with one-click approve
- Audit log — append-only
content_review_logtable, every action recorded content.approveandcontent.bypass_reviewpermissions for granular role control- Auto-publish on approval (configurable, on by default)
WorkflowServicestatic helper for plugin authors
Widget System
WidgetArea/WidgetRegistry/WidgetInterface— plugin API for registering widget areas and widgets- Admin screen to manage widget instances per area
- 6 built-in widgets: Latest Posts, Recent Comments, Categories, Tag Cloud, Search Box, Custom HTML
- Widget area support in the default theme
White-label / Branding
- Replace Contensio branding with a custom logo and favicon from the admin panel
WhitelabelServicefor plugins to pull branding at runtime
Frontend Hooks
contensio/frontend/head,contensio/frontend/body-start,contensio/frontend/body-endcontensio/frontend/post-before-content,contensio/frontend/post-meta,contensio/frontend/post-after-contentcontensio/content/bodyfilter — transform rendered post HTML before output
Full changelog: https://github.com/contensio/contensio/compare/v2.0.0...v2.1.0
What's New in v2.0.0
This is a major release. The headline feature is the Hook system -- a full plugin extension API that makes Contensio genuinely extensible for the first time. This release also adds Contact Forms, a built-in Backup system, Version Checker, Logo & Favicon management, and Themed 404 pages.
Hook System -- Plugin Extension API
The core of v2.0.0. Hook is a lightweight, priority-ordered extension registry that lets plugins inject UI, trigger side-effects, and transform data without touching core files. Three hook types:
Render hooks -- inject HTML into core views
// In your plugin's service provider:
use Contensio\Support\Hook;
Hook::add('contensio/admin/dashboard-stats', function () {
$orders = \MyPlugin\Models\Order::count();
return view('my-plugin::dashboard.stats', compact('orders'))->render();
});
Hook::add('contensio/admin/dashboard-quick-actions', function () {
return '<a href="/account/shop/orders" class="...">Orders</a>';
});
Built-in render hook points in v2.0.0:
| Hook name | Location |
|---|---|
contensio/admin/login-after-form |
Below the sign-in form |
contensio/admin/dashboard-quick-actions |
Header action bar, right of "Upload media" |
contensio/admin/dashboard-stats |
Below the built-in stat cards row |
contensio/admin/dashboard-widgets |
Between content panels and activity log |
contensio/admin/dashboard-after |
Below the activity log |
contensio/admin/profile-sections |
Bottom of the profile page (receives $user) |
contensio/admin/settings-cards |
Extra cards in the settings hub grid |
Actions -- fire-and-forget side effects
// Register
Hook::addAction('contensio/content/published', function ($content) {
\MyPlugin\Jobs\NotifySubscribers::dispatch($content);
}, priority: 10);
// Fire (core or plugin)
Hook::doAction('contensio/content/published', $content);
Filters -- transform values through a pipeline
// Register
Hook::addFilter('contensio/content/excerpt', function ($excerpt, $content) {
return strip_tags($excerpt);
}, priority: 10);
// Apply (core or plugin)
$excerpt = Hook::applyFilters('contensio/content/excerpt', $rawExcerpt, $content);
All callbacks run in priority order (lower = first). Errors in individual callbacks are reported and do not stop execution. Core views call {!! Hook::render('hook/name') !!} -- safe to call unconditionally, returns empty string when nothing is registered.
New Features
Contact Form Builder Build and manage contact forms from the admin. Supports text, email, phone, select, textarea, and file upload fields. Each form has its own submissions inbox with read/unread tracking.
Contact Labels Color-coded labels to organize contact messages -- mark messages as read, categorize by topic, or build a simple support triage flow.
Built-in Backup System
- BackupService -- pure PDO SQL dump + ZipArchive, no external dependencies
- Admin UI -- Tools -> Backups: create, download, delete, restore
- Restore flow -- upload a ZIP, review the manifest (site URL, date, CMS version, table/file counts), confirm with your account password before executing
- Artisan commands --
contensio:backup [--no-files]andcontensio:restore {file} [--force]
Version Checker Checks GitHub Releases for the latest version (12 h cache). Shows a dismissible update banner on the admin dashboard when a new version is available.
SiteConfig Support Class
SiteConfig::all() / SiteConfig::get() -- memoized per-request cache for core settings.
Logo & Favicon Management Settings -> General now has a Branding section. Theme developers can use:
site_logo() // URL or empty string
site_favicon() // URL or empty string
site_name()
site_tagline()
The default theme uses these -- logo replaces the site-name text in the header when set; favicon is injected into <head>.
Themed 404 Pages
NotFoundHttpException is caught in ContensioServiceProvider::boot() and rendered with the active theme layout. Admin, API, and install routes are excluded. Falls back to Laravel's default if the database is not ready.
Content API
ContentApiController -- headless endpoint for reading published content.
Term Images Categories and tags now support a featured image field.
Theme Template Resolver
ThemeTemplateResolver resolves the correct theme view for each content type.
Bug Fixes
- 12-digit numeric user codes -- replaced alphanumeric codes with
random_int(100000000000, 999999999999). Digits only, no leading zero. Affects registration, admin user create, and the installer. - Comments container width -- now uses
theme-container(same width as the article) instead of a narrowermax-w-3xlinner box. - Comments text sizes -- removed all
text-sm/text-xsoverrides; comment text matches the rest of the site. - Author profile bio -- full-width standalone paragraph below the avatar/name row, no longer constrained by the flex layout.
- Author page text sizes -- normalized throughout, matching the rest of the theme.
- Default theme favicon -- added
<link rel="icon">viasite_favicon()with the built-in Contensio favicon as fallback.
Upgrade Notes
php artisan migrate
No breaking changes to the public API. Plugin providers and themes are unaffected.
What's new in v1.4.0
Content scheduling
Entries can now be scheduled for future publication from the edit screen. Set the status to Scheduled and pick a date and time — the entry goes live automatically when the clock hits. The contensio:publish-scheduled Artisan command (already registered in the scheduler) handles the transition.
- New
Scheduledoption in the status dropdown across pages, posts, and custom content types - Inline datetime picker appears when Scheduled is selected
- Scheduled status badge (blue) added to all content list screens
resolvePublishedAthelper centralisespublished_atlogic across all store/update paths
Bulk actions
Select multiple entries from any content list and act on them all at once.
- Tick individual rows or use the header checkbox to select all
- Blue action bar appears showing selected count with Publish, Set as Draft, and Delete options
- Delete triggers the existing confirmation dialog before proceeding
- Works across pages, posts, and all custom content types
- Routes:
contensio.account.pages.bulk,contensio.account.posts.bulk,contensio.account.content.bulk
JSON API
A lightweight, read-only HTTP API for fetching published content from any client — mobile apps, static site generators, custom frontends.
GET /api/v1/content/{type} — paginated list
GET /api/v1/content/{type}/{slug} — single entry by slug
- No authentication required — published content only
- Supports
per_page(max 100) andlangquery parameters - Response includes id, status, timestamps, author, title, slug, excerpt, meta fields, featured image variants (url / thumbnail / medium), and taxonomy terms
featured_imageisnullwhen unset; variant URLs fall back to the original if not yet generated- 404 JSON response for unknown type or missing slug
- Route names:
contensio.api.content.index,contensio.api.content.show
Global admin search
A search box is now built into the admin top navigation bar.
- Type at least 2 characters — results appear on a dedicated results page
- Content section: matches title and excerpt across all content types; shows type label, status badge, links to the edit screen
- Media section: matches original file name; shows thumbnail for images
- Available at
GET /{admin-prefix}/search?q= - Route name:
contensio.account.search
Already shipped (confirmed complete, no changes needed)
- Menu builder —
contensio.account.menus.*routes, full drag-and-drop UI - RSS feeds — auto-generated at
/feed/rss.xmland per-type - Audit log — admin activity log at
contensio.account.audit-log.index
What's new in v1.3.0
New features
- Reading settings — configure homepage display (latest posts or a static page) and posts-per-page from Settings → Reading
- RSS feed — subscribe to published posts via
/feed(RSS 2.0) - Scheduled posts — content set to a future
published_atdate is auto-published every minute via the built-in scheduler; no cron setup needed beyondphp artisan schedule:run - Custom fields in the editor — post/page edit form gains a General / Custom Fields tab when field groups are attached to the content type; fields are rendered full-width per section, no more collapsed sidebar cards
- Custom fields on the frontend — published post pages now show custom field values below the content, grouped by section heading
- Taxonomy term archives — each taxonomy term gets a public archive page at
/{taxonomy-slug}/{term-slug}; taxonomy term URLs are also included in the XML sitemap - Search — full-text search across post and page titles, excerpts, and body content at
/search?q=… - Media bulk delete — check multiple files in the media library and delete them in one action; image dimensions (W×H) are now displayed in the grid
- Comment moderation email — when a comment requires approval the site admin receives an email with a direct link to the moderation queue
Bug fixes
- Custom fields were not rendering on the frontend post page due to MorphToMany eager-loading using closure syntax (switched to string syntax, confirmed working)
- Custom fields showed the field group name as the section heading instead of the field's section attribute
- Theme views in
packages/themes/take priority over built-in views — edits to the wrong copy had no effect; all theme changes now applied to the correct local copy
What's new
Comments
- Frontend comment form for guests and authenticated users
- Comment display with real avatars and links to author profiles
- Enable/disable comments per content item from the editor
- Admin comments management: paginated list, approve/reject/delete, bulk actions
User registration
- Custom registration form with username field
- Live sanitization: strips non-
[a-z0-9_]characters, forces lowercase - Support for require-approval flow and default role assignment
Author profiles
- Public
/author/{code}pages showing avatar, name, member since, bio, and recent posts
Users & Registration settings
12 new admin settings at /account/settings/users:
- Disable registration
- Require admin approval for new accounts
- Default registration role
- Disable email verification (with backfill for existing unverified users)
- Allow email / bio / avatar changes
- Allow account self-deletion
- Username change cooldown (configurable in days)
- Inactivity logout (configurable in days)
- Max concurrent sessions per user
Session & activity tracking
TrackUserActivitymiddleware enforces inactivity logout and max-sessions limit- Oldest sessions are evicted automatically when the per-user cap is exceeded
Profile page
- Fields (email, bio, avatar, username) shown/editable based on admin settings
- Username cooldown end date shown when change is temporarily blocked
- Account self-deletion card (with password confirmation and confirmation modal)
UI
- Alpine.js pill toggles replace all checkbox inputs across settings pages
Permissions
comments.managepermission added to installer seed and default admin role
Migrations
Four new migrations included — run php artisan migrate after updating:
add_bio_to_users_tableadd_username_to_users_tableadd_tracking_to_users_table(username_changed_at,last_active_at)create_user_sessions_table
What's New
This is the first stable release of Contensio. It includes a complete web-based installer, image processing pipeline, and a round of admin UI improvements built on top of the RC releases.
✨ New Features
Web-Based Installer
- Multi-step installation wizard covering server requirements, database connection, site information, and admin account creation
- No manual
.envediting required — the installer writes all configuration automatically
Image Variant Processing
- Uploaded images are automatically processed into configurable size variants (thumbnail, small, medium, large, OG, square) as a background queue job
- New
MediaVariantmodel andProcessMediaVariantsjob - New migrations:
create_media_variants_table,add_avatar_path_to_users_table,fix_role_permissions_primary_key
Featured Images
- Featured image support on post, page, and custom content type edit pages
- Media picker sidebar card — select from library or upload; shows a live preview with remove button
- Featured image thumbnail shown in the posts listing table
Roles & Permissions Management
- Full RBAC management UI: create custom roles, edit permissions via a grouped matrix, delete unused roles
- Moved from the sidebar into the Configuration section as a card
- Super Admin role protected from editing (wildcard
*permission never shown in UI; direct-URL edit blocked)
Admin UI Improvements
- Posts listing — featured image thumbnail, author column with avatar, full name (linked to user profile), and email
- Add Block — dropdown replaced with a modal showing block type cards with icons and descriptions
- Preview button on content edit — single link for single-language sites, per-language dropdown for multilingual
- Content editor sidebar — 30 rem width on displays wider than 1400 px
- OG default image on SEO settings now uses the media picker instead of a plain URL text field
- Reset to defaults button on image sizes in the content type form
Infrastructure
- Service provider renamed:
CmsServiceProvider→ContensioServiceProvider - Config file renamed:
config/cms.php→config/contensio.php - User avatar upload on the profile page with automatic thumbnail generation
🐛 Bug Fixes
- Critical — Save Changes deleted the post: a nested
<form>inside the main content form violated the HTML spec; the browser closed the outer form early, causing the submit button to submit the delete form instead - Tagify empty
[]dropdown on focus:enabled: 0causes Tagify to auto-open on focus regardless of whitelist content; changed toenabled: 1 - Autosave "0 seconds ago" always showed on page load: the form snapshot was taken before Alpine and Tagify finished initialising, making every clean page load look dirty; fixed with a 600 ms delayed snapshot and a
snapshotReadyguard - Broken sidebar links for Menus and Import/Export: route names used the old
cms.admin.*prefix instead ofcontensio.account.* - Super Admin editable via direct URL: the index page hid the Edit button but
GET /roles/{id}/editremained accessible; guard added inRoleController::edit()andupdate() fieldsBuilder—openNewnot defined:this.blank()was called during object literal construction wherethisrefers towindow, causing the Alpine component to fail silently; moved toinit()- Duplicate tags on every post save: when a tag was typed manually (without selecting from the Tagify dropdown),
syncContent()always created a new term; now checks for an existing term by name before creating - Duplicate terms in taxonomy management: no uniqueness check on slug or name;
TermController::store()andupdate()now validate both within the same taxonomy - Avatar showing initials instead of uploaded image on the users list: incorrect thumbnail path computation
hasPermission()called as an undefined global function: corrected toauth()->user()?->hasPermission()/logineaten by/{slug}catch-all route: Fortify auth routes now registered before the public catch-all
⚙️ Upgrade Notes
- Run
php artisan migrateto apply the three new migrations - If you published the config, rename
config/cms.phptoconfig/contensio.phpand update the key fromcmstocontensio - Update any references to
CmsServiceProvider→ContensioServiceProvider
Second release candidate — the three items rc.1 promised as the path to stable are now in.
Activity log — the viewer now shows real events
rc.1 shipped the read-only audit trail at /admin/activity-log with empty shelves. This release adds the writers: a new Contensio\Cms\Support\Activity helper emits from every significant admin action.
- Content: created, updated, published, deleted — pages, posts, and custom types
- Users: created, updated, deleted (with per-field change diff on updates)
- Roles: created, updated, deleted
- Plugins: plugin_enabled, plugin_disabled (with version info)
- Authentication: login, logout
All writes are silent-on-failure — logging can never break the host action. The old/new change diff on updated only records fields that actually changed, so the properties column stays tight.
Media Library picker
The custom-field media type in rc.1 was a plain text input asking for "URL or ID". That's gone.
- Modal picker included once in the admin layout, fired with
$dispatch('cms:media-pick', { inputName, multiple, accept }) - Search + MIME filter (e.g.
accept: 'image/'to restrict to images) - Upload from inside the modal — files appear in the grid immediately, preselected
- Thumbnail strip auto-renders for selected media below the field label
- Two JSON endpoints:
/admin/media/pickand/admin/media/pick/upload
media.config.multiple fields now JSON-encode on save, returning an array from $content->field('gallery') in themes.
Rich-text editor (Tiptap)
The rich-text custom-field type in rc.1 was a plain textarea. This release swaps it for a real editor.
- Tiptap 2.11 loaded via an ESM import map from
esm.sh(pattern matches the existing Tailwind + Alpine CDN loading; no bundler added to core) - Toolbar: bold, italic, strike, code, H2/H3, bullet and ordered lists, blockquote, code block, link (with prompt), horizontal rule, undo/redo
- Per-textarea opt-in:
x-init="window.initRTE($el)"— so only rich-text fields get the editor, plain textareas stay plain - Server-side still stores HTML in the same column; no schema change
Pragmatic notes
- No breaking changes from
rc.1. No migrations.composer updateand you're good. - Path to
1.0.0stable: needs a round of hands-on QA and any small UX polish that shakes out. No features intentionally left on the list. - Activity log is now load-bearing; removing it is a breaking change from this point on.
Next release, barring surprises, is 1.0.0 stable.
First release candidate of the 1.0 line. Contensio jumps from 0.1 straight to 1.0-rc — the core now covers enough ground to credibly carry a stable tag: multilingual content, custom types, custom fields, block editor, plugins & themes, roles & permissions, SEO, security, and the auxiliary tools you'd otherwise install as plugins.
Headline: Custom Fields
ACF-class custom fields, built into the core — no license, no plugin.
- Reusable field groups attach to one or many content types through a polymorphic pivot (v1 attaches to content types; taxonomies/users/media in v1.x). Define a group once, reuse it across Pages, Posts, Products, Devices.
- Two-level hierarchy — groups contain fields; "sections" are a simple string attribute on a field (rendered as subtle dividers in the edit form). Simpler than ACF's tab/accordion pseudo-fields; same UX outcome.
- 10 field types: text, textarea, rich-text, number, boolean, date, select, multi-select, media, URL. Each with type-specific config (max length, min/max/step/suffix, option lists, media mime filter, with-time for date).
- Native multilingual support — mark a field as translatable and it stores per-language values automatically. No second plugin required.
- Anti-tamper save — submitted field IDs are whitelisted against groups actually attached to the content's type.
- Theme helper:
$content->field('price')resolves via attached groups, JSON-decodes multi-value types, caches per request.
Also in this RC
- Email / SMTP settings — configure mailer, host, port, encryption, credentials and sender from the admin. Password encrypted at rest. "Send test email" button. Overlays
config('mail.*')at boot. - Redirects admin — CRUD for URL redirects with 301/302, search, hit counters, last-hit timestamps. Middleware prepended to the web group so admin intent wins over existing routes.
- Activity log viewer — read-only audit trail at
/admin/activity-logwith filters by user, action, subject type, date range. - Hook system + plugin navigation — plugins inject UI into core views (
login.after_form,profile.sections,settings.hub_cards) and register sidebar entries withroot,toolsorappearanceplacement. A plugin can register multiple entries. - Plugins: auto-migrations on enable — no CLI step required.
- Sidebar restructure — group labels removed; Appearance (Themes + Menus) and Tools (Import/Export) are collapsible dropdowns that auto-open on the active route.
- Import / Export — single JSON file for pages, posts, menus, translations, blocks and meta, with skip-or-overwrite conflict handling.
- Social login extracted to
contensio/plugin-social-connect— first official plugin and reference implementation for the new Hook + nav APIs.
Breaking change from 0.1.0-rc.2
The custom-fields schema was reset: content_type_fields and content_type_field_translations are replaced by field_groups, fields, field_group_attachments, and field_translations. Anyone on rc.2 needs to run php artisan migrate:fresh. No production users exist yet, so this is safe — the break happens before 1.0 on purpose.
Path to 1.0.0 stable
- Wire activity-log emitters across core controllers (the viewer exists; the writers land in the next pre-release)
- Replace the Media field plain input with a Media Library picker
- Swap the rich-text textarea for a real editor
Repeater, relationship fields, conditional-visibility UI and drag-reorder for the fields builder are deferred to 1.1.
Second release candidate. Plugin system matures with view hooks, multi-placement sidebar navigation and an auto-migrations step on enable. Sidebar is restructured around collapsible dropdowns to stay clean as plugins pile on. Ships with core Import/Export.
Plugin system
- Hook registry — plugins inject UI into core views at three extension points (
login.after_form,profile.sections,settings.hub_cards). One broken plugin can no longer crash the page. - Sidebar navigation — plugins declare a
menublock inplugin.jsonwith one of three placements:root,tools,appearance. A single plugin may register multiple entries (object or array form). - Auto-migrations on enable — enabling a plugin from the admin now runs its migrations; no CLI step required. Failures are non-fatal.
Admin UI
- Sidebar group labels removed in favor of a flat list. Appearance (Themes / Menus) and Tools (Import / Export) are now collapsible dropdowns that auto-open on the active route.
- New Import / Export tool under Tools — single JSON file covering pages, posts, menus, languages (translations, blocks, meta included), with skip-or-overwrite conflict handling on import.
Internal
- Social login removed from core and rebuilt as a separate plugin (
contensio/social-connect), serving as the reference implementation for the new Hook + nav systems.
Release candidate — 0.1.0-rc.1
Contensio is feature-complete for the 0.1 cycle. This is the release candidate — it would ship as 0.1.0 stable unless bugs surface.
The last piece — two-factor authentication — brings the admin into parity with any modern CMS.
New in this release: 2FA
Full TOTP + recovery-code flow, powered by Laravel Fortify under the hood.
For users:
- Go to Profile → Two-factor authentication, click Enable 2FA
- Scan the QR with any authenticator app (Google Authenticator, 1Password, Authy, etc.) — or type the setup key manually
- Enter the 6-digit code to confirm
- Save the 10 recovery codes somewhere safe
- On next login: after entering email + password, you'll be asked for a 6-digit code
- Lost your authenticator? Swap to the recovery code form on the challenge screen — one code, used once
Admin profile page (/admin/profile):
- Update name / email
- Change password (requires current password)
- Manage 2FA:
- Disabled → Enable button (you'll re-confirm your password first)
- Pending → QR code + setup key disclosure + 6-digit confirmation input + Cancel
- Enabled → Recovery codes grid + Regenerate + Disable
Login challenge screen:
- Clean card with
••••••placeholder input, autocomplete="one-time-code" for iOS autofill - Toggle to "Use a recovery code instead" without a page reload
- Cancel-and-sign-out escape hatch
Login controller integrates Fortify's 2FA flow: credentials validated first, then redirect to challenge if 2FA is confirmed on the account.
Full feature set at rc.1
Content
- Block-based editor with 7 core block types (richtext, heading, quote, code, image, divider, spacer)
- Custom content types defined from the admin UI
- Taxonomies + terms, hierarchical or flat
- Content autosave (2.5s debounced + 30s safety + restore banner)
- Multi-language: every field translatable
Appearance
- Themes: admin-installable (ZIP upload) or Composer, one-click activate, per-theme settings schema
- Menus: drag-and-drop builder, auto-save on reorder, theme-declared locations
- Media library with translations
Extensibility
- Plugins: admin-installable or Composer, enable/disable, per-plugin settings schema, declare their own permissions and roles
Authentication
- Login + logout (custom controller with is_active check)
- Password reset with branded emails
- Email verification (opt-in via
MustVerifyEmail) - Password confirmation gate for sensitive actions
- Two-factor authentication (TOTP + recovery codes) ← new
Users & roles
- 4 predefined roles (Administrator, Editor, Author, Viewer)
- 30+ core permissions, wildcard + per-content-type scoping
- Custom roles from admin, plugins can declare their own
SEO
- sitemap.xml + robots.txt
- Open Graph + Twitter Cards in default theme
- Global noindex toggle, custom robots override, Google verification
Ops
- Dashboard with quick stats, recent published, drafts, activity log
- AGPL-3.0-or-later
- On Packagist with auto-sync on push
Install
composer require contensio/contensio:^0.1@rc
php artisan migrate
Or with the scaffold:
git clone https://github.com/contensio/project my-site
cd my-site
composer install
cp .env.example .env
php artisan key:generate
php artisan migrate
What happens next
If rc.1 proves stable for real-world use → 0.1.0 ships with the same code.
If bugs surface → fixes land as rc.2, rc.3 until clean.
After 0.1.0, future minor versions (0.2.x, 0.3.x) will add new features. Breaking API changes stay allowed until 1.0.0.
License
AGPL-3.0-or-later. Copyright (c) 2026 Iosif Gabriel Chimilevschi. Contensio is operated by Host Server SRL.
Authentication — 0.1.0-alpha.6
The missing piece that was blocking any real production deployment: password reset and email verification. Powered by Laravel Fortify under the hood, with Contensio shipping its own views and branded reset emails.
What's new
- Forgot password flow — "Forgot password?" link now appears on the sign-in screen. Users enter their email, receive a reset link, pick a new password, and are automatically signed in.
- Email verification (opt-in) — full flow with "check your inbox" screen, resend button, and verification link handling. Activate by making your
Usermodel implementMustVerifyEmailand gating routes with theverifiedmiddleware. - Password confirmation gate — reusable view for sensitive admin actions (future use: "confirm your password to delete this user," etc.).
- Branded reset email — subject line and body pulled from
config('cms.name'), clear copy about expiry + reassurance for users who didn't request the reset.
New routes
| Method | Path | Purpose |
|---|---|---|
| GET | /forgot-password |
Request-reset form |
| POST | /forgot-password |
Send the reset link |
| GET | /reset-password/{token} |
New-password form |
| POST | /reset-password |
Update the password |
| GET | /email/verify |
Verify-your-email notice |
| GET | /email/verify/{id}/{hash} |
Verification link handler |
| POST | /email/verification-notification |
Resend verification email |
| GET / POST | /user/confirm-password |
Password re-entry gate |
What's intentionally disabled
Fortify's login and register features are off. Contensio keeps its own login controller (with is_active check + role-aware redirect), and registration is admin-managed — users are created from the admin Users page, not via public self-service.
Setup — what admins need to configure
Password reset needs outgoing mail. In .env:
MAIL_MAILER=smtp
MAIL_HOST=smtp.example.com
MAIL_PORT=587
MAIL_USERNAME=...
MAIL_PASSWORD=...
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="no-reply@your-site.com"
MAIL_FROM_NAME="${APP_NAME}"
For local development, set MAIL_MAILER=log to see reset links in storage/logs/laravel.log.
Two-factor authentication
Fortify's 2FA backend (google2fa + recovery codes) is installed as a dependency — just not yet wired into the admin UI. Planned for a future alpha (roughly a day's work; the hard part is already done).
What's still ahead before beta
- Submit
contensio/contensioto Packagist → installable viacomposer require - Real public documentation site
- Frontend search
Once Packagist + docs are live → 0.1.0-beta.1.
License
AGPL-3.0-or-later. Copyright (c) 2026 Iosif Gabriel Chimilevschi. Contensio is operated by Host Server SRL.
Content autosave + SEO essentials — 0.1.0-alpha.5
Two features that meaningfully change how Contensio feels for real use.
Content autosave
Data loss in an editor is the single worst UX failure. Contensio now prevents it.
- Form state is saved 2.5 seconds after any edit, plus a safety save every 30 seconds regardless of activity
navigator.sendBeacon()onbeforeunloadsaves on navigation / tab close, best-effort- Per-user, per-content autosave — one in-flight draft per editor (upsert on
content_id, user_id) - Status indicator (bottom-right): Saving… → Draft saved → fades out. If something goes wrong: Couldn't autosave in red.
- On reopening a content item with newer autosaved work, a restore banner appears at the top: "You have unsaved changes from 5 minutes ago — Restore / Discard."
- Restore walks the saved form state and refills every field (including Alpine
x-modelbindings — events are dispatched so reactive UI catches up) - Autosaves are cleared automatically on every real save, so the banner only appears when there's actually recovered work to offer
Works across Pages, Posts, and any custom content type — all three use the same edit view.
SEO essentials
New /sitemap.xml and /robots.txt routes shipped in the core — no plugin needed:
- Sitemap lists home, blog, and every published page/post slug with accurate
<lastmod>from content timestamps. Respects the global noindex toggle (returns an empty sitemap when noindexed). - robots.txt allows everything except
/adminby default, points to the sitemap. Admins can override the body entirely via a textarea in admin settings. - Routes are outside the
webmiddleware group — zero session/CSRF overhead for crawlers.
Default theme layout now emits full SEO metadata:
<meta name="robots" content="index, follow"> <!-- or noindex, nofollow -->
<meta name="google-site-verification" content="…"> <!-- when set -->
<meta property="og:type" content="website">
<meta property="og:site_name" content="…">
<meta property="og:title" content="…">
<meta property="og:description" content="…">
<meta property="og:url" content="…">
<meta property="og:image" content="…">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="…">
<meta name="twitter:description" content="…">
<meta name="twitter:image" content="…">
Post and Page views expose their featured image as the OG image automatically. When no featured image exists, the site-wide default OG image is used. Posts set og:type=article for richer social previews.
New admin: Settings → SEO
Four clean sections:
- Discourage search engines — single checkbox, applies site-wide (great for staging)
- Default OG image URL — shown when a page/post doesn't have its own
- Google Search Console verification code — paste once, it's rendered site-wide
- robots.txt override — optional textarea for admins who want full control
Quick links at the bottom open the generated sitemap and robots.txt in new tabs so you can verify the output after changing settings.
What's next
- Submit to Packagist → becomes installable via Composer by anyone
- Real documentation site
- Frontend search
- Scheduled publishing
- Then:
0.1.0-beta.1(feature-freeze, invite testers)
License
AGPL-3.0-or-later. Copyright (c) 2026 Iosif Gabriel Chimilevschi. Contensio is operated by Host Server SRL.
Users, Roles, Permissions — 0.1.0-alpha.4
Complete access-control foundation. The admin panel now has real multi-user, multi-role support — non-technical users can manage who can do what entirely through the UI, and plugins can extend the system with their own permissions and roles.
What's new
Four predefined core roles
| Role | Summary |
|---|---|
| Administrator | Full access (* permission) — manage users, themes, plugins, settings, everything |
| Editor | All content operations on anyone's content — no site management |
| Author | Create / edit / publish own content only — no access to others' work |
| Viewer | Read-only admin — useful for clients and stakeholders |
Core roles can't be deleted, but their permissions are fully editable.
Permission system
- 30+ core permissions, named
{module}.{action}.{scope}— e.g.content.update.any,media.upload,themes.install,users.delete - Special
*permission grants everything (granted to Administrator) - Per-content-type scoping via
role_permissions.content_type_id(nullable) — lets you build roles that operate on a subset of content types (e.g. "Shop Manager can manage Products, not Pages or Posts") - Seeded automatically on first boot — idempotent, safe to re-run
Plugin extensibility — first-class support
Plugins declare permissions + roles in plugin.json:
{
"name": "acme/shop",
"permissions": {
"shop.orders.view": "View orders",
"shop.orders.refund": "Issue refunds",
"shop.products.manage": "Create / edit / delete products"
},
"roles": {
"shop_manager": {
"label": "Shop Manager",
"permissions": ["shop.*", "media.upload", "content.view"]
}
}
}
On plugin enable: permissions and roles auto-sync into the DB. On uninstall: cleanup happens automatically. Administrator gets every new permission for free (via *).
Programmatic API (AccessControl::syncPermissions(), AccessControl::syncRoles()) available for dynamic cases.
Admin UI
/admin/users— list, create, edit (inline password change), delete. Guarded against removing the last Administrator or deleting your own account./admin/roles— list with source badges (Core / Plugin / Custom) and user counts; create custom roles with a grouped permission matrix; edit name/description/permissions on any role; delete custom roles (blocked when assigned to users).- Permission matrix groups perms by module with Bootstrap Icons + plugin badges so admins can tell at a glance where each permission came from.
- Sidebar links permission-gated (
users.view,roles.manage) — each user sees only what they can actually access.
Enforcement
- New
cms.permissionmiddleware alias — apply to any route with->middleware('cms.permission:shop.orders.view') $user->hasPermission('content.update.any', $typeId)— type-scoped checks built in@if(auth()->user()->hasPermission('...'))works in Blade for menu/button gating
License
AGPL-3.0-or-later. Copyright (c) 2026 Iosif Gabriel Chimilevschi. Contensio is operated by Host Server SRL.
Dashboard, screenshots, plugin settings — 0.1.0-alpha.3
Contensio — the open content platform for Laravel.
This release polishes the first-login experience and completes the symmetry between themes and plugins.
Dashboard widgets
Replaced the basic stats with a full dashboard:
- Personalized greeting — adapts to time of day
- Quick actions bar — New page / New post / Upload media, one click
- 5 stat cards — Pages, Posts, Media, Users, Drafts. Each links to its admin section. Drafts card turns amber when > 0.
- Recently published widget — 6 most recent live items, clickable to editor, with type badge + author + relative date
- Drafts in progress widget — 5 most recently touched drafts (pick up where you left off)
- Activity log — 8 most recent entries with user avatars
- Helpful empty states with action suggestions
Link resolution is content-type-aware: clicking a recent page/post/custom-type item opens its correct editor.
Theme screenshots
Themes can now ship their own preview image, rendered in the admin Themes grid:
- New route
GET /admin/themes/screenshot?theme=vendor/name - Serves from the theme's
public/directory, no symlink required - Tries
screenshot.svg,.png,.jpg,.webpin order - Gracefully falls back to the generic icon if no screenshot is shipped
Cache-Control: public, max-age=3600for fast repeat loads
The default theme (contensio/default) now ships with a hand-crafted SVG preview.
Plugin settings
Plugins can now declare settings in plugin.json — exactly like themes do in theme.json:
{
"name": "acme/seo",
"provider": "Acme\Seo\ServiceProvider",
"autoload": { "psr-4": { "Acme\Seo\\": "src/" } },
"settings": {
"sections": [{
"key": "general",
"label": "General",
"icon": "bi-globe",
"fields": [{
"key": "google_verification",
"type": "text",
"label": "Google site verification code",
"default": ""
}]
}]
}
}
- New
PluginOptionsservice mirrorsThemeOptionsexactly - New
plugin_option($plugin, $key, $default)helper for plugin code - New admin route
GET /plugins/settings?plugin=vendor/name— tabbed settings form - A Settings button appears on plugin cards when enabled + declares schema
- Reuses the existing field renderer — all 9 field types work (text, textarea, number, range, color, select, radio, checkbox, image)
- Per-plugin storage in
settingstable as JSON — keys preserved even if schema changes
What's still ahead
- Users admin — currently a stub, next priority
- Content autosave — biggest single UX win
- Auto-run migrations on plugin enable
- Plugin-declared admin menu items (let plugins add sidebar links)
- Submit to Packagist when ready for v1.0
License
AGPL-3.0-or-later. Copyright (c) 2026 Iosif Gabriel Chimilevschi. Contensio is operated by Host Server SRL.
🔌 Plugins admin — 0.1.0-alpha.2
Contensio now has a full plugin system. Non-technical users can install, enable, and disable plugins from the admin panel — same UX as WordPress.
What's new
- PluginRegistry service — discovers plugins from both paths:
packages/plugins/vendor/name/plugin.json(ZIP uploads via admin)vendor/composer/installed.jsonwithextra.cms.type: "plugin"(Composer require)
- Admin → Plugins — WordPress-style card grid:
- One-click Enable / Disable (many plugins simultaneously)
- Install Plugin modal — drag-and-drop ZIP upload
- Remove button for local plugins (Composer ones managed via
composer remove) - Clear enabled badge + green highlight for active plugins
- Warning shown if a plugin's manifest lacks a service provider
- Safe provider registration — bad manifests, missing classes, or throwing providers are caught silently so one broken plugin doesn't take down the admin panel
- Enabled state stored in DB (
settings.core.enabled_pluginsas JSON array) — no.envedits, no CLI, toggleable by any admin user - Documentation —
_docs/05-plugin-architecture.mdcovers the full system;_docs/00-how-it-works.mdadded as source material for the public docs site
Both install paths, confirmed working
- Admin upload: user ZIPs a plugin → admin → Install → appears in grid → Enable
- Composer install: developer runs
composer require acme/plugin→ appears in grid → Enable
Both produce identical runtime behavior. The admin UI shows them side-by-side with the only difference being the Remove button (local plugins only).
Plugin manifest example (local)
{
"name": "acme/seo",
"label": "SEO Tools",
"version": "1.0.0",
"provider": "Acme\Seo\SeoServiceProvider",
"autoload": {
"psr-4": { "Acme\Seo\\": "src/" }
}
}
Still ahead
- Plugin settings schema (same pattern as theme settings)
- Auto-run migrations when enabling a plugin
- Plugin-declared admin menu items
- Users admin page (currently a stub)
- Dashboard widgets
License
AGPL-3.0-or-later. Copyright © 2026 Iosif Gabriel Chimilevschi. Contensio is operated by Host Server SRL.
🧱 First alpha — 0.1.0-alpha.1
Contensio — the open content platform for Laravel.
This is the first public alpha. APIs may change. Not recommended for production yet.
What's in this release
- Block-based content editor — reorderable blocks (richtext, heading, quote, code, image, divider, spacer), per-language content, translatable labels
- Themes — admin-installable (ZIP upload), one-click activate, per-theme customization panel (colors, fonts, layout, custom CSS)
- Custom content types — define any content shape from the admin (not just posts/pages)
- Taxonomies & terms — create taxonomies per content type, hierarchical or flat, multi-language
- Menu builder — drag-and-drop reordering (auto-saves), per-language labels, theme-declared locations
- Media library — upload, organize, translate alt/caption
- Multi-language — every content type, term, menu, and theme option is translatable
- Default theme (
contensio/default) — clean, responsive, fully customizable via admin
Known limitations
- Plugin admin UI is a stub
- Users admin UI is a stub
- Not yet published to Packagist — install via git clone + path repository for now
Requirements
- PHP 8.3+
- Laravel 13.x
- MySQL 8 / PostgreSQL 14 / SQLite 3
License
AGPL-3.0-or-later. Copyright © 2026 Iosif Gabriel Chimilevschi. Contensio is operated by Host Server SRL.