Add an admin page
In this guide you extend the counter plugin with two things in the admin: a settings form where the owner sets a goal, and a small page that shows the count next to that goal. Both appear on the plugin's Settings tab, and editors can open the page too.
Before you start
- You have the
counterplugin from Keep data and add a route, loaded and grantedkv, its key-value store. - You can sign in as an owner, and ideally also as an editor to check what they see.
The page runs in the sandbox, a closed space where it can do only what its permissions allow. It cannot reach the network, the admin's cookies or other plugins. It talks to your plugin through a small script the server provides, called the bridge.
Steps
Declare the page and the settings
- Replace
plugin.jsonwith version0.2.0. It adds thesettingspermission, an admin-only route (a web address the plugin answers), the page (ui) and asettings_schema, a JSON Schema that the admin turns into a form:{ "name": "counter", "version": "0.2.0", "sdk": "^0.11", "description": "Counts publishes and shows the number.", "permissions": ["kv", "settings"], "routes": [ { "method": "GET", "path": "/count", "public": true }, { "method": "GET", "path": "/stats", "roles": ["owner", "editor"] } ], "cron": [ { "name": "report", "every": "1h" } ], "ui": { "title": "Counter", "roles": ["owner", "editor"] }, "settings_schema": { "type": "object", "properties": { "goal": { "type": "integer", "title": "Goal", "description": "How many publishes you aim for.", "minimum": 1, "default": 10 } } } }/statshas nopublicflag, so only signed-in owners and editors can call it.uiwithoutentryshowsui/index.html. - In
main.go, replace thebody, _ := json.Marshal(...)line of the route handler with this block, so/statsalso returns the goal:out := map[string]int{"published": n} if r.Pattern == "/stats" { var s struct { Goal int `json:"goal"` } if err := guest.Call(sdk.FnSettingsGet, struct{}{}, &s); err != nil { return sdk.HTTPResponse{Status: 503, Body: err.Error()}, nil } out["goal"] = s.Goal } body, _ := json.Marshal(out)r.Patternis the path fromplugin.jsonthat matched.settings.getreturns the saved settings, with the schema'sdefaultfilled in for anything not saved yet.
Write the page
- Create
ui/index.html. The page may not contain inline script or style, so both live in their own files:<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>Counter</title> <link rel="stylesheet" href="app.css"> </head> <body> <h1>Counter</h1> <p id="total">Loading...</p> <script src="/plugin-ui/_sdk/bridge.js"></script> <script src="app.js"></script> </body> </html> - Create
ui/app.js.hatamoto.routecalls one of the plugin's own routes, as the person who opened the page:(async () => { await hatamoto.ready; const res = await hatamoto.route("GET", "/stats"); const out = document.getElementById("total"); if (res.status !== 200) { out.textContent = "Could not load the count (" + res.status + ")."; return; } const s = JSON.parse(res.body); out.textContent = s.published + " published. Goal: " + s.goal + "."; })(); - Create
ui/app.css:body { font-family: system-ui, sans-serif; margin: 1rem; }
Build, load and grant
- Build, and copy the
uidirectory into the build as well:GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o build/plugin.wasm . cp plugin.json build/ cp -r ui build/ rm -rf <plugins>/counter cp -r build <plugins>/counter - Restart the server. Open Plugins, then counter, then Permissions, tick
settingsas well askv, and save.
Check it worked
- As the owner, open Plugins > counter > Settings (
/admin/plugins/counter/settings). You see a Goal field and, below it, the page with "1 published. Goal: 10." (your count may differ). The goal is 10 because nothing is saved yet and the schema's default applies. - Change Goal to 20 and click Save settings. The page now ends with "Goal: 20."
- Sign in as an editor and open the same address. The page shows the same line, without the settings form, because only owners change settings.
If the page says "Could not load the count (503).", the plugin lacks a permission: grant both kv and settings. If the page stays on "Loading...", open the browser console. A message about the Content Security Policy means a script or style is inline in index.html; move it to its own file.
The bridge has one more call, hatamoto.host, for host functions, but only the owner may use it. Its rules, the full page policy, template functions and public files are on Admin pages and templates.