Plugin frontend and Power SDK
Add pages to the FeatherPanel sidebars, public pages and global scripts from a plugin, and use the window.FeatherPanel Power SDK and iframe bridge.
On this page
A plugin has three ways to put something on screen:
- Pages in the client, admin, server, VDS or WebSpace sidebar, or at a public URL. Each page is an HTML file shown in an iframe.
- Widgets placed in named slots on existing pages. See Widget slots.
- Global code, a script and a stylesheet loaded on every panel page.
Theme and UI packs, which restyle or replace parts of the panel, are covered in Themes and UI packs.
Sidebar pages
Frontend/sidebar.json has one object per sidebar. Keys are the page path relative to that sidebar.
{
"client": {
"/demoshowcase": {
"name": "Demo Showcase",
"lucideIcon": "sparkles",
"redirect": "/demoshowcase",
"component": "/demoshowcase/dist/client-page.html",
"description": "Example client page",
"category": "client",
"group": "Demo"
}
},
"admin": {
"/demoshowcase": {
"name": "Demo Showcase",
"lucideIcon": "puzzle",
"component": "/demoshowcase/dist/admin-page.html",
"category": "admin",
"group": "Demo",
"permission": "admin.plugins.view"
}
}
}
| Sidebar key | Page URL |
|---|---|
client | /dashboard/<path> |
admin | /admin/<path> |
server | /server/<uuidShort>/<path> |
vds | /vds/<id>/<path> |
webspace | /webspace/<uuidShort>/<path> |
| Field | Notes |
|---|---|
name | Label in the sidebar. |
lucideIcon | A Lucide icon name. |
component | Path of the HTML file inside Frontend/Components/. It is served from /components/<identifier>/. |
redirect | Path the entry opens, relative to the sidebar prefix. Defaults to the key. |
description | Shown on hover and in search. |
category | Same as the sidebar key. |
group | Sidebar group. A built-in name puts the entry there: management, files, networking, automation or configuration in the server, VDS and WebSpace sidebars, or an admin group such as users, infrastructure or system. Any other name makes its own group. Without a group the entry goes to plugins. |
permission | Permission node a user needs to see the entry. |
priority | Order inside the group. In the server sidebar, built-in entries use steps of 100, for example Backups at 300 and Import at 400, so 350 lands between them. |
Admins can hide individual entries from users on the plugin’s Visibility tab, and limit server entries to certain spells on the Spells tab.
What the page receives
The iframe URL gets query parameters you can read with URLSearchParams:
| Parameter | When |
|---|---|
serverUuid | Server pages. |
vdsId | VDS pages. |
webspaceUuid | WebSpace pages. |
__theme | Always: light or dark. |
If your component already contains serverUuid=notFound (or the VDS or WebSpace equivalent), the panel replaces the
placeholder instead of appending a new parameter.
Public pages
Frontend/public.json adds pages outside the dashboard, for example a public status page:
{
"pages": [
{
"path": "/status-board",
"name": "Status board",
"component": "/myplugin/dist/status.html",
"nav": { "label": "Status", "order": 50 }
}
]
}
path and component are required. Paths that belong to the panel itself are reserved and ignored with a warning in
the log. query adds fixed query parameters, fallbackPath (default /dashboard<path>) is used when the page cannot
be shown, and nav puts a link in the public navigation, sorted by order (default 100).
Global script and stylesheet
Frontend/index.js and Frontend/index.css from every installed plugin are joined and served at
/api/system/plugin-js and /api/system/plugin-css, and the panel loads both on every page. Each plugin’s script is
wrapped in its own function, so top-level variables do not leak between plugins. The joined files are cached for two
minutes, so an edit can take that long to appear. Use them for small
additions: a keyboard shortcut, an action handler, a style fix. Code here runs in the panel window and can use the
Power SDK directly.
The Power SDK
The panel exposes window.FeatherPanel (host API version 2) to code running in the host page.
| Namespace | Methods |
|---|---|
events | on, once, off, emit |
actions | register, unregister, run |
theme | getMode, getAccent, getPackId, subscribe |
api | get, post, put, delete (axios, with the user’s session) |
toast | success, error, info, message |
navigate | navigate(path) |
context | getPathname, getUser, getUiPackId, subscribe |
ui.modal | register, open, close |
search | contribute, query |
shortcuts | register |
// Frontend/index.js
window.FeatherPanel?.events.on('fp:server:power:result', (payload) => {
console.log('Power action finished', payload);
});
window.FeatherPanel?.toast.info('My plugin is loaded');
Events
The SDK declares these events on the event bus:
fp:host:ready, fp:route:change, fp:page:mount, fp:page:unmount, fp:theme:change, fp:uipack:change,
fp:session:change, fp:server:context, fp:server:power:result, fp:server:console:ready, fp:files:saved,
fp:files:selection, fp:api:error.
The current panel never emits fp:files:selection, and nothing runs the fp:files:upload action below. The
Power SDK reference lists every event and action with its payload and where it fires.
Actions
Actions are named hooks that handlers can extend or cancel. The built-in ids are:
fp:server:power, fp:files:save, fp:files:upload, fp:nav:push, fp:form:submit, fp:ui:confirm,
fp:api:request, fp:search:query, fp:shortcut:invoke, fp:data:action.
Any element with a data-fp-action attribute runs that action on click. data-fp-payload takes JSON that is passed
to the handlers, and afterwards fp:data:action runs with the action id and payload:
<button data-fp-action="myplugin:refresh" data-fp-payload='{"scope":"all"}'>Refresh</button>
Talking to the panel from an iframe
Plugin pages and widgets run in iframes on the panel origin. They talk to the host with postMessage. The host only
accepts messages from its own origin.
const host = window.parent;
// Emit an event on the host bus
host.postMessage(
{ type: 'featherpanel-bus', op: 'emit', event: 'myplugin:saved', payload: { id: 1 } },
window.location.origin
);
// Subscribe to a host event
host.postMessage(
{ type: 'featherpanel-bus', op: 'on', event: 'fp:theme:change', subscriptionId: 'theme-1' },
window.location.origin
);
window.addEventListener('message', (e) => {
if (e.origin !== window.location.origin) return;
if (e.data?.type === 'featherpanel-bus-event') {
// e.data carries the event and payload
}
if (e.data?.type === 'featherpanel-bus-reply') {
// acknowledgement for emit, on, off and runAction
}
});
op | Fields | Notes |
|---|---|---|
emit | event, payload, optional requestId | Emits on the host bus. |
on / off | event, subscriptionId | Events arrive as featherpanel-bus-event messages. |
runAction | actionId, ctx, requestId | Runs an action on the host and replies with the result. |
unregisterAction | actionId, handlerId | Removes a handler. |
registerAction | Not supported from an iframe. Register handlers in Frontend/index.js. |
Because the page is same-origin, it can also call your plugin routes with
fetch('/api/addons/myplugin/...').
Related
Frequently asked questions
Yes. Plugin pages are served from the panel's own origin, so a fetch to /api/... from the page carries the user's session. The request runs with that user's permissions.
No. Functions cannot cross postMessage, so the bridge refuses registerAction from an iframe. Register handlers in Frontend/index.js, which runs in the host page, and use emit or runAction from the iframe.