---
title: "Build Your First EmDash Plugin: Developer Guide"
description: "How to build an EmDash plugin with hooks, storage, admin UI, and custom Portable Text blocks. From definePlugin() to a working plugin."
article_type: sub-pillar
canonical: https://dashstro.com/learn/emdash-plugin-development
---
:::bluf
EmDash plugins run in sandboxed Worker isolates with explicit capability manifests. Define your plugin with `definePlugin()`, declare what APIs it needs, add hooks/storage/admin pages/block types, register it in `astro.config.mjs`. The ecosystem is empty at v0.1 — every category is an opportunity. The first quality plugin for analytics, contact forms, or SEO auditing owns that category for years.
:::

EmDash's plugin system is architecturally impressive but the ecosystem is empty. That's an opportunity. This guide walks through building a plugin from scratch — from understanding the sandboxed runtime to shipping a working plugin with hooks, storage, and admin UI.

## How EmDash plugins work

EmDash plugins run in sandboxed Worker isolates, separate from your main application. Each plugin declares capabilities in a manifest — what data it can access, what events it listens to, what APIs it needs. The runtime enforces these declarations at the platform level, not by convention.

This means a plugin that declares "read content" literally cannot access user data, media storage, or anything outside its declared scope. Compare this to WordPress where every plugin has full access to your database, filesystem, and server environment.

## Plugin anatomy

A plugin is a function that returns a configuration object via definePlugin(). It declares:

- Capabilities — what the plugin needs access to (content, media, storage, email)
- Hooks — functions that fire on content lifecycle events (beforePublish, afterCreate, etc.)
- Storage — key-value store for plugin data, scoped to the plugin
- Settings — configuration fields that appear in the admin panel
- Admin pages — custom pages in the admin panel for plugin-specific UI
- API routes — custom endpoints for webhooks, external integrations, etc.
- Block types — custom Portable Text blocks for the content editor

You register your plugin in astro.config.mjs alongside the emdash() integration.

| Hooks | React to content lifecycle events | afterPublish, beforeCreate, onDelete |
| --- | --- | --- |
| Storage | Scoped key-value store for plugin data | store.get('analytics'), store.set('count', n) |
| Settings | Admin-editable config fields for your plugin | API key field, toggle, select option |
| Admin pages | Custom pages injected into the admin nav | Analytics dashboard, form submission log |
| API routes | Custom HTTP endpoints for external calls | Webhook receiver, form POST handler |
| Block types | Custom Portable Text blocks in the editor | CTA, comparison table, embedded map |

## Building a simple plugin

Let's build a plugin that logs content publish events and provides a dashboard showing recent publications. This demonstrates hooks, storage, and admin UI — the three most common plugin patterns.

Start by creating a plugin file. Export a function that calls definePlugin() with your plugin name, version, capabilities, and hooks. The afterPublish hook receives the published entry — store the entry title, collection, and timestamp in the plugin's key-value storage.

For the admin page, define a React component that reads from storage and renders a list of recent publications. EmDash injects your admin pages into the admin panel navigation automatically.

## Custom Portable Text blocks

Plugins can add custom block types to the Portable Text editor. This lets editors insert structured content — CTAs, feature grids, comparison tables, embedded maps — that your templates render with custom components.

Define a block type with a schema (the fields editors fill in) and a renderer (the Astro component that renders it on the frontend). EmDash adds the block to the editor's insert menu and handles serialization automatically.

## Plugin distribution

EmDash will have a plugin marketplace, but it's not live yet. For now, plugins are distributed as npm packages or Git repositories. Users install them as dependencies and register them in astro.config.mjs.

When the marketplace launches, early plugins with established users will have a significant head start. If you build something useful now, you're positioning it for discovery when the ecosystem grows.

## Plugin ideas with no competition

Every category is empty. Here are the highest-value plugins that don't exist yet:

:::pullquote
Every plugin category is empty. The first quality plugin for analytics, contact forms, or SEO auditing will own that category for years.
:::

::::columns

:::column
### Content & growth

- Analytics — privacy-first page view tracking using D1 storage. No third-party scripts, no GDPR headaches.
- Newsletter integration — connect to Buttondown, ConvertKit, or Mailchimp from publish hooks.
- Social sharing — Open Graph image generation at the edge and share buttons as a custom block type.
:::

:::column
### Utility & operations

- Contact forms — form builder with email notifications, spam protection, and a submissions log in the admin.
- SEO auditing — content analysis and optimization suggestions surfaced inline in the editor.
- Backup/export — scheduled content exports to R2 or external storage via a Workers cron trigger.
:::

::::

Build any of these well and you'll own the category. That's the advantage of building in a new ecosystem.

## Frequently asked questions

::::faq

:::q
How do I publish my plugin for others to install?
:::
:::a
Publish it as an npm package or link it as a Git repository. Users install it as a dependency and register it in `astro.config.mjs`. EmDash's marketplace isn't live yet — plugins are distributed manually in v0.1.
:::

:::q
Can my plugin access other plugins' data?
:::
:::a
No. Each plugin is isolated in its own Worker isolate with its own storage. There's no cross-plugin API. Plugins that need to coordinate do so through the content or event system.
:::

:::q
Can plugins run without sandboxing?
:::
:::a
Yes — plugins registered directly in `astro.config.mjs` run unsandboxed, which is useful for projects where you control all the code. Marketplace plugins must be sandboxed and require the paid `worker_loaders` binding on Cloudflare.
:::

:::q
What's the best plugin to build right now?
:::
:::a
Anything in a category that doesn't exist. Analytics, contact forms, SEO auditing, backups, and newsletter integrations are all empty. The first quality implementation of each owns the category.
:::

::::

[Learn about the MCP server →](/learn/emdash-mcp-server)
