What this covers
How the Plugins sidebar entry works and what you do when you need to add, remove, or temporarily disable a plugin. Plugin install is a Composer-side workflow (or a Docker build-arg workflow if you run the container stack); the editor is a read-only browser.
For the developer-side view of plugins (the API a plugin uses to contribute modules, events, and menu items), see the Concept: Plugin discovery and Build a Module tutorial.
Walkthrough
Step 1: See what is installed
Click Plugins in the sidebar. The page shows two sections:
- A table of booted plugins with five columns:
- Plugin: the plugin's name (
core/editorfor the built-in editor plugin,vendor/packagefor Composer- installed plugins). - Version: the installed version string. For Composer
plugins this is the Composer manifest version (matches the
git tag Composer resolved); for core plugins it is the
string the plugin's
version()method returns. - Events: the PSR-14 events the plugin subscribes to. Empty if the plugin does not listen to any.
- Editor modules: the editor module slugs the plugin
registers (each becomes a
/editor/<slug>/route). - Menu items: the sidebar menu items the plugin contributes.
- Plugin: the plugin's name (
- A disabled section below the table, listing any plugin
FQCN entries in
plugins.disabled(see Step 3 below). Empty most of the time.
This view is read-only. There is no "install", "remove", "enable", or "disable" button.
Step 2: Install a plugin
How you install depends on whether you run Scriptor natively or in Docker. The persisting paths are:
Native install (host with PHP + Composer):
composer require bigins/scriptor-markdown-pages
Scriptor's composer.json already has a repositories block
that points at the VCS sources for bigins/* plugins, so no
extra setup is needed for those. Third-party plugins on
Packagist work the same way.
After the require finishes, reload the editor. The new plugin appears in the table; any sidebar items the plugin contributes appear in the sidebar.
Docker (compose stack):
Container filesystems below the volumes are immutable across restarts, so the workflow is to bake the plugin into the image via a build arg in your compose override:
services:
scriptor:
build:
args:
SCRIPTOR_PLUGIN_REPOS: "https://github.com/bigin/scriptor-markdown-pages"
SCRIPTOR_PLUGINS: "bigins/scriptor-markdown-pages:^0.1"
Then:
docker compose up -d --build
Scriptor's Dockerfile runs composer require $SCRIPTOR_PLUGINS
during image build; the plugin lands in vendor/ and survives
every restart, recreation, or deploy.
Multiple plugins go in the same args, space-separated. Each
non-Packagist plugin needs its repo URL in SCRIPTOR_PLUGIN_REPOS
too:
SCRIPTOR_PLUGIN_REPOS: "https://github.com/bigin/scriptor-markdown-pages https://github.com/bigin/scriptor-markdown-feed"
SCRIPTOR_PLUGINS: "bigins/scriptor-markdown-pages:^0.1 bigins/scriptor-markdown-feed:^0.1"
Trap: docker exec scriptor composer require ... works
immediately because the discovery cache invalidates from
installed.json mtime, but the change lives in the running
container's writable layer and is wiped by the next
docker compose down && up. Use it for "does this plugin
boot" probes; never as an install path for anything you want
to keep.
Step 3: Disable a plugin without uninstalling
Sometimes you want to temporarily turn a plugin off (debugging
a suspected interaction, A/B comparing site behaviour) without
running composer remove. Set
$config['plugins']['disabled'] in
data/settings/custom.scriptor-config.php:
return [
'plugins' => [
'disabled' => [
'bigins\\ScriptorMarkdownPages\\Plugin',
],
],
];
The value is the plugin's fully-qualified class name (the
same string the Plugins table's first column shows for that
plugin, with the backslashes doubled because PHP string
escaping). The plugin stays in vendor/; the PluginManager
skips its register() hook on the next request.
Reload the editor. The plugin's row moves from the table into the Disabled section. Any sidebar items it contributed are gone.
To re-enable, remove the entry from the array (or comment out
the whole disabled block) and reload.
Step 4: Uninstall a plugin
Reverse of the install path you used. First check whether the plugin owns database schema, because that changes the order of the steps.
Most plugins are stateless: they add events, modules, or menu
items, but no database schema of their own. Those you remove with
a plain composer remove. Some plugins are lifecycle plugins:
on install they register their own iManager fields or categories,
and they ship an uninstall hook to remove that schema again. If
the plugin's README mentions a bin/scriptor plugin:install step,
it is a lifecycle plugin and needs the extra CLI step below.
Native, stateless plugin:
composer remove bigins/scriptor-markdown-pages
The plugin disappears from vendor/composer/installed.json;
the discovery cache invalidates on the next request; the row
is gone from the Plugins table; any editor modules and menu
items the plugin contributed are gone.
Native, lifecycle plugin (order matters):
Run the uninstall hook before removing the code, so the plugin can drop the schema it created while its class is still loadable:
bin/scriptor plugin:uninstall vendor/package-name # removes its schema, clears state
composer remove vendor/package-name # then drop the code
Add --purge-data to the plugin:uninstall call if you also want
the plugin's per-row values stripped from the database (the default
keeps them, in case you reinstall later).
If you run composer remove first by mistake: the uninstall
hook can no longer run, so the schema the plugin created stays in
the database and the plugin shows up as an ORPHAN row in
bin/scriptor plugin:list. Clear the leftover state entry with:
bin/scriptor plugin:cleanup-orphan vendor/package-name
This only clears the state entry. It cannot remove the orphaned
schema (the plugin's code is gone, so Scriptor no longer knows what
to remove). To clean the schema too, reinstall the package, run
plugin:uninstall properly, then composer remove. That is the
whole reason the uninstall order matters.
Docker:
Drop the plugin from the SCRIPTOR_PLUGINS build arg in your
compose override (or remove the whole arg block if it was the
only plugin), then rebuild:
docker compose up -d --build
The new image is built without that composer require step,
so the plugin never lands in vendor/. For a lifecycle plugin,
run docker compose exec scriptor bin/scriptor plugin:uninstall vendor/package-name before you rebuild without the arg, for the
same reason as the native flow above; otherwise its schema is left
orphaned.
Important: plugin-owned data is not removed. Content trees
the plugin manages, settings it persists, uploads it took
ownership of, or database rows it wrote stay where they are.
Check the plugin's README for its on-disk footprint and delete
those paths manually if you want a clean state. For
bigins/scriptor-markdown-pages, that means the per-theme
themes/<name>/content/ directories with _index.md files.
What to check after
After installing a plugin:
- The plugin appears as a row in the Plugins table with the version Composer resolved.
- If the plugin contributes events, modules, or menu items, those columns show their values; empty columns are also fine.
- The frontend or editor surface the plugin claims to provide works (check the plugin's README for what to expect).
After disabling via plugins.disabled:
- The plugin's row moves to the Disabled section.
- Sidebar items the plugin contributed are gone.
- The frontend or editor behaviour the plugin provided is gone.
After uninstalling:
- The plugin no longer appears anywhere on the Plugins page.
- The
vendor/<vendor>/<package>/directory is gone. - For a lifecycle plugin,
bin/scriptor plugin:listshows noORPHANrow for it. AnORPHANrow means the code was removed before its uninstall hook ran; see the troubleshooting entry below.
Troubleshooting
"No plugins booted" but I just installed one
Three causes, most to least common:
- The package is not marked as a Scriptor plugin. The
plugin's
composer.jsonneeds"type": "scriptor-plugin"and"extra": {"scriptor": {"plugin": "<FQCN>"}}for the PluginManager to discover it. A package that does not declare both is a regular library, not a plugin. - Discovery cache is stale. Touch
vendor/composer/installed.json(Composer normally does this on install, but a manually-edited vendor tree can leave it inconsistent). The PluginManager invalidates its discovery cache by mtime. - Plugin's
register()threw. Checkdata/logs/for a stack trace. A plugin that throws during boot is silently skipped and the editor continues without it.
The Plugins table shows the wrong version
The table prefers the Composer manifest version (the git tag
Composer resolved) over the plugin's version() string. If
the two disagree, the manifest version wins (and is shown).
The plugin author probably forgot to bump the version()
string in their release commit; harmless, but worth noting if
you are debugging against a specific release.
"Plugin disabled" but its sidebar item is still there
The disabled list takes the plugin's fully-qualified class
name, not the package name. bigins/scriptor-markdown-pages
in the disabled list does nothing because the PluginManager
looks for the class name (e.g.
bigins\\ScriptorMarkdownPages\\Plugin). Check the Plugins
table's first column for the exact string to disable.
plugin:list shows an ORPHAN row after uninstalling
This means a lifecycle plugin's code was removed (composer remove
or a rebuild without the SCRIPTOR_PLUGINS arg) before its
uninstall hook ran, so its state entry was left behind and the
schema it created is still in the database. Clear the state entry
with bin/scriptor plugin:cleanup-orphan vendor/package-name. To
also remove the orphaned schema, reinstall the package, run
bin/scriptor plugin:uninstall vendor/package-name, then
composer remove. Always run plugin:uninstall before removing a
lifecycle plugin's code to avoid this in the first place.
After docker compose up -d --build, the plugin still does not boot
Two checks:
- The build arg name is exactly
SCRIPTOR_PLUGINS(all caps, underscore). A typo (SCRIPTOR_PLUGINsingular) silently has no effect. - The
--buildflag is actually rebuilding the image.docker compose up -dwithout--buildreuses the existing image even when the compose file changed.docker compose build --no-cache scriptorthenup -dforces a clean rebuild if you suspect a stale image.
See also
- Site settings and theme switch: where
plugins.disabledlives in the override file - Editor UI tour: map of the Plugins sidebar entry
- Concept: Plugin discovery: developer-side concept for how the PluginManager finds and boots plugins
- Build a Module: the tutorial for writing a plugin from scratch
docs/install.mdin the Scriptor repo: the install reference, including the Docker build-arg flowdocs/plugin-lifecycle.mdin the Scriptor repo: the full lifecycle CLI reference (plugin:install,plugin:uninstall,plugin:cleanup-orphan)