The manifest.json reference
Every EmDash plugin declares its identity, version, capabilities, hooks,
storage collections, and admin surfaces in a
manifest.json
file. The CMS validates the manifest against a Zod schema before loading
the plugin — invalid manifests fail to load.
Who this is for: plugin authors writing or updating a manifest.
Fields
| Field | Required | Type | Purpose |
|---|---|---|---|
| id | yes | string | Plugin identity — lowercase alphanumeric + hyphens, or @scope/name. Immutable. |
| version | yes | string | Strict semver X.Y.Z. No pre-release or build metadata. |
| capabilities | yes | PluginCapability[] | What the plugin is allowed to do. See the capabilities reference. |
| allowedHosts | no | string[] | Hostnames the plugin can reach. Required when declaring network:fetch. |
| storage | no | Record<string, { indexes, uniqueIndexes }> | Named document collections the plugin reads/writes. |
| hooks | no | Array<string | { name, exclusive?, priority?, timeout? }> | Lifecycle events the plugin attaches to. |
| routes | no | Array<string | { name, public? }> | Named routes the plugin registers under its own namespace. |
| admin.entry | no | string | Entry module for admin-side plugin code. |
| admin.settingsSchema | no | Record<string, SettingField> | Form schema for the plugin's settings page. Field types: string, number, boolean, select, secret. |
| admin.pages | no | { path, label, icon }[] | Full admin pages the plugin adds to the dashboard. |
| admin.widgets | no | { id, size?, title? }[] | Dashboard widgets the plugin provides. |
| admin.fieldWidgets | no | { name, label, fieldTypes[], elements[] }[] | Custom field editors for the content editor. |
| name | no | string | Display name for the marketplace listing. Falls back to id. |
| description | no | string | Short description for the marketplace listing. |
| minEmDashVersion | no | string | Minimum EmDash core version this plugin supports. |
| changelog | no | string | Markdown changelog for this release. Shown on the plugin page. |
A complete example
A minimal-but-real SEO plugin manifest:
{
"id": "seo-toolkit",
"version": "1.2.0",
"name": "SEO Toolkit",
"description": "Meta tags, sitemaps, and structured data for EmDash sites.",
"minEmDashVersion": "0.9.0",
"capabilities": [
"read:content",
"page:inject",
"network:fetch"
],
"allowedHosts": [
"search.google.com"
],
"storage": {
"redirects": {
"indexes": ["from"],
"uniqueIndexes": ["id"]
}
},
"hooks": [
"content:afterSave",
"page:metadata",
{ "name": "cron", "priority": 10 }
],
"routes": [
"sitemap.xml",
{ "name": "robots.txt", "public": true }
],
"admin": {
"entry": "dist/admin.js",
"settingsSchema": {
"siteName": { "type": "string", "label": "Site name" },
"ogImageDefault": { "type": "string", "label": "Default OG image URL" }
},
"pages": [
{ "path": "/seo", "label": "SEO", "icon": "search" }
]
},
"changelog": "Add BreadcrumbList schema to product pages."
}
Rules worth internalising
- The schema is closed. Unknown fields are stripped or rejected — no hidden configuration drift.
- Write capability implies the read. Declare
write:contentand you getread:contentfor free. Same forwrite:media. - network:fetch requires allowedHosts.
network:fetch:anyis the unrestricted escape hatch and is scrutinised hard during audit. - Version bumps are irrevocable. A published version can be flagged and unpublished, but the same semver cannot be republished with different contents.
- Hooks keep their scope. A hook fires only if its capability is also declared. Declaring
content:afterSavewithoutread:contentfails validation.
Updated . Previous: ← how plugins work · Next: capabilities reference →