Coming in Broadcast 3.0. Until then, this page applies only to the Upcoming (edge) build.

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.

Last updated

Was this page helpful?

Thanks for your feedback!

Thanks for letting us know. We'll work on improving this page.