The manifest.json reference

Every EmDash plugin declares its identity, version, capabilities, hooks, storage collections, and admin surfaces in amanifest.jsonfile. 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

FieldRequiredTypePurpose
idyesstringPlugin identity — lowercase alphanumeric + hyphens, or @scope/name. Immutable.
versionyesstringStrict semver X.Y.Z. No pre-release or build metadata.
capabilitiesyesPluginCapability[]What the plugin is allowed to do. See the capabilities reference.
allowedHostsnostring[]Hostnames the plugin can reach. Required when declaring network:fetch.
storagenoRecord<string, { indexes, uniqueIndexes }>Named document collections the plugin reads/writes.
hooksnoArray<string | { name, exclusive?, priority?, timeout? }>Lifecycle events the plugin attaches to.
routesnoArray<string | { name, public? }>Named routes the plugin registers under its own namespace.
admin.entrynostringEntry module for admin-side plugin code.
admin.settingsSchemanoRecord<string, SettingField>Form schema for the plugin's settings page. Field types: string, number, boolean, select, secret.
admin.pagesno{ path, label, icon }[]Full admin pages the plugin adds to the dashboard.
admin.widgetsno{ id, size?, title? }[]Dashboard widgets the plugin provides.
admin.fieldWidgetsno{ name, label, fieldTypes[], elements[] }[]Custom field editors for the content editor.
namenostringDisplay name for the marketplace listing. Falls back to id.
descriptionnostringShort description for the marketplace listing.
minEmDashVersionnostringMinimum EmDash core version this plugin supports.
changelognostringMarkdown 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:content and you get read:content for free. Same for write:media.
  • network:fetch requires allowedHosts. network:fetch:any is 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:afterSave without read:content fails validation.

Updated . Previous: ← how plugins work· Next: capabilities reference →