Most Laravel CMS projects start clean.
Then you add a blog.
Then a contact form.
Then multilingual support, analytics, cookie consent, newsletter subscriptions, SEO settings, custom pages...
Before long, the "core" knows about everything.
I wanted to build the opposite.
Laravel Addon CMS is an open-source CMS built with Laravel 12 where the core stays intentionally small, product features live inside installable addons, and themes completely own the public-facing website.
Instead of starting every new project by copying the previous Laravel project, the idea is simple:
Build the website by composing reusable features rather than forking an existing codebase.
The Problem I Wanted to Solve
I've seen the same pattern repeatedly in Laravel projects.
A feature starts reusable, but eventually becomes tightly coupled to a specific project.
A blog module gets mixed into the main application.
A contact form gets rewritten.
Theme sections become one-off Blade files.
Analytics code gets scattered around layouts.
Then a bug gets fixed in one project while five older projects continue running the previous version.
The problem isn't necessarily Laravel.
The problem is where we draw the boundaries.
For Laravel Addon CMS, I decided on three major boundaries:
Core
├── Shared CMS infrastructure
│
Addons
├── Product features
│
Themes
└── Website presentation
The core shouldn't need to know what features developers will build in the future.
What the Core Owns
The CMS core handles the things almost every website needs:
- Admin authentication
- Media management
- Pages
- SEO settings
- Database-backed settings
- Theme customization
- Addon installation
- Addon lifecycle management
- File handling
- Shared helpers
- Dynamic addon/theme loading
Everything outside that shared infrastructure can become an addon or part of a theme.
The project currently uses:
Laravel 12
PHP 8.2+
MySQL
Blade
Composer
Vite
Bootstrap
Artisan
The Interesting Part: The Database Decides What Laravel Loads
This is probably the architectural decision I like most about the project.
Normally, Laravel service providers are registered through application configuration.
But if an addon can be installed, enabled, disabled, or removed through the admin panel, I don't want developers manually editing configuration every time.
So after installation, the application reads the active addons from the database.
Conceptually:
Application boots
↓
Check CMS tables
↓
Read active addons
↓
Find addon ServiceProviders
↓
Register providers
↓
Read active theme
↓
Register theme ServiceProvider
Addons live here:
app/
└── Addons/
├── PostCraft/
├── MailMate/
└── ...
Themes live here:
app/
└── Themes/
├── Brixly/
├── YourTheme/
└── ...
This means enabling an addon isn't just changing a boolean that the UI checks.
It changes what Laravel actually boots.
The addon provider can determine:
- Which routes exist
- Which controllers become available
- Which views are registered
- Which sidebar items appear
- Which frontend sections become available
The same idea applies to themes.
Switch the active theme and its provider becomes responsible for registering the public website.
An Addon Is Basically a Laravel Feature Package
I deliberately avoided creating an entirely new framework developers would have to learn.
If you know Laravel, the structure should already feel familiar.
A typical addon can contain:
app/Addons/Blog/
├── BlogServiceProvider.php
├── Activator.php
├── composer.json
├── routes/
├── controllers/
├── models/
├── views/
├── assets/
└── database/
└── migrations/
Each addon owns its feature.
That means routes, controllers, models, migrations and views don't have to leak into the main application.
Scaffolding an Addon
I also wanted addon development to feel similar to normal Laravel development.
The project uses the cmsaddoncommands package to provide Artisan commands for scaffolding.
For example:
# Create the addon
php artisan make:addon Blog
# Create an addon model
php artisan addon:model Blog Post -m
# Create a controller
php artisan addon:controller Blog PostController
# Create an addon migration
php artisan addon:migration Blog create_posts_table
Addon migrations can also be handled independently:
php artisan addon:migrate Blog
php artisan addon:migrate:rollback Blog
This keeps database changes attached to the feature that owns them.
Registering Routes, Views and Admin Navigation
An addon ServiceProvider can look something like this:
<?php
namespace App\Addons\Blog;
use App\Lib\Sidenav;
use Illuminate\Support\ServiceProvider;
class BlogServiceProvider extends ServiceProvider
{
public function boot(): void
{
$this->loadRoutesFrom(
__DIR__.'/routes/cms_blog.php'
);
$this->loadViewsFrom(
__DIR__.'/views',
'Blog'
);
Sidenav::add(
'Content',
'Blog posts',
'admin.blog.index',
'ph ph-article'
);
}
}
Nothing particularly magical is happening here.
And that's intentional.
It's still Laravel.
The CMS mainly provides conventions and lifecycle management around familiar Laravel concepts.
Addon Lifecycle Management
Installable addons create another problem:
What happens to their database structure when they're activated or removed?
Each addon can provide an Activator class with lifecycle hooks for:
activate
deactivate
delete
During activation, the CMS can run migrations belonging specifically to that addon before marking it active.
During deletion, it can:
- Roll back addon migrations.
- Execute the addon delete hook.
- Remove its files.
- Remove its database record.
The lifecycle stays intentionally simple.
Laravel developers already understand migrations and Artisan, so I didn't want another abstraction hiding them.
Addon Metadata and Dependencies
Each addon also contains metadata describing itself.
When distributing an addon, things like its nickname, version and dependencies matter.
For example, an addon may declare:
required_addons
required_system_version
There are a few conventions I try to keep strict:
- Addon nickname should match its folder/provider naming.
- Database changes belong inside the addon.
- Addons should use their own view namespaces.
- Dependencies should be declared before distribution.
- Lifecycle hooks should only handle work migrations can't handle.
These restrictions make addons easier to move between projects.
Themes Are More Than Blade Templates
I didn't want themes to mean:
"Change a CSS file and replace some Blade templates."
A theme should own the actual website presentation.
A theme can contain:
app/Themes/YourTheme/
├── YourThemeServiceProvider.php
├── routes/
├── controllers/
├── Lib/
├── views/
│ ├── layouts/
│ ├── sections/
│ └── slices/
├── assets/
├── config.json
└── screenshot.png
The interesting directory here is:
Lib/
It contains the definitions for configurable website sections.
Building Configurable Theme Sections
Imagine a theme has a hero section.
Instead of hardcoding its content into Blade, the theme can describe the fields that should be editable.
For example:
<?php
namespace App\Themes\YourTheme\Lib;
class HeroSection
{
public static function register($themeConfig): void
{
$themeConfig->addSection([
'key' => 'hero_section',
'pages' => ['home'],
'position' => 1,
'title' => 'Hero Section',
]);
$themeConfig->addField('hero_section', [
'type' => 'text',
'key' => 'heading',
'label' => 'Heading',
'default' => 'Build something useful',
]);
$themeConfig->addField('hero_section', [
'type' => 'image',
'key' => 'image',
'label' => 'Hero Image',
'size' => '1200x800',
'accept' => '.png,.jpg,.jpeg,.webp',
]);
$themeConfig->addField('hero_section', [
'type' => 'url',
'key' => 'button_url',
'label' => 'Button URL',
'attr' => 'href',
'default' => '#',
]);
}
}
Now the CMS understands that the section contains:
Heading → text
Image → image
Button → URL
The admin customizer can generate editing controls from those definitions.
Rendering the Section
The corresponding Blade file might look like:
<section class="hero-section">
<div class="container">
<div class="hero-content">
<h1>
<x-ui.text
section="hero_section"
key="heading"
/>
</h1>
<x-ui.a
class="btn"
section="hero_section"
key="button_url"
>
Learn more
</x-ui.a>
</div>
<x-ui.img
class="img-fluid"
section="hero_section"
key="image"
alt="Hero image"
/>
</div>
</section>
These components resolve the saved theme content while preserving the editing hooks required by the CMS customizer.
So the flow becomes:
Section PHP class
↓
Defines editable fields
↓
Admin customizer
↓
User edits content
↓
Content stored as JSON
↓
Blade UI components
↓
Public website
This gives theme developers control over presentation without requiring site owners to edit Blade.
Repeaters and Dynamic Content
Some sections aren't simple fields.
Think about:
- Testimonials
- Client logos
- Services
- Team members
- Pricing cards
These need repeatable groups.
The theme API therefore supports repeater definitions, while their frontend templates can live under:
views/slices/{section_key}/{repeater_key}
The theme controls how repeated items look, while the CMS controls how their content is edited and stored.
Addons Can Also Push Frontend Sections
This was another important requirement.
Suppose I create a newsletter addon.
The addon shouldn't only provide:
/admin/subscribers
It might also provide a reusable newsletter signup section.
That section could then be inserted into a supported theme page.
This lets feature addons participate in the frontend without owning the entire theme.
For example:
MailMate Addon
│
├── Subscriber management
├── Newsletter logic
└── Newsletter section
↓
Theme page
The feature remains owned by the addon while presentation remains compatible with the theme system.
Why Service Providers Were the Right Boundary
I considered creating a more custom module-loading system.
But Laravel already has a good extension boundary:
Service Providers.
They can register:
- Routes
- Views
- Bindings
- Configuration
- Boot logic
So rather than hiding Laravel, the CMS uses Laravel itself as much as possible.
That's an architectural principle I've tried to maintain throughout the project:
Add conventions around Laravel instead of replacing Laravel concepts.
A developer shouldn't need to learn a private mini-framework just to build a CMS addon.
The Trade-Off: Architecture Got More Attention Than UI
One thing I'm very aware of is that the current UI/UX isn't the strongest part of the project.
Most of the early work went into:
- Addon boundaries
- Dynamic provider registration
- Theme registration
- Migration lifecycle
- Content configuration
- Installation
- Developer tooling
That produced a system I'm happier extending, but it also means the admin interface still needs work.
The customizer, media manager, addon management and theme controls could all be cleaner and more polished.
I don't see that as an architectural problem.
The foundation is there.
The next stage is making that architecture pleasant for non-technical users too.
What I Would Keep
If I rebuilt the project today, I would absolutely keep the separation:
Core → infrastructure
Addons → features
Themes → presentation
I'd also keep the Laravel-native approach.
Service providers, Blade, migrations, route files, Composer metadata and Artisan commands are already familiar to Laravel developers.
That's valuable.
What I Would Improve
There are several things I'd invest more time in next.
1. Admin UI/UX
The functionality exists, but workflows can be more intuitive and visually consistent.
2. Automated Tests
Especially around:
Install addon
Activate addon
Deactivate addon
Delete addon
Run migrations
Rollback migrations
Dependency validation
These lifecycle operations deserve stronger automated coverage.
3. Theme API Documentation
The section system becomes much easier to use once you understand it, but that understanding shouldn't require reading the source.
4. Smaller Demo Data
A smaller default installation would make the architecture easier for new contributors to explore.
5. Contributor Experience
I'd like the path from:
git clone
to:
my first addon
to be extremely short.
It's Open Source
Laravel Addon CMS is released under the MIT License.
I'm especially interested in contributions around:
- UI/UX improvements
- Documentation
- Automated tests
- Accessibility
- Bug fixes
- Validation
- Theme sections
- New themes
- Focused addons
I prefer small, understandable pull requests over huge rewrites.
One addon.
One section.
One admin workflow.
One failing test.
One documentation gap.
Those improvements compound quickly in an extensible system.
Final Thoughts
The biggest lesson from building this project wasn't how to create a CMS.
It was about boundaries.
The difficult part of an extensible system isn't adding features.
It's deciding what should not know about those features.
For Laravel Addon CMS, the answer became:
The core provides infrastructure.
Addons provide capabilities.
Themes provide presentation.
The database decides what is active.
Laravel does the rest.
There is still plenty to improve, especially around UI/UX, tests and documentation.
But I think the architecture is moving in the right direction: a CMS where building the next Laravel website means assembling reusable pieces instead of copying the previous project.
If you're a Laravel developer, I'd be interested to hear how you approach modularity in larger applications.
Do you prefer packages, modules, domains, plugins—or something else?
Project: Laravel Addon CMS
Stack: Laravel 12, PHP, MySQL, Blade, Composer, Vite, Bootstrap
License: MIT
Source: https://github.com/kbzaman76/laravel-cms
Originally written from my experience designing and building Laravel Addon CMS.
Content rewriting, editing, and formatting assistance provided by ChatGPT.
Top comments (3)
the core, addon, and theme boundaries are clear in this design. the part i would formalize next is an addon contract with a version, dependencies, supported cms versions, and a health check before activation. enable and disable should be transactional, with a migration plan and a safe rollback path if provider boot fails. that would make dynamic loading easier to operate as the addon catalog grows.
These are some really good points, especially around the addon contract, health checks, and safe rollback. I’d love to see this evolve with community input. If you’re interested, contributions are very welcome & feel free to open an issue or PR and we can explore the approach together. Would be great to have you involved!
thank you. a stable addon contract and health checks should make extensions easier to trust. safe rollback will help when a theme or addon fails during loading. community feedback should also help test these boundaries across real projects.