Why Build an EmDash Plugin?

EmDash plugins let you extend your CMS with custom functionality without modifying core code. Whether you need custom SEO meta fields, AI-generated content summaries, automated image optimization, or a completely new content type — plugins give you a sandboxed environment to build it in.

Unlike traditional CMS plugin systems (WordPress, Drupal) that run PHP on the server, EmDash plugins run on Cloudflare Workers. This means they:

- Scale to zero when not in use (no infrastructure cost)

- Can access Cloudflare's edge network (KV, D1, R2, Queues)

- Are sandboxed from each other (no plugin can crash your site)

- Deploy independently (update a plugin without touching your CMS)

Prerequisites

Before starting, make sure you have:

```bash

An EmDash site (v0.8+) running on Cloudflare Pages

npx emdash --version

wrangler CLI with D1 access

npx wrangler --version

Cloudflare account with Workers and D1 enabled

```

Step 1: Scaffold Your Plugin

Every plugin lives in `plugins/<name>/` at your project root. EmDash provides a scaffold command:

```bash

npx emdash create-plugin my-custom-seo

```

This generates:

```

plugins/my-custom-seo/

src/

index.ts # Main plugin entry

plugin.json # Plugin manifest

package.json

tsconfig.json

```

Step 2: Define Your Plugin Manifest

The `plugin.json` file tells EmDash about your plugin:

```json

{

"name": "my-custom-seo",

"version": "1.0.0",

"displayName": "My Custom SEO",

"description": "Adds advanced SEO fields to your posts",

"hooks": ["onAdminRoute", "onContentRender"],

"permissions": ["storage:read", "storage:write"]

}

```

Key fields:

- **hooks**: Which lifecycle events your plugin listens to

- **permissions**: What resources your plugin can access (storage, KV, D1)

Step 3: Add Admin UI Fields

Plugins can add custom fields to the admin editor. Here's how to add SEO meta fields to the post editor:

```typescript

import { definePlugin } from "emdash/plugin";

export default definePlugin({

name: "my-custom-seo",

hooks: {

onAdminRoute: {

handler: async ({ route, form }) => {

if (route.collection === "posts" && route.action === "edit") {

form.addField({

name: "seoTitle",

label: "Custom SEO Title",

type: "text",

placeholder: "Override the default title tag"

});

form.addField({

name: "seoDescription",

label: "Custom SEO Description",

type: "textarea",

placeholder: "Override the default meta description"

});

}

}

}

}

});

```

Step 4: Store Plugin Data

Plugins get isolated storage via KV. No need to worry about key collisions with other plugins:

```typescript

async onContentSave({ entry, storage }) {

// Save SEO data when the user publishes

await storage.set(`seo:${entry.slug}`, JSON.stringify({

title: entry.data.seoTitle,

description: entry.data.seoDescription

}));

}

```

Step 5: Render Custom Content

The `onContentRender` hook lets you transform how content appears on the frontend:

```typescript

onContentRender: {

handler: async ({ content, storage, entry }) => {

const seoData = await storage.get(`seo:${entry.slug}`);

if (seoData) {

const parsed = JSON.parse(seoData);

// Inject meta tags into the page head

content.head.push({

tag: "meta",

attrs: {

name: "description",

content: parsed.description

}

});

}

return content;

}

}

```

Step 6: Deploy Your Plugin

Deploying is a single command:

```bash

npx emdash deploy-plugin my-custom-seo

```

This bundles your plugin, uploads it to Cloudflare Workers, and registers it with your EmDash site. The admin UI picks it up automatically — no restart needed.

Plugin Architecture Tips

Here are patterns we've found valuable from building plugins for our own EmDash sites:

Use KV for Plugin Settings

Every plugin gets its own KV namespace. Store user-facing settings (API keys, toggles, thresholds) in KV, not in the code:

```typescript

const apiKey = await storage.get("settings:apiKey");

const enabled = await storage.get("settings:enabled");

```

Cache Expensive Operations

Workers have a 10ms CPU time budget per request. Cache database queries and API calls:

```typescript

const cached = await caches.default.match(request);

if (cached) return cached;

```

Handle Errors Gracefully

A crashing plugin should never take down the page. Wrap your handlers:

```typescript

try {

// Your plugin logic

} catch (error) {

console.error("Plugin error:", error);

// Return unmodified content

return content;

}

```

Going Further

Once you've built your first plugin, try these next steps:

1. **Add plugin settings UI** — Let users configure your plugin from the admin panel

2. **Use D1 for relational data** — Create custom tables for your plugin's data model

3. **Register API routes** — Expose custom endpoints under `/_emdash/api/plugins/your-plugin/`

4. **Publish to the marketplace** — Share your plugin with other EmDash users

Conclusion

EmDash's plugin system gives you WordPress-level extensibility with serverless-scale infrastructure. Your first plugin takes about 30 minutes to build and deploy. The sandboxed architecture means you can experiment freely without risking your production site.

Start with something small — a custom field, a content filter, or an SEO helper — and iterate from there. Every plugin you build expands what your CMS can do.