How a finished plugin gets onto a site, how it is upgraded, and how it is removed — with the exact commands and the limits of the Plugin Manager.
A package is a ZIP whose contents are one module folder. Both layouts are accepted by the validator:
testimonials-1.0.0.zip testimonials-1.0.0.zip
Testimonials/ (a) module.json (b)
module.json composer.json
composer.json app/ ...
app/ ...module.json and it defines the module root; everything must live under that root (multiple_roots otherwise — remove __MACOSX).module.json, composer.json, app/, config/, database/, lang/, resources/, routes/, and published assets under resources/assets/.tests/, node_modules/, vendor/, .git, editor folders, symlinks (rejected), and files with Windows-reserved names.config/plugins.php).PowerShell — build a ZIP of the sample plugin
$src = "examples\Testimonials" # from the documentation folder
$tmp = Join-Path $env:TEMP "pkg"; Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory $tmp | Out-Null
Copy-Item -Recurse $src "$tmp\Testimonials"
Remove-Item "$tmp\Testimonials\tests" -Recurse -Force
Compress-Archive -Path "$tmp\Testimonials" -DestinationPath testimonials-1.0.0.zipcd /tmp && rm -rf pkg && mkdir pkg && cp -r /path/to/Testimonials pkg/ && rm -rf pkg/Testimonials/tests
(cd pkg && zip -r ../testimonials-1.0.0.zip Testimonials -x '*.DS_Store')A ready-made testimonials-1.0.0.zip is available to download. It contains the complete example plugin without the tests.
install plugins), choose Add new and upload the ZIP. The archive is validated and unpacked into a private temporary folder; nothing in it is executed. On success it is moved to Modules/{Name} and listed as inactive.composer dump-autoload (or composer dump-autoload -o in production). This step is required: the Plugin Manager never runs Composer.activate plugins). Missing, inactive or out-of-range dependencies are reported next to the plugin.php artisan migrate, and the plugin's seeder if it has one: php artisan db:seed --class="Modules\Testimonials\Database\Seeders\DatabaseSeeder".php artisan module:publish Testimonials copies resources/assets to public/modules/testimonials.php artisan optimize:clear (the Plugin Manager already does this on every operation).cp -r Testimonials Modules/Testimonials # or unzip into Modules/
composer dump-autoload
php artisan module:enable Testimonials
php artisan migrate
php artisan module:publish Testimonials # only if it ships resources/assets
php artisan optimize:clear
php artisan module:list # shows Enabled/Disabled and priorityCommand-line commands bypass safeguards
module:enable, module:disable and module:delete are plain nwidart commands: they do not check dependencies, protection or locks. The UI path through PluginManager does.
The upload flow refuses a plugin that is already installed ("Plugin updates are not supported by this upload flow"). Ship an update as a replacement of the files:
php artisan down --secret=...).Modules/{Name} with the new version, keeping any site-specific files.composer dump-autoload
php artisan migrate --force
php artisan optimize:clearmodule.json so dependants that require a version range ("yourplugin": "^1.2") can tell.Rules that make updates painless:
Schema::hasTable / hasColumn when appropriate.add_booking_refund_permissions): they run once per site and assign to the right roles.ShortcodeBlog still accepts retired blog layout attributes for exactly this reason).The platform's own Vite build lists every bundled module entry in the root vite.config.js; a plugin installed from a ZIP cannot rely on that. Choose one:
| Approach | How | Best for |
|---|---|---|
| Static assets | Put compiled CSS/JS/images in resources/assets; after install run php artisan module:publish {Name}; reference with asset('modules/{alias}/app.css') in Blade | ZIP-distributed plugins |
| Inline in Blade | @push('page_header') / @push('page_scripts') with small snippets | Tiny additions |
| Vite entry in the root config | Add the file paths to input in vite.config.js and run npm run build, then @vite('Modules/{Name}/resources/js/app.js') | In-repo modules and custom builds |
| Shortcode assets | Declare ->assets(['vite' => [...], 'styles' => [...], 'scripts' => [...]]) on the shortcode definition; they load only on pages containing the tag | Page-builder blocks |
<x-shortcode-*> tags can no longer be compiled by Blade (unknown component). PageSlugRenderer swallows that exception and prints the stored HTML unrendered, so every shortcode on that page stops rendering, not only yours. Warn site owners before deactivating a plugin that provides blocks.php artisan migrate:rollback --path=Modules/Testimonials/database/migrations) to run before deleting.Setting::forget('key') + Setting::save() in your own cleanup command.module.json: valid name, alias, description, version, providers, accurate requires.composer.json: PSR-4 map for app/, database/factories, database/seeders.lang/en/*.php (add the other languages you support).admin.{alias}.*.vendor/bin/pint {Module} and vendor/bin/phpstan analyse {Module} pass; tests pass.env() calls outside config/; no hard-coded URLs; no {{ }}/@php in data baked into page content.