Guides · Integrations
Plugins
Your Organization's own inputs and pages in Content. A web page you host, loaded in a sandboxed frame as a field's input or a dashboard page, registered once and turned on per environment, with what it may see and a short-lived token for its backend.
Content has an input for every field type, but not for your brand palette, a map pin or a product from your PIM. A plugin is a web page you build and host that Content loads in a frame: as the input of a field, or as a page of its own in the dashboard. The value it sets is the field's, checked like any other, so sites and the API see nothing different.
A plugin is a manifest
A plugin describes itself in a JSON manifest at a URL of yours:
{
"id": "brand-colour",
"name": "Brand colour",
"url": "https://plugins.example.com/brand-colour/",
"locations": [
{ "type": "field", "fieldTypes": ["text", "json"] },
{ "type": "page", "title": "Palette" }
],
"permissions": ["user", "token"],
"config": {
"type": "object",
"properties": {
"palette": {
"type": "string",
"title": "Palette",
"enum": ["web", "print"]
}
},
"required": ["palette"]
}
}| Key | What it is |
|---|---|
id | A lowercase slug, unique in your Organization. Fields name it. |
url | The page Content frames: https, never on obelism.studio. |
locations | field: the input of fields of the fieldTypes it lists (any type but blocks). page: a page in the dashboard, under Plugins. At most one of each. |
permissions | What it gets beyond its own field's value: entry:read (the entry it is in, as it is edited), user (the editor's id, name and role), token (a context token). Empty by default. |
config | The settings each environment gives it: string, number and boolean settings with title, description, enum and required. Never secrets: they are handed to the frame. |
The entry's sidebar isn't extendible: it holds what Content manages about the entry.
Registering and turning it on
- An admin opens Plugins, clicks Register plugin, pastes the manifest URL and clicks Check manifest. Content fetches the manifest (https only, to a public hostname, no redirects, 5 seconds, 64 KB) and shows what it adds, what it can read and change, and the host it loads from. Register registers exactly that; it is on nowhere yet.
- A developer turns it on per environment under Settings → Preview and site → Plugins, with that environment's settings. Turn it on in develop first. An environment may load another URL of the same plugin on the registered origin (a version or branch), or
http://localhost:…in develop, to build against real content. - A developer picks it as a field's Input in the content model. The field stores
editor: { "plugin": "brand-colour", "config": { … } }, with up to 8 KB of settings for that field.
In a plugin's ⋯ menu, Check for updates fetches the manifest again and lists what changed (origin, locations, permissions, settings) before Update saves it. Remove… turns it off in every Space; fields that used it get their own input back, with their values.
editor is part of the model: in model files, promotions and the agent API. Whether a plugin is on isn't promoted, like tokens and webhooks. A plan for an environment where the plugin is off says so as a warning and applies anyway; the field has its own input there until the plugin is turned on.
In the dashboard
- A field whose plugin is on here gets the plugin's frame instead of its own input, wherever the entry form is: the entry page, the canvas's panel and editing on your site.
- A frame loads when it comes into view (block cards start closed). After five plugin frames in one form, the next ones show a Load button with the plugin's name, and load when clicked.
- A plugin that is off, removed, or doesn't answer within 10 seconds leaves the field's own input, with a line saying so. The value stays as it is.
- Agents and MCP write the field's value directly.
GET …/plugins(listPlugins) lists the plugins on in an environment. - The activity log records plugins registered, updated, removed, turned on and off, and their settings changed.
Building one
The frame is sandboxed (allow-scripts allow-same-origin allow-forms allow-popups) on your own origin, so it never sees Content's cookies or page. Your page must allow being framed by Content, and, for editing on your site, by your site too, since browsers check every ancestor:
Content-Security-Policy: frame-ancestors https://content.obelism.studio https://www.example.com@obelism/content-plugin does the protocol:
import { connect } from '@obelism/content-plugin'
const plugin = await connect()
// plugin.context: the field, its config, the value, locale, Space, environment
// and theme; the entry and user when it may have them.
input.value = String(plugin.context.value ?? '')
input.oninput = () => plugin.setValue(input.value)
plugin.onValue((value) => (input.value = String(value ?? '')))Under the hood, Content posts { type: 'obelism:init', protocol: 1 } to your page's origin once, with a MessagePort; everything after goes over that port. The plugin sends ready, then setValue (a field's value, JSON, up to 1 MB), height, openLink and toast; Content sends context, value when the form changes it, and entry updates with entry:read. A plugin never saves or publishes: what it sets is an unsaved change the editor reviews and saves.
Context tokens
With the token permission, await plugin.token() gives a five-minute JWT (ES256, typ: obelism-plugin+jwt) for your backend: aud is the plugin id, sub the user, with org, space, env and role, and entry: { id, contentType } with entry:read. Verify it against https://content.obelism.studio/.well-known/plugin-jwks.json:
import { verifyContextToken } from '@obelism/content-plugin/server'
const claims = await verifyContextToken(token, { pluginId: 'brand-colour' })The token also reads the delivery API of its Space environment, drafts included, as a bearer token: while the person may read there and the plugin is still on. Turning the plugin off ends every token it was given.