Plugin Development
A Laravix plugin is an ordinary Composer package with a Laravel service provider that plugs into the CMS through registries. The first-party Docs and Changelog plugins — the ones running laravix.com — are built exactly this way, and this article walks you through the same recipe.
Prerequisites
- A working Laravix installation (Installation) or the monorepo (Local Development).
- Familiarity with Laravel packages (service providers, auto-discovery).
1. Package skeleton
my-plugin/
├── composer.json
├── src/
│ └── MyPluginServiceProvider.php
├── routes/ (optional)
├── database/migrations/ (optional)
└── resources/
├── views/
└── lang/
composer.json with provider auto-discovery:
{
"name": "acme/my-plugin",
"type": "library",
"autoload": {
"psr-4": { "Acme\\MyPlugin\\": "src/" }
},
"require": { "php": "^8.4" },
"extra": {
"laravel": {
"providers": ["Acme\\MyPlugin\\MyPluginServiceProvider"]
}
}
}
Install it into a site with composer require acme/my-plugin (or a path repository during development).
2. The service provider
The Changelog plugin's provider is a compact template of the moving parts:
namespace Acme\MyPlugin;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Laravix\Cms\Support\BlockRegistry;
use Laravix\Cms\Support\FilamentPluginRegistry;
use Laravix\Cms\Support\HydratorRegistry;
use Laravix\Cms\Support\RouteRegistry;
class MyPluginServiceProvider extends ServiceProvider
{
public function register(): void
{
// Filament panel plugins must register early:
FilamentPluginRegistry::register(MyPluginFilamentPlugin::make());
}
public function boot(): void
{
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
$this->loadViewsFrom(__DIR__.'/../resources/views', 'my-plugin');
$this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'my-plugin');
BlockRegistry::register(MyBlock::definition());
HydratorRegistry::register(MyHydrator::class);
RouteRegistry::register(function () {
Route::get('/my-page', MyController::class)->name('my-plugin.page');
});
}
}
Where to register what: the one hard rule is that FilamentPluginRegistry calls go in register() — the panel collects its plugins early. Everything else (content types, taxonomy types, fields, blocks, routes, hydrators, views, migrations) works from boot(); the registries are read at request time. The docs plugin registers its content type in boot(), the core registers its own in register() — both are fine.
3. What the registries can do
| Registry | You register | Result |
|---|---|---|
ContentTypeRegistry |
content type definitions | New section under Contents, editing form, frontend rendering |
TaxonomyTypeRegistry |
taxonomy type key + label | New type in the taxonomy form |
FieldRegistry |
field definitions (global or per type) | Fields in the content form |
SettingRegistry |
setting definitions | Fields in Settings tabs |
BlockRegistry |
block definitions | Blocks in the visual builder |
NavigationRegistry |
navigation definitions | Additional menus in the Navigation screen |
RouteRegistry |
closures adding routes | Frontend routes registered inside the web middleware group, before the CMS catch-all |
HydratorRegistry |
BlockHydrator class names |
Post-processing of builder HTML on render |
FilamentPluginRegistry |
Filament Plugin instances |
Anything Filament plugins can do: resources, pages, widgets |
Signatures and examples for each: Registries Reference.
4. Frontend pages
Routes registered through RouteRegistry run before the CMS catch-all, so /my-page wins over a content slug my-page. In controllers, reuse the CMS services the docs plugin uses: SiteResolver to find the current site by hostname and PageDataBuilder to assemble standard view data, then render a view — check the active theme first so themes can override your plugin's look:
$theme = $site->theme ?? 'default';
$view = view()->exists("themes.{$theme}::my-plugin.page")
? "themes.{$theme}::my-plugin.page"
: 'my-plugin::page';
5. Migrations and models
Plugins can ship their own tables — the Changelog plugin creates changelog_releases and changelog_items via loadMigrationsFrom(). Scope rows to sites with a site_id column to stay multi-tenant.
Study the real thing
Both first-party plugins live in the monorepo and are intentionally small:
packages/laravix/docs-plugin— content type + taxonomy type + fields + routes + controllers (no Filament plugin, no migrations).packages/laravix/changelog-plugin— Filament plugin, migrations, models, a builder block and an HTML hydrator.