Blog emdash
emdash

Six weeks evaluating EmDash for production work: what we learned before betting on it

For the agency owner who's read all the EmDash hype and wants the boring version: what we measured across six weeks of building, evaluating, and pitching EmDash work — before any client site went live. The bugs, the gaps, the things we'd wait on, and the things that already work.

A timeline of six violet circles with hash-mark activity clusters above and below each; two circles have a small violet x beside them indicating incidents.

A confession up front, because the rest of this post depends on it: we have not deployed an EmDash site to a paying client yet. This isn’t the “six weeks in production” retrospective the title might have promised in another timeline. It’s the version that’s actually true.

We’ve been building toward EmDash work since Cloudflare announced the project on April 1. We’ve installed the CMS, read the source, written a plugin against it, run WXR imports on de-identified client copies, and pitched the migration to prospects on intro calls. Six weeks of evaluator’s-eye-view work, with our consulting practice’s weight slowly leaning onto it. Here’s what we’ve learned, what we’ve filed, and what we’d still wait on before betting a client engagement on it.

TL;DR — what we learned in six weeks

  • We have not deployed an EmDash site to a paying client yet. Here’s what we learned anyway.
  • The plugin API is real. We built a working GA4/GTM/UTM injector against the page:fragments hook and a content-aware admin UI. The pattern holds.
  • WXR import handled our test fixtures (synthetic + de-identified) cleanly for posts and pages. Page-builder content (Elementor, Bricks) still needs a manual content-type pass — no surprise, but worth saying out loud.
  • The 33 MCP tools shipped with core are enough to drive a marketer-friendly editing flow with Claude. We tested it. It’s the most genuinely useful AI-CMS surface we’ve seen.
  • The marketer UX question isn’t answered yet. TinyMCE is a regression from Gutenberg, and we wouldn’t put a non-technical 50-person editorial team on it today.
  • We’d recommend EmDash for greenfield SaaS landing pages and small marketing sites today, with a developer in the loop. We’d wait another quarter for editorial-team rollouts.

What “evaluating” means (not the same as a client case study)

A real production retrospective has uptime numbers, a postmortem on the worst bug, and a marketer who’s been editing the site for two months. We don’t have those. What we have is six weeks of:

  • Building the OC WEBFIRM EmDash GA4/GTM/UTM plugin in production code (real artifact, not a demo).
  • Reading the EmDash core source on commit 47f511d (the 2026-04-24 snapshot we cloned), tracing the request flow, and filing a couple of small things upstream.
  • Running WXR imports on synthetic WordPress exports and on copies of two real client sites with PII stripped.
  • Eight intro calls with prospects who’d seen the Cloudflare announcement and wanted to know if it was real.
  • Tracking the BRIEF’s two stated open questions through the whole window: does the plugin API support our admin UI patterns, and does the marketer UX hold up?

This isn’t the same as deploying a paying client to it. We don’t pretend it is. But the empirical gap between “I read the docs” and “I shipped a plugin against it for six weeks” is wide enough to be worth writing about. If you’re an A1 agency owner trying to figure out if EmDash is real, the evaluator’s view is at least directionally useful. The deployment-retrospective version comes when the first client site has been live for sixty days.

If you want the more argumentative case for moving at all, the empirical follow-up to that argument lives here, and the practical comparison for marketing-first teams is here.

The plugin we actually built — the GA4/GTM/UTM injector

Every site we ship needs analytics. So the first plugin we wrote against EmDash was the one we’d need on day one of any client engagement: a GA4 + GTM tag injector with first-party UTM capture and a hidden-field helper for HubSpot and Typeform forms. Boring, well-scoped, every-client-needs-it.

The injection path is a hook called page:fragments. It fires during page render and lets a plugin push HTML into <head> or before </body>. The skill docs in templates/marketing/.agents/skills/creating-plugins/references/hooks.md literally use GTM as the worked example. That was a good sign — first-party docs that match the first thing you’d ever build.

Here’s the shape of the plugin’s hook registration, simplified from our actual code:

// plugin/ocwebfirm-ga4-gtm/src/index.ts
import { definePlugin } from 'emdash/plugin';
import type { PageFragmentsContext } from 'emdash/types';

export default definePlugin({
  name: 'ocwebfirm-ga4-gtm',
  version: '0.2.0',
  format: 'native', // page:fragments is trusted-only as of v0.7.0
  settings: {
    ga4MeasurementId: { type: 'string', label: 'GA4 Measurement ID' },
    gtmContainerId: { type: 'string', label: 'GTM Container ID' },
    utmCookieDays: { type: 'integer', default: 30 },
    debug: { type: 'boolean', default: false },
  },
  hooks: {
    'page:fragments': async (ctx: PageFragmentsContext) => {
      const { ga4MeasurementId, gtmContainerId } = await ctx.settings.get();
      return {
        head: renderTagSnippets({ ga4MeasurementId, gtmContainerId }),
        bodyEnd: renderUtmCaptureScript(ctx.settings),
      };
    },
  },
});

A few things worth saying about this. First, the definePlugin factory is typed end-to-end — settings show up in the admin UI auto-generated from the schema, which is the kind of thing you’d hand-roll three times in a WordPress plugin. Second, page:fragments is trusted-only in v0.7.0, which means it can’t go through the sandboxed emdash plugin publish pipeline. We accepted MIT-on-GitHub-only distribution for now. Cloudflare’s published intent is to widen the sandboxed surface over time, and we’ll resubmit when injection becomes safe-by-default.

What worked: the admin UI scaffold (settings-driven), the hook contract, the dev loop with hot reload, the test fixtures shipped in the scaffold, the sanity of the JSON output.

What didn’t work the first time: under bash on Windows, emdash dev shells out to npx which isn’t on PATH in our setup. Workaround in the Phase 1 install notes is node ./node_modules/astro/bin/astro.mjs dev --port 4321. Worked from PowerShell out of the box. A footgun, not a blocker.

WXR import on real-shaped data — what worked

We can’t migrate a client without confidence that their content actually moves. So we ran the WXR importer on three fixtures, in increasing realism:

FixtureSourceWhat workedWhat didn’t
Synthetic WP exportHand-crafted, 30 posts, 5 pages, 4 categoriesPosts, pages, categories, tags, featured images, author mappingNothing failed
Anonymized real site AA small services site, ACF custom fields, Yoast SEOPosts, pages, taxonomy. SEO meta moved with a 30-line transformACF flexible-content blocks needed a content-type rewrite (expected)
Anonymized real site BA landing-page-heavy site with Elementor sectionsPages imported as raw HTML; structured rebuild neededElementor’s serialized layout doesn’t map to portable text — we knew this

The honest read: WXR is a baseline-good importer for content that lives in real WordPress fields. It is not a magic migrator for page-builder content, and it shouldn’t be expected to be. The work for an Elementor or Bricks site is the same work we’d do for a Sanity or Payload migration: rebuild the content types, then move the prose and images through the importer, then rebuild the layout in EmDash blocks. We’ve quoted this work as a separate engagement on intro calls; it’s not a 90-minute push-button move and we don’t pretend it is.

For a smaller, structurally-clean WordPress site, the importer plus a half-day of content-type cleanup gets you ~80% of the way there. That matches our migration playbook’s risk list and didn’t surprise us.

Reading the EmDash codebase (and what we filed as issues)

We cloned emdash-cms/emdash to C:\Apps\emdash-upstream\ on April 24 and read enough of it to know what we were betting on. A few notes from that read:

  • MCP server surface. 33 tools, every one with a Zod input schema and a field description. The taxonomy and content tools cover the marketer flows we’d want Claude to drive. The schema tools (e.g. schema_create_field) are typed against the same field-type enum the seed parser uses. That kind of single-source-of-truth lines up with how the project is written end-to-end.
  • Plugin sandbox. Standard plugins run in Worker isolates with explicit permissions. The trade-off is that page:fragments and a handful of other hooks are trusted-only — you can’t publish them through the marketplace pipeline yet. For a CMS at v0.7.0 this is the right call. We’d rather have a strict sandbox by default than a loose one that breaks security later.
  • CLI ergonomics. The CLI outputs JSON for non-interactive flows and pretty text for humans. The emdash plugin init command is the right shape. We’d flag the bash/npx footgun on Windows but it’s not a blocker.

We filed two small things upstream — one a docs typo, one a missing field description on a content-type create call. Maintainer responsiveness over six weeks: actually responsive. Both got triaged within a week. That’s a more telling signal than the announcement post.

Marketer UX: the unanswered question

Here’s the part where we don’t yet have data. Our WordPress vs EmDash comparison post said marketer familiarity with the WordPress editor is a structural advantage that takes years to close. Six weeks in, we still believe that.

EmDash ships TinyMCE as the rich-text editor. For copy editors used to Gutenberg’s block model, this feels like a regression. Block-level structure exists in EmDash — every content type is a typed JSON object — but the editing affordance for an in-paragraph rewrite is back to a contentEditable surface that’s been around since 2010.

The Cloudflare team has signalled that a richer editor is on the roadmap. We believe them. We don’t know when. Our position on intro calls has been: if your editorial team is two or three marketers comfortable with structured content, you’re fine today. If it’s a 50-person org used to drag-and-drop on a page builder, wait another quarter or two. We wouldn’t migrate that team yet.

This is the part of the BRIEF’s open questions list (§8) we still can’t close out. We’ll re-evaluate when v0.8 ships.

The MCP-with-Claude reality at six weeks

The most-hyped feature of EmDash is the built-in MCP server. We wired Claude Desktop to it via Streamable HTTP transport with a session cookie and ran a series of small test cases:

  • “Add a new feature card to the homepage with the headline ‘Imports work, mostly’.” → worked.
  • “Translate the hero subhead to Spanish and create a Spanish version of the page.” → worked (taxonomy + clone).
  • “Find every page tagged ‘pricing’ and update the CTA copy to say ‘Book a call’ instead of ‘Get started’.” → worked.
  • “Roll back the homepage hero to the version from yesterday.” → worked, via the revision tools.

For the kind of marketer-driven copy/structure edits we’re betting the consulting practice on, this is real. Not a demo. Real enough that we recorded a 60-second screencast of it for prospects, and the response on calls has been notably different from the response to the same flow described in slides.

Our remaining concerns are about session lifecycle (passkey auth makes the demo flow a little awkward for first-time prospects) and about MCP transport stability across long sessions. Six weeks isn’t enough to surface those at scale. We’ll know more at twelve.

Bugs that would have killed us at scale

We didn’t run an EmDash site at production traffic. So this section is honest about what we didn’t test. The things we’d want to see before shipping a client:

  • D1 cold-start latency under spiky traffic. SQLite-on-D1 is fast at steady state. We haven’t profiled it under a flash sale.
  • R2 image transformation pipeline behaviour on a site with thousands of media items. Joost de Valk’s coverage of EmDash flagged this as an area he’d want to see proved out. We agree.
  • Plugin-collision behaviour when you have five trusted plugins all hooking page:fragments. The order is documented (registration order, with priority field), but we haven’t watched it break under five real plugins. We’ve watched it work under two.
  • Long-running migrations on the WXR importer for a 5,000-post site. Our biggest test fixture was ~120 posts.

If any of these turn out to be a problem at scale, they’d kill us in a paying engagement and we’d have to roll back. We’re not going to find out without running a real client. But the honest version of “before we’d bet a client on it” is “we’d want pilot traffic on a non-critical site first.”

What we’d advise an agency to do today

Six weeks in, here’s the recommendation we’re giving on intro calls.

Yes today, with hands-on attention:

  • Greenfield SaaS or services landing pages. Small site, structured content, marketing-first goals.
  • A second site for an existing WordPress client — a microsite, a campaign page, a product launch page. Low risk, high learning value.
  • Internal tools, marketing handbooks, anything where you control the editorial team.

Wait at least one more release for:

  • 50+ person editorial teams used to Gutenberg drag-and-drop.
  • E-commerce sites that need WooCommerce-class plugin coverage.
  • Sites with complex membership/paywall logic.
  • Multilingual sites with five or more locales (i18n is on the roadmap; not shipped).

Do regardless:

  • Build content schemas portably. Astro components and structured content port out of EmDash to Sanity, Payload, or Decap if the bet doesn’t pay off. The BRIEF’s hedge (§4) holds.
  • Watch the GitHub release cadence. Cloudflare’s been shipping every two-to-three weeks. That cadence either continues or it doesn’t — both will be visible by month four.
  • Talk to the EmDash community on the Cloudflare Workers Discord. The signal-to-noise on “is this real” questions is genuinely good there.

This matches the stance we ran in our migration playbook, which lists EmDash as a real option for the right shape of site, not a panacea.

For agency owners who want a senior pair of hands during the evaluation — to help write the test plan, run the WXR import on a real export, or build a small custom plugin against the API — the optimization retainer is the right starting point. Half a day per week is enough to de-risk the decision without committing to a full migration. If you’ve already decided to migrate and want execution, migration consulting handles the four-week sequence end-to-end. If you need a custom plugin for analytics, schema, or a workflow integration, custom plugin work is scoped to a fixed deliverable.

FAQ

Why publish this without client deployments?

Because the alternative is silence for another sixty days while a real case study cooks, and the prospects we’ve been talking to want a credible answer now. The honest version of “what we know at six weeks” is more useful than the breathless version of “what we hope is true.” When the first paying client is live for sixty days, we’ll publish that retrospective separately. The audience for “is this evaluator-real” is genuinely different from the audience for “is this client-deployed-real.”

When will you publish a real case study?

Conservatively, end of Q3 2026. We’d want a client site live for at least sixty days, a marketer who’s been editing it weekly, and a Lighthouse-and-uptime data set we can show without anonymizing past usefulness. If we get one on the calendar this quarter, that timeline pulls in. We won’t publish a faked one.

Does this change the migration recommendation in your earlier posts?

No. Our original argument and our comparison post both said EmDash is a real option for the right shape of site, not a universal replacement, and that the 30–40% adoption odds we estimated were a real bet. Six weeks of hands-on work hasn’t moved that estimate up or down meaningfully. The work has confirmed the shape of the bet, not the size of it.

What would actually change your mind in either direction?

A v0.8 release that ships a Gutenberg-class block editor, or a meaningful enterprise customer announcement from Cloudflare, would push us toward a more aggressive recommendation. A six-month gap with no releases, or a security incident in the plugin sandbox, would push us toward “wait another quarter and re-evaluate.” Both are knowable from public signals.

Is the GA4/GTM plugin you built open source?

The core injection is MIT and on GitHub. Because page:fragments is trusted-only as of v0.7.0, the publishable-marketplace version isn’t there yet. When the sandboxed surface widens, we’ll split it into a publishable standard core plus a small trusted adapter and ship both. Until then, GitHub-only with copy-paste install instructions in the README.

What we’re tracking next

The honest closing: we’re going to keep building, keep evaluating, and write the actual production retrospective when we have one. The post you’d hoped to read here — the one with two clients live for sixty days — is in the calendar. It’s not in the archive yet.

If you’re an agency owner trying to make the same call we’re making, the ports-of-entry haven’t changed. Build small, build greenfield, keep your content schemas portable, and watch the releases. EmDash is more real than the obituary writers think and less polished than the launch announcement implies. Both can be true. Both are.

Sources we relied on for this evaluation, beyond our own work:

Need help applying any of this?

We do this for clients every week. 30 minutes, no obligation.