Plugin Pages
A plugin can show its own screens inside Broadcast. You write plain HTML, CSS and JavaScript in a folder named ui.
Three places for a page
| Slot | Where it shows | Size |
|---|---|---|
nav |
A full page, listed under Plugins in the sidebar | Fills the main area, edge to edge. Your page sets its own padding |
subscriberPanel |
A small box on each subscriber’s page | 16rem high |
dashboardWidget |
A small box on the dashboard | 16rem high |
Full example: a hello page
Build pages with Signal, the UI kit that Broadcast serves. It gives you ready-made components, and your page looks like part of Broadcast.
This plugin has one menu page. It shows a greeting, and it counts how many times the page opened.
hello-page/ manifest.json dist/main.js ui/index.html ui/app.js
manifest.json:
{ "id": "local.hello-page", "name": "Hello page", "version": "1.0.0", "requires": { "broadcast": ">=3.0.0 <4.0.0", "pluginApi": "1" }, "ui": { "pages": [ { "slot": "nav", "title": "Hello", "path": "ui/index.html" } ] }, "settings": { "greeting": { "type": "string", "label": "Greeting" } } }
dist/main.js:
export default definePlugin({ async pageData(request, ctx) { const opens = await ctx.store.increment("opens"); return { greeting: ctx.settings.get("greeting") || "Hello", channel: ctx.channel.name, opens, }; }, });
ui/index.html:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>Hello</title> <link rel="stylesheet" href="/plugin-kit/v1/kit.css"> <script type="module" src="/plugin-kit/v1/kit.js"></script> <script type="module" src="app.js"></script> </head> <body> <sg-page title="Hello" description="A first page, built with Signal."> <sg-banner id="problem" tone="danger" hidden>The page could not load.</sg-banner> <sg-card title="This channel"> <p id="greeting">Loading...</p> <p class="sg-help" id="opens"></p> </sg-card> </sg-page> </body> </html>
ui/app.js:
try { const data = await sg.pageData(); document.getElementById("greeting").textContent = `${data.greeting} from ${data.channel}.`; document.getElementById("opens").textContent = `This page opened ${data.opens} times.`; } catch { document.getElementById("problem").hidden = false; }
Pack it with the ui folder:
zip -r -X ../hello-page-1.0.0.broadcast-plugin manifest.json dist ui
After you deploy it and turn it on, open Plugins in the sidebar, and click Hello.
For a full page with a form and a table, follow Signal Quick Start.
The sandbox
Your page runs in a sandboxed frame. Think of it as a page from another website, placed inside Broadcast. This protects the people who use Broadcast: a plugin page cannot act as them on its own.
Here is what that means for your code:
| Not allowed | Do this instead |
|---|---|
Inline <script> |
Put scripts in a file, such as ui/app.js |
Inline <style> and style="..." |
Use Signal, or put styles in a file, such as ui/style.css |
fetch or any network call |
Ask Broadcast through the bridge. Your plugin code can use fetch, and your page can ask it through pageData |
| Files from another website, such as a CDN or web font | Load Signal from your server, and put other files in ui/ or assets/ |
window.confirm and window.alert |
Use sg.confirm and sg.toast |
| New windows and links out of the frame | Use sg-link external or sg.openExternal. Broadcast shows the address and the person opens it |
Setting location to another address |
Your page cannot leave your plugin’s files. Use sg.openExternal |
Cookies and localStorage |
Save data with store.set through the bridge |
| Sending a form to a server | Use sg-form, which hands your code the values |
Images can come from your own files or from data: addresses.
If something does not load, open the browser console. A blocked file shows a Content-Security-Policy message there.
The bridge
The bridge is how your page talks to Broadcast. Signal wraps it in window.sg:
try { const opens = await sg.call("store.get", { key: "opens" }); console.log(opens); } catch (error) { console.log(error.code, error.message); }
sg.call gives you the result, or it throws an error with a code.
Without Signal, your page sends a message to the Broadcast window with postMessage, in the form { id, op, args }, and Broadcast answers with { id, ok, result } or { id, ok: false, error }. Prefer sg: it does this for you.
What a page can call
| Operation | Needs |
|---|---|
pageData |
Nothing more. Calls your pageData function |
channel.get |
Nothing more |
store.get, store.set, store.delete, store.increment, store.list |
Nothing more |
subscribers.get, subscribers.find, subscribers.list |
subscribers: read |
tags.list |
tags: read |
broadcasts.get, broadcasts.list |
broadcasts: read |
sequences.get, sequences.list |
sequences: read |
segments.get, segments.list |
segments: read |
The arguments are the same as for ctx. To read one person’s data, add subscriber_id to a store call. store.list also takes a limit.
Secrets are never available to a page, and neither are writes to subscribers or tags. For those, use pageData.
pageData
pageData runs your plugin code for the page. Your page sends input, and your function gets it.
// In the page const data = await sg.pageData({ action: "refresh" });
// In dist/main.js export default definePlugin({ async pageData({ input }, ctx) { if (input.action === "refresh") ctx.log.info("Someone asked for a refresh."); return { rows: [] }; }, });
Warning
pageData runs with your plugin’s permissions, and input comes from the browser. Check input the same way you check any other input.
Small frames
A subscriberPanel page learns which person it is for from the part of its address after #:
const subscriberId = sg.subscriberId();
A dashboardWidget page gets no such value. Use sg-widget and sg-panel in these frames.
Who can see a page
A page runs as the person viewing it. That person must hold every data permission that the plugin holds in this channel. For example, if a plugin can read subscribers, its pages show only for people who may read subscribers in that channel. Users & Permissions explains those permissions.
Common mistakes
Your styles do nothing. You used an inline <style> or style="...". Move it to a CSS file. Signal Versioning shows how.
You put data in innerHTML. Use textContent, or a Signal component, which does this for you. A subscriber’s name can hold anything, and it must never run as code in an admin’s browser.
You change a UI file and see the old one. Browsers keep UI files for 5 minutes. Each version has its own address, so a new version shows at once after you deploy it and reload.
You forget to pack ui. You see a refused package: ui.pages: "ui/index.html" must be an .html file in ui/ in the package.
You build a sign-in form. Never ask for a Broadcast password in a plugin page.