docs(plugins): plugin CLI, manifest & authoring-shape docs#1059
Conversation
… changes (#1040, #1057) Adds migration guides for site operators and plugin authors, new reference pages for emdash-plugin.jsonc and the emdash-plugin CLI, and rewrites the sandboxed-plugin guides to the new default-export shape. Migration guides follow Astro's breaking-changes format; Atmosphere account terminology used throughout.
|
Scope checkThis PR changes 1,278 lines across 14 files. Large PRs are harder to review and more likely to be closed without review. If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs. See CONTRIBUTING.md for contribution guidelines. |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-perf-coordinator | de4f38e | May 18 2026, 03:55 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-i18n | de4f38e | May 18 2026, 03:56 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
docs | de4f38e | May 18 2026, 03:56 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | de4f38e | May 18 2026, 03:57 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-cache | de4f38e | May 18 2026, 03:57 PM |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/blocks
@emdash-cms/cloudflare
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
There was a problem hiding this comment.
Pull request overview
Documents the new sandboxed plugin authoring shape and toolchain introduced by the plugin CLI/manifest work (referencing #1040 and the in-flight #1057), and rewrites the plugin docs set to match the new model for site operators and plugin authors.
Changes:
- Adds new migration guides for site operators and plugin authors covering the CLI rename and the new default-export + manifest-based authoring shape.
- Adds new reference pages for
emdash-plugin.jsoncand theemdash-pluginCLI, and updates existing “creating plugins” docs to use them. - Wires the new docs pages into the Starlight sidebar.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/src/content/docs/plugins/upgrading-sites.mdx | New site-operator migration guide for CLI rename + default export plugins. |
| docs/src/content/docs/plugins/creating-plugins/your-first-plugin.mdx | Rewrites tutorial to the new manifest + src/plugin.ts authoring shape and new CLI flow. |
| docs/src/content/docs/plugins/creating-plugins/storage.mdx | Updates storage docs to declare storage in the manifest and use the new runtime export shape. |
| docs/src/content/docs/plugins/creating-plugins/settings.mdx | Updates settings docs to new sandboxed authoring/runtime shape + manifest-declared admin pages. |
| docs/src/content/docs/plugins/creating-plugins/publishing.mdx | Rewrites publishing docs for Atmosphere identity model + hosted tarball publishing. |
| docs/src/content/docs/plugins/creating-plugins/migrating-to-the-cli.mdx | New plugin-author migration guide from definePlugin() to satisfies SandboxedPlugin + manifest/CLI build. |
| docs/src/content/docs/plugins/creating-plugins/manifest.mdx | New manifest reference page (identity/profile/trust contract/publisher pinning). |
| docs/src/content/docs/plugins/creating-plugins/hooks.mdx | Updates hook signature guidance to rely on satisfies SandboxedPlugin inference. |
| docs/src/content/docs/plugins/creating-plugins/cli.mdx | New CLI reference page for emdash-plugin commands and outputs. |
| docs/src/content/docs/plugins/creating-plugins/choosing-a-format.mdx | Updates sandboxed-vs-native comparison to the new authoring shape terminology. |
| docs/src/content/docs/plugins/creating-plugins/capabilities.mdx | Moves capability declarations to the manifest and updates validation guidance. |
| docs/src/content/docs/plugins/creating-plugins/block-kit.mdx | Updates Block Kit examples to the new sandboxed plugin export shape. |
| docs/src/content/docs/plugins/creating-plugins/api-routes.mdx | Updates route examples/signature notes for the new sandboxed plugin typing model. |
| docs/astro.config.mjs | Adds new docs pages to the plugins sidebar navigation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| </Steps> | ||
|
|
||
| `--validate-only` skips tarball creation but still produces the `dist/` artifacts — "validate" implies "build first". |
| ## Profile | ||
|
|
||
| These feed the registry listing. `license` and a security contact are required; the rest are optional. | ||
|
|
||
| | Field | Required | Notes | | ||
| | -------------------------- | -------- | ------------------------------------------------------------------ | | ||
| | `license` | Yes | SPDX expression (`"MIT"`, `"Apache-2.0"`, `"MIT OR Apache-2.0"`). Used on first publish; the existing profile wins on later publishes. | | ||
| | `author` / `authors` | Yes | One of the two. `author: { name, url?, email? }` for a single author; `authors: [...]` (≤ 32) for several. Setting both is an error. | | ||
| | `security` / `securityContacts` | Yes | One of the two. Each contact needs at least one of `email` or `url`. `securityContacts: [...]` (≤ 8) for several. Setting both is an error. | | ||
| | `name` | No | Display name. Defaults to the slug. | |
| ## Prerequisites | ||
|
|
||
| ## Bundle format | ||
| - A valid [`emdash-plugin.jsonc`](/plugins/creating-plugins/manifest/) with `slug`, `publisher`, `license`, and a security contact. Run `emdash-plugin validate` to confirm. |
|
|
||
| Replace the `definePlugin()` wrapper with a bare object and a `satisfies` annotation: | ||
|
|
||
| ```ts del={1} ins={2} "} satisfies SandboxedPlugin;" |
| "scripts": { | ||
| "build": "tsdown src/index.ts src/sandbox-entry.ts --format esm --dts --clean" | ||
| "build": "emdash-plugin build", | ||
| "dev": "emdash-plugin dev" | ||
| }, | ||
| "peerDependencies": { | ||
| "emdash": "*" | ||
| "emdash": ">=0.12.0" | ||
| }, | ||
| "devDependencies": { | ||
| "emdash": "*", | ||
| "tsdown": "^0.6.0", | ||
| "typescript": "^5.5.0" | ||
| "@emdash-cms/plugin-cli": ">=0.1.0", | ||
| "emdash": ">=0.12.0", | ||
| "typescript": "^5.9.0" |
| "emdash": "*", | ||
| "tsdown": "^0.6.0", | ||
| "typescript": "^5.5.0" | ||
| "@emdash-cms/plugin-cli": ">=0.1.0", |
Self-review of #1059 against the standards formalized since it was written. Removed definition-by-negation and bundle-internal framing from the guide pages (migration guides left as-is — comparison/changelog is their purpose): - 'You do not write a descriptor or a build script' -> dropped (the positive sentence already says what build does) - 'the build produces both, so you never hand-write either' -> 'generates both' - 'type-only, so the emdash runtime does not enter the plugin bundle' / 'the bundler erases them — no emdash runtime enters' -> 'provides only types, so a sandboxed plugin has no runtime dependency on emdash' - 'you never write it by hand' -> 'EmDash derives ... automatically' - 'init never requires extra flags to succeed' -> 'A slug is the only required input' - 'wire-side filename' -> '(the filename the registry expects)' - 'The registry never stores your plugin's code' lead -> dropped; the positive 'you host the tarball; registry stores a link' carries it Tropes scan clean; em-dashes appositive; bold bullets definitional.
* docs: add documentation style guide and bring all docs up to standard Adds a Documentation Style Guide (contributing/docs-style-guide) distilling Astro-grade conventions: readability, neutral voice (no we/us/let's), code-sample intros, Expressive Code diffs, the upgrade-guide breaking-changes format, headings, and EmDash specifics (sandboxed vs native, Atmosphere accounts, experimental surfaces, no messages.po in PRs). Audits and updates every other docs page against it. Excludes the pages in PR #1059 (plugin CLI/manifest/authoring docs). Also fixes real defects found during the audit: broken Astro frontmatter in three coming-from/wordpress code samples, two dead internal links (plugin-porting -> porting-plugins), an inconsistent field-type count, and competitor name-drops. * docs: separate 'how to use' from 'how it's built' Adds contributing/architecture (internals) and rewrites concepts/architecture, concepts/content-model, and concepts/admin-panel around what a reader decides and does. Relocates table layouts, the request path, code generation, admin internals, and the ImportSource extensibility API into the contributor doc rather than deleting them. Trims protocol/format internals from reference/mcp-server, migration/content-import, and guides/preview. Also corrects contributing/translating: AI-generated translations are accepted when a fluent speaker reviews every string and previews them in the running admin panel; only unsupervised machine output is rejected. * docs: cut recency bias, straw-man framing, and definition-by-negation Removes 'database-first/schema as data' framing from user-facing pages (it described an internal decision, not a user capability) and reframes around what the reader does. The mechanism stays only in the contributor architecture doc. Removes definition-by-negation ('no migration to write', 'no rebuild', 'without code') in favour of positive statements of what happens. Keeps honest, specific comparison only on the evaluation and 'coming from' orientation pages. Adds 'What to emphasise', 'Evergreen, not changelog', and a definition-by-negation rule to the style guide, and reorders its own 'EmDash specifics' so it models the principle rather than ranking by recency. Corrects the translating policy (supervised AI translations are accepted). * docs: remove AI tropes (negation, em-dash drama, builder-salience leaks) Sweep against the AI-tropes reference across all in-scope pages. Mostly definition-by-negation and straw-man self-definition still hiding in intros and benefit cards ('no PHP', 'no API round-trips', 'without rebuilds', 'no bundler, no manifest — just a regular npm package'), restated as what the reader does or gets. Also: dramatic-pause em-dashes split into sentences, prose pipeline arrows and a breadcrumb arrow de-unicoded, a Kysely reference dropped from the introduction diagram, and roadmap/changelog voice removed from the encryption-key and auth-secret deployment notes. Definitional bold lists, appositive em-dashes, UI breadcrumbs, code/diagram arrows, native plugin API code, and honest comparison on evaluation/coming-from pages were preserved. * docs: fix factual errors found auditing claims against source Verified each against packages/* source: - getSections returns { items, nextCursor }, not Section[] (sections guide, api ref) - removed fictional getSectionCategories / getSections({category}) from API ref - getEmDashEntry options param, getSiteSetting("title"), isPreviewRequest/getPreviewToken take URL, search() returns { items } (api ref) - content create flag is --draft (+ --locale/--translation-of), not --status (cli ref) - r2/d1 import from @emdash-cms/cloudflare not emdash/astro|emdash/db; emdashLoader() takes no args; fonts list missing farsi (config ref) - hook page:metadata link.rel allowlist + jsonld graph union (hooks ref) - field type count 16 not 14; added url/repeater rows; corrected reserved field slugs (field-types ref, collections) - REST: admin plugin paths, PUT not PATCH for collection/field/user updates, search response envelope, error codes (rest-api ref) - MCP: 45 tools not 43, role requirements, error-envelope shape (mcp ref) - media-library/architecture r2/local import paths; menus getMenus includes locale; widgets post.data.slug; admin-panel roles (no 'Developer' role) * docs: remove hallucinated APIs flagged in correctness audit Confirmed against source as non-existent (not planned, not drift-to-real): - auth config object { selfSignup, session, cloudflareAccess }: emdash({auth}) takes an adapter (access() from @emdash-cms/cloudflare). Rewrote the configuration + authentication auth sections to the real AuthDescriptor / access() surface with accurate AccessConfig options; removed the invented selfSignup/session config; pointed self-signup at the real provider allowlists. - reference/api.mdx: removed Database functions / Repositories / Schema registry sections (createDatabase is not a public export and there is no public Kysely handle) and the searchCollection example (wrong signature, needs a non-public db arg). Kept the verified-public search(). - guides/internationalization: removed the fictional 'emdash import wordpress --locale --translation-of-locale' (no import CLI command/flags); point to the real admin import flow + content create --locale --translation-of. - configuration.mdx: dropped package.json emdash.description / emdash.preview (not read by any code; label/seed/url are). * docs: address Copilot review on #1060 - installing.mdx: fix marketplace + config snippets — default import 'emdash from emdash/astro', add missing defineConfig import, sandboxRunner is a module specifier string not a boolean (verified: EmDashConfig.sandboxRunner?: string) - field-kit.mdx: integration imports from emdash/astro, not emdash - contributing/docs-style-guide.mdx: 'memorise' -> 'memorize' (US spelling) - themes/seed-files.mdx: correct the slug-validation summary (slug rules differ by type) instead of the misleading 'lowercase, underscores' Field-type count (field-types/collections, '14' -> '16') was already fixed in the earlier source-correctness pass; verified still correct. * docs: stop presenting EmDash as SQLite-only — PostgreSQL is a first-class adapter postgres() is exported from emdash/db alongside sqlite/libsql (verified in source); only deployment/database + reference/configuration mentioned it. Corrected the SQLite-only framing in the pages that enumerate supported databases: - introduction: 'SQLite-compatible databases (D1, libSQL, local SQLite)' / cloud-portable bullets / aside / diagram - why-emdash: Cloud-Portable card + Cloudflare-deployment intro - concepts/architecture: diagram + 'what you configure' bullet - contributing/architecture: Kysely supported-dialect list - coming-from/astro: two comparison-table cells - deployment/nodejs: note libSQL/PostgreSQL also work, link to Database Options Diagram lines re-padded to preserve box alignment. Opinionated SQLite examples (getting-started, the nodejs walkthrough body) left as-is — they demonstrate one choice, they don't claim it's the only one.
- Manifest profile prose lists author and security contact as required, matching the table and the actual ManifestSchema (.refine() rules in packages/plugin-cli/src/manifest/schema.ts require one of author/ authors and one of security/securityContacts). - Publishing prerequisites match the same required-fields story. - Bundle flag documentation uses kebab-case (--out-dir, --validate-only) to match the post-rename CLI (#1091). - your-first-plugin scaffolding example bumps emdash to >=0.13.0 (the first release exposing the emdash/plugin entrypoint) and pins @emdash-cms/plugin-cli to 0.2.0 (exact pin per the publishing page's experimental-registry guidance).
| @@ -7,21 +7,20 @@ import { Aside, Tabs, TabItem } from "@astrojs/starlight/components"; | |||
|
|
|||
| Plugins can expose API routes for their admin UI and external integrations. Routes are mounted under `/_emdash/api/plugins/<plugin-id>/<route-name>` and run inside the sandbox runtime with the same `PluginContext` that hooks receive. | |||
| interface SandboxedRouteContext<TInput = unknown> { | ||
| input: TInput; |
| Hooks let plugins run code in response to events. All hooks receive an event object and the plugin context, and they're declared at plugin definition time — there's no dynamic registration at runtime. | ||
|
|
||
| This page covers sandboxed (standard-format) plugins. Hooks work identically in native plugins; the only difference is that native plugins can also register `page:fragments`, which sandboxed plugins can't. | ||
| This page covers sandboxed plugins. Hooks work identically in native plugins; native plugins can additionally register `page:fragments`. |
There was a problem hiding this comment.
Thanks — but keeping the current framing here. The user-facing distinction in EmDash docs is native vs sandboxed (where the plugin is installed: plugins: [] vs sandboxed: [] in live.config.ts), not trusted vs sandboxed. We are deliberately moving away from the "trusted" terminology in user-facing docs because it is misleading — "trusted" describes a runtime mode, not something a plugin author chooses, and conflating it with the install location confuses readers more than it clarifies.
A sandboxed-format plugin that gets installed under plugins: [] is, from the docs perspective, a native plugin — so saying page:fragments is "native only" is accurate at the level docs operate at.
| 1. **Custom React admin pages or widgets.** Sandboxed plugins describe their admin UI with Block Kit — a JSON schema that the admin renders on the plugin's behalf. If you need full React (custom hooks, third-party components, complex state), you need native. | ||
| 2. **Astro components for rendering Portable Text blocks on the public site.** A plugin can declare a custom block type with `format: "standard"`, but the Astro components that render it on the public site have to be loaded at build time from npm. Only native plugins can provide a `componentsEntry`. | ||
| 2. **Astro components for rendering Portable Text blocks on the public site.** A sandboxed plugin can declare a custom block type, but the Astro components that render it on the public site have to be loaded at build time from npm. Only native plugins can provide a `componentsEntry`. | ||
| 3. **Injecting raw HTML, scripts, or stylesheets into public pages.** The `page:fragments` hook ships first-party code to visitors' browsers — outside any sandbox boundary. It's restricted to native plugins. Sandboxed plugins can still contribute to public pages through the `page:metadata` hook, which covers a lot of real use cases: | ||
|
|
There was a problem hiding this comment.
Thanks — but keeping the current framing here. The user-facing distinction in EmDash docs is native vs sandboxed (where the plugin is installed: plugins: [] vs sandboxed: [] in live.config.ts), not trusted vs sandboxed. We are deliberately moving away from the "trusted" terminology in user-facing docs because it is misleading — "trusted" describes a runtime mode, not something a plugin author chooses, and conflating it with the install location confuses readers more than it clarifies.
A sandboxed-format plugin that gets installed under plugins: [] is, from the docs perspective, a native plugin — so saying page:fragments is "native only" is accurate at the level docs operate at.
| | `network:request:unrestricted` | Outbound HTTP to any host. Used instead of `network:request`. | | ||
| | `hooks.email-transport:register` | Register an email transport hook. | | ||
| | `hooks.email-events:register` | Register email lifecycle hooks. | | ||
| | `hooks.page-fragments:register` | Register a `page:fragments` hook (native only). | |
There was a problem hiding this comment.
Thanks — but keeping the current framing here. The user-facing distinction in EmDash docs is native vs sandboxed (where the plugin is installed: plugins: [] vs sandboxed: [] in live.config.ts), not trusted vs sandboxed. We are deliberately moving away from the "trusted" terminology in user-facing docs because it is misleading — "trusted" describes a runtime mode, not something a plugin author chooses, and conflating it with the install location confuses readers more than it clarifies.
A sandboxed-format plugin that gets installed under plugins: [] is, from the docs perspective, a native plugin — so saying page:fragments is "native only" is accurate at the level docs operate at.
| emdash-plugin info <handle-or-did> <slug> Show package details | ||
| ``` | ||
|
|
||
| All commands accept `--json`. Discovery commands (`search`, `info`) accept `--aggregator <url>` (or `EMDASH_REGISTRY_URL`). |
- api-routes: clarify that the route URL segment is the plugin's slug (the same value as ctx.plugin.id at runtime) — previously phrased as <plugin-id> without explaining what that is. - api-routes: SandboxedRouteContext is non-generic in emdash/plugin (input is unknown; authors narrow with a route-level Zod schema). The earlier reference snippet showed a TInput generic that doesn't exist in the exported type. - cli: --json is supported by the non-interactive output commands (whoami, validate, search, info, login, publish), not all commands — logout, switch, init, build, dev, bundle do not define a json arg.
The CLI flag for overriding the discovery endpoint is being renamed from --aggregator to --registry-url in #1092, matching the EMDASH_REGISTRY_URL env var and the user-facing concept of a registry. Update the docs to use the new flag name and drop the user-facing mention of 'aggregator' in publishing.mdx in favour of 'registry'.
Catches deployment/suhail-ski up with emdash-cms/emdash main. Conflict in pnpm-lock.yaml resolved by taking upstream's lock and re-running pnpm install to pick up the @suhailski/57th-parallel template deps. Notable upstream changes folded in: - Plugin authoring CLI + manifest format (emdash-cms#1057, emdash-cms#1059) - Registry packages — lexicons, client, CLI (emdash-cms#923) - Migration 036 taxonomy preservation (emdash-cms#1086) - WXR import w/ WPML/Polylang translations (emdash-cms#1087) - TypeScript 6 upgrade, tsgo beta (emdash-cms#1074) - FTS5 corruption fix on publish (emdash-cms#768) - Plugin bundle size caps (emdash-cms#978) - And ~280 smaller fixes / chores CLI rebuilt against the new core. Verified by listing posts on suhail.ski — both tutorial posts come back as expected. 57th-parallel template untouched by upstream. Our 5 local commits preserved across the merge. Used --config.manage-package-manager-versions=false to bypass pnpm's self-switch to 11.1.3, which packages weirdly on Windows. The current shell's pnpm 10.12.4 worked fine against the merged manifests.
What does this PR do?
Documents the plugin CLI / manifest / authoring-shape changes from #1040 (merged) and #1057 (open).
plugins/upgrading-sites) —@emdash-cms/registry-cli→@emdash-cms/plugin-clirename, and the named-export → default-export change forplugin-audit-log,plugin-webhook-notifier,plugin-atproto.plugins/creating-plugins/migrating-to-the-cli) —definePlugin()→satisfies SandboxedPlugin, two-file →src/plugin.ts+emdash-plugin.jsonc,tsdown→emdash-plugin build/dev, type renames for sandbox-runner authors.emdash-plugin.jsoncmanifest (fields, trust contract, publisher pinning) and theemdash-pluginCLI (init/build/dev/validate/bundle/publish), plus a "Your Atmosphere account" section.your-first-plugin,publishing(now the Atmosphere/atproto model),storage,api-routes,settings,capabilities,block-kit,hooks,choosing-a-format. Native-plugin docs left untouched (unaffected by feat(plugin-cli): sandboxed plugin authoring CLI #1057).docs/astro.config.mjs.Both migration guides follow Astro's breaking-changes format (per-change entries with a "What should I do?" section and minimal Expressive Code diffs). Atmosphere account terminology is used throughout, cross-linked to the existing Atmosphere login guide and atmosphereaccount.com.
Note
This documents the open PR #1057. It should merge with or after #1057, since it describes that PR's behavior. #1040 is already merged.
Closes #
Type of change
Checklist
pnpm typecheckpasses — N/A, docs-only (content)pnpm lintpasses — N/A, docs-only (content)pnpm testpasses (or targeted tests for my change) — N/A, docs-onlypnpm formathas been run — N/A, oxfmt does not format.mdx; frontmatter/JSX/fence/link/anchor checks done manuallyAI-generated code disclosure
Screenshots / test output
Full
astro buildis blocked locally by a pre-existing Cloudflare env issue (AI_SEARCHbinding instance missing) unrelated to content. Validated instead: frontmatter, JSX tag balance, code-fence parity, internal links, and heading anchors across all touched pages.