Contensio logo

Plugin anatomy

The file layout every Contensio plugin follows, the namespace convention, and what each file does.

Every Contensio plugin follows the same folder structure. Here's the full layout - most files are optional; only three are strictly required.

The minimum

acme/plugin-awesome/
├── plugin.json              <- required
├── composer.json            <- required (for Packagist)
├── LICENSE                  <- required
└── src/
    └── AwesomeServiceProvider.php    <- required: the entry point

The full layout

acme/plugin-awesome/
├── plugin.json
├── composer.json
├── LICENSE
├── README.md
├── .gitignore
│
├── src/                                 PHP code
│   ├── AwesomeServiceProvider.php
│   ├── Http/
│   │   └── Controllers/
│   │       ├── Admin/
│   │       │   └── SettingsController.php
│   │       └── Frontend/
│   │           └── AwesomeController.php
│   ├── Models/
│   │   └── AwesomeItem.php
│   ├── Support/
│   │   └── AwesomeConfig.php
│   └── Widgets/
│       └── AwesomeWidget.php
│
├── routes/
│   ├── admin.php                        admin routes (auth-gated)
│   └── public.php                       public frontend routes
│
├── database/
│   └── migrations/
│       └── 2026_05_01_000001_create_contensio_awesome_items_table.php
│
├── lang/
│   ├── en/
│   │   └── acme-awesome.php
│   └── fr/
│       └── acme-awesome.php
│
├── resources/
│   └── views/
│       ├── admin/
│       │   ├── index.blade.php
│       │   └── form.blade.php
│       └── partials/
│           └── settings-hub-card.blade.php
│
├── config/
│   └── acme-awesome.php                 optional developer config
│
└── public/                              published assets (optional)
    ├── css/
    │   └── acme-awesome.css
    └── js/
        └── acme-awesome.js

The namespace convention

Every plugin gets a single namespace string ($ns) that it uses everywhere: view loading, translation loading, config merging, route name prefixes, publish tags, and public asset paths.

The formula is:

{vendor}-{slug}  where slug = composer name minus the 'plugin-' prefix

Examples:

Composer name $ns
acme/plugin-awesome acme-awesome
contensio/plugin-social-connect contensio-social-connect
contensio/plugin-ads-manager contensio-ads-manager

This is a hard requirement for the Contensio plugin directory. Two plugins from different vendors can share the same short name (e.g. both called shop) - the vendor prefix in the namespace prevents all collisions in views, config, translations, route names, and Artisan commands.

The $ns property is declared on the service provider:

class AwesomeServiceProvider extends ServiceProvider
{
    protected string $ns = 'acme-awesome';

    public function boot(): void
    {
        $this->loadViewsFrom(__DIR__ . '/../resources/views', $this->ns);
        // ...
    }
}

What each file does

plugin.json - the manifest

The most important file. Contensio's registry reads this to learn the plugin's name, provider class, and how it appears in the admin.

See plugin.json reference for every field.

composer.json - for Packagist

Identical responsibilities to plugin.json but in the Composer world. The extra.cms block tells Contensio this is a plugin:

{
    "name": "acme/plugin-awesome",
    "type": "library",
    "extra": {
        "cms": {
            "type":     "plugin",
            "provider": "Acme\\Awesome\\AwesomeServiceProvider"
        }
    }
}

LICENSE

Required for all plugins. Use AGPL-3.0-or-later for plugins you release on the Contensio directory. Any OSI-approved license is accepted.

src/{Name}ServiceProvider.php - the entry point

A standard Laravel service provider. Contensio boots it when the plugin is enabled. This is where you load routes, views, migrations, register hooks, and declare the $ns.

See The service provider for a full walkthrough.

routes/admin.php and routes/public.php

Route declarations, split by auth context. Both are loaded by your service provider. Admin routes use the contensio.auth + contensio.admin middleware group. Public routes that need to reference runtime config (like a URL prefix stored in settings) should be registered via $this->app->booted() so the config is available.

A simple plugin with only an admin settings page can use a single routes/web.php instead.

database/migrations/

Standard Laravel migrations. Run automatically when the user enables your plugin. Table names must be prefixed with the vendor name:

contensio_awesome_items    (for acme/plugin-awesome - if vendor is "contensio")
acme_awesome_items         (for acme/plugin-awesome - if vendor is "acme")

See Migrations and Data storage for the full strategy.

lang/{locale}/{ns}.php

Per-locale language files. The file name must match $ns (acme-awesome.php). Loaded under the same namespace as views.

resources/views/

Blade views, loaded under $ns. Admin views extend contensio::admin.layout. Frontend views extend layouts.public.

config/{ns}.php

Developer-level configuration (not admin-editable). Most plugins don't need this - use PluginOptions (stored in DB, admin-editable via a settings page) instead.

public/css/ and public/js/

Static assets. Published to public/vendor/{ns}/ via $this->publishes(). Only needed if your plugin ships its own CSS or JS.

Next

Head to The service provider for a complete walkthrough of the entry point.