Problem
A plugin's editor module is reachable by URL (/editor/<slug>/)
but invisible in the chrome. You want it pinned into the sidebar
so logged-in editors can click into it without remembering the
URL. The plugin should own the registration; the theme should
not have to know about it.
Recipe
Construct a MenuItem and hand it to
PluginContext::addEditorMenuItem() from inside
Plugin::register(). The registry collects entries from every
plugin, sorts them by position then registration order, and
renders them into the slot named by displayType. The sidebar
slot is the default and is what most plugins want.
namespace Acme\TeamEditor;
use Scriptor\Boot\Editor\Menu\MenuItem;
use Scriptor\Boot\Plugin\Plugin as ScriptorPlugin;
use Scriptor\Boot\Plugin\PluginContext;
final class Plugin implements ScriptorPlugin
{
public function register(PluginContext $context): void
{
$context->registerEditorModule(
'team',
static fn ($c, $editor) => new TeamModule($c, $editor),
);
$context->addEditorMenuItem(new MenuItem(
slug: 'team',
label: 'Team',
icon: 'people',
position: 50,
));
}
public function version(): string { return '0.1.0'; }
}
Three pieces worth flagging:
slugis the link target and the module key. Whenhrefis null (default), the layout derives the URL as$site->siteUrl . '/' . $slug . '/'. The module registered under the same slug is what the link lands on; keeping the two matched is how the chrome and the module stay in sync.positionis an integer, not a fraction. Built-in entries use10(Pages),20(Files),30(Plugins),40(Profile). Pick something between two existing values to slot in; pick something much larger (100+) to land at the end of the rail. Re-numbering is a per-release decision, not a per-deploy one.iconis a string that the chrome's template maps to its icon set. The built-in Scriptor admin uses theheroiconsoutline names. A non-matching value renders as no icon, which is fine for one-off custom slugs; for sites with a custom icon set, the theme replaces the icon-lookup helper.
Variants
Top-right profile cluster instead of the sidebar
The profile slot is the small cluster of icons next to the
logged-in user's name (logout, change password, etc.). Add to it
by setting displayType:
$context->addEditorMenuItem(new MenuItem(
slug: 'team',
label: 'Team admin',
icon: 'people',
displayType: 'profile',
position: 15,
));
The profile slot is small; reserve it for plugin-owned identity or session actions, not for general feature modules. A feature module pinned there gets lost.
Explicit href for non-module entries
When the menu entry points to something other than an editor
module (an external dashboard, a CSRF-protected logout URL, a
deep link inside an existing module), pass href explicitly:
$context->addEditorMenuItem(new MenuItem(
slug: 'analytics',
label: 'Analytics',
href: 'https://plausible.io/yoursite.com',
icon: 'chart-bar',
position: 200,
));
The slug is still required (it acts as a CSS hook + sort tie-
breaker) but it does not have to correspond to a registered
module when href is set explicitly.
Conditional entry (logged-in check, capability check)
addEditorMenuItem() registers unconditionally; the rendered
chrome decides what to show. For dynamic entries that depend on
the current user, register from a PageResolving listener
instead:
public function register(PluginContext $context): void
{
// Always register the module
$context->registerEditorModule('team', ...);
// Conditionally add the menu item per request
$context->container()->get(\Imanager\Http\SessionStore::class);
// ... and gate the addEditorMenuItem call on the session check
}
Conditional sidebar entries are rare; the cleaner pattern is usually to render the entry unconditionally and have the module itself emit a "you don't have access" page when the user lacks the capability. The editor's auth gate already prevents anonymous users from reaching the module at all.
See also
- Editor module with CRUD sub-routes: the module this recipe pins into the sidebar; without one, the menu item is a dead link
MenuItem: the readonly DTO this recipe constructs, including the full constructor surfacePluginContext:addEditorMenuItem()is the registration entry point;registerEditorModule()is the natural sibling call- Concept: Editor extensions: walks the module + menu + container picture as narrative
Editor: the surface a module receives once its sidebar entry is clicked