I shipped eight EmDash plugins in one session.
Not prototypes.
Not stubs.
Working, tested, event-driven plugins with admin settings, dashboard widgets, storage, and API routes.
Every one of them runs in production on this blog right now.
This post is what I learned about building fast when the framework handles infrastructure and you only have to think about behavior.
The plugin architecture that makes this possible
EmDash's definePlugin() gives you five primitives:
Storage — per-plugin document collections with indexes. Not key-value — full queryable collections with cursor pagination.
KV — simple key-value for settings and state. Persists across deploys.
Hooks — lifecycle events like content:afterSave, plugin:install, cron. Your code runs in response to things happening, not on a schedule you manage.
Routes — plugin-scoped API endpoints. The framework handles auth, CORS, and request parsing.
Admin UI — settings schemas, dashboard widgets, and admin pages. You declare the schema; EmDash renders the UI.
The key insight: none of these require you to think about databases, authentication, deployment, or infrastructure. You write behavior. The framework provides everything else.
What I built
1. Agent Analytics
Classifies every request by user-agent against 15 known AI crawler patterns. Records per-hit and daily aggregates in plugin storage. Dashboard widget shows agent vs human traffic percentage.
The core is one pure function — classifyUserAgent(ua) — that returns { isAgent, name, operator }. Everything else is wiring.
2. Tip Jar
An x402 tip button on every post.
Visitors send $0.
25 USDC with one click.
The payment settles on Base mainnet to my hardware wallet.
The plugin records tips in storage and shows total revenue on the dashboard.
The entire payment flow is EmDash's native x402.enforce() — I didn't write any payment code.
3. MiniMax TTS Narration (Event-Driven)
When a post is published, the content:afterSave hook checks if narration exists.
If not, it generates audio via MiniMax's speech-2.
8-hd model, uploads to R2, and records the narration in plugin storage.
If quota is exhausted, it marks the item as quota-blocked with a 5-hour retry window and a cron handler processes the queue later.
The narration is auto-discovered by the AudioPlayer component on the client — no manual wiring per post.
4. MiniMax Hero Image (Event-Driven)
Same pattern as TTS: content:afterSave fires on publish, checks if a hero image exists, generates one via MiniMax image-01 with configurable style presets (editorial-minimal, tech-dark, hand-drawn, photo-reportage, abstract-geometric).
Five style presets, each expanding to a paragraph-level prompt with composition directives. The output is a 16:9 JPEG that goes straight to R2.
5. Agent SEO
Generates /robots.
txt with explicit allow rules for 16 AI crawlers, /llms.
txt per the llmstxt.
org spec, and Organization JSON-LD.
The bot catalog is versioned data — adding a new crawler is one line in a TypeScript array.
6. PostHog Analytics
Snippet builder with admin-path auto-exclusion and Do Not Track respect. The snippet only fires on public pages; the admin panel never gets tracked.
7. Related Posts
Pure-functional tag-overlap algorithm. Takes a post's tags and all posts' tags, returns the top N related posts ranked by shared tag count. The plugin stores the graph; the template renders the component.
8. Forms (EmDash First-Party)
The one I didn't build — EmDash ships it. Contact forms, submissions, Turnstile spam protection, email notifications.
What makes this fast
Three things made it possible to ship eight plugins in one session:
No boilerplate. definePlugin() handles registration, storage provisioning, settings persistence, admin UI rendering, and route mounting. The smallest useful plugin is about 20 lines.
Pure functional cores.
Every plugin has a pure function at its center — classifyUserAgent(), buildRobotsTxt(), findRelatedPosts(), buildPostImagePrompt(), portableTextToNarration().
These are trivial to test (64 unit tests, all passing in under a second) and trivial to reuse outside the plugin context.
Event-driven by default.
The content:afterSave hook means I never had to build admin buttons, cron schedules, or manual trigger endpoints for the common case.
Publish a post and things happen.
Quota runs out and things retry later.
The plugins are autonomous.
The numbers
8 plugins, all running in production
64 unit tests, 0 failures
7 bridge routes for MiniMax API (image, TTS, music, video, video-status, video-download, voices)
5 MiniMax-generated hero images
1 ambient music track (music-2.5)
1 cinematic hero video (Hailuo-2.3)
1 AI narration with auto-discovery AudioPlayer
All plugins MIT licensed, structured for npm extraction
Zero npm packages published (yet)
What's next
Every plugin extracts cleanly as a standalone npm package.
The plan is to publish them individually, write a companion blog post for each, and sell the skills that teach people how to build their own.
The blog is the storefront; the plugins are the product; x402 is the revenue layer.
If you're building on EmDash and want to see how any of these work, the source is at github.com/integrate-your-mind/mondello-dev. Pull requests welcome.