Developer Plugin Development

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.
Laravix Documentation · 13.07.2026
Star on GitHub