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: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 →