The Starting Point

April 2026. My goal: a modern, fast, AI-native website for an automation magazine called Automated Web – as cheap and low-maintenance as possible.

I chose EmDash, Cloudflare's open-source CMS launched on April 1, 2026, positioned as the spiritual successor to WordPress. What followed was 75 minutes of productive collaboration with Claude, real errors, and API quirks – ending with a live website for around $6 per month. This article documents everything.

The Real Cost Breakdown

Component Cost/Month Notes
Domain automatedweb.net ~$1 $11.86/year at Cloudflare
Workers Paid Plan $5 Required – Free Plan is not enough
R2 Storage (up to 10 GB) $0 Free, but a credit card is required
D1 Database $0 Included
SSL Certificate $0 Automatic
Global CDN $0 Cloudflare network worldwide
Total ~$6 WordPress hosting: $15–50/month

Step 1: Buy the Domain

Register directly at Cloudflare Registrar – domain and Workers hosting in the same dashboard. No DNS configuration, no nameserver transfers.

Step 2: Install EmDash

npm create emdash@latest

The setup wizard walks you through: project name → Cloudflare Workers (D1 + R2)Starter template → npm. About 2 minutes total.

⚠️ Pitfall #1: You must explicitly type "y"

npm asks: "Ok to proceed? (y)". Pressing Enter without typing y results in npm error canceled.

Step 3: Test Locally

cd automatedweb && npm run dev

Admin panel at localhost:4321/_emdash/admin. Set a title, create a passkey (no password required), and enable sample content.

Step 4: Prepare Cloudflare

⚠️ Pitfall #2: worker_loader requires the Paid Plan – no workaround

X [ERROR] binding LOADER of type worker_loader is invalid [code: 10021]

Upgrade to the Workers Paid Plan before deploying. There is no free-tier workaround.

Step 5: Deploy

npm run deploy

⚠️ Pitfall #3: KV Namespace already exists [code: 10014]

Fix: Cloudflare Dashboard → Workers & Pages → KV → delete the existing namespace → re-run deploy.

⚠️ Pitfall #4: wrangler.jsonc is ignored during provisioning

Delete the namespace and let Wrangler recreate it from scratch.

Step 6: Connect Custom Domain

Cloudflare Dashboard → Workers & Pages → Settings → Custom Domains → Add. The domain goes live immediately.

⚠️ Pitfall #5: Production database is empty – run setup twice

Local and production are completely separate databases. Run setup twice: once locally and once on the live site.

Step 7: AI Integration via MCP

  1. Open the Admin on the live URL (not localhost)
  2. Settings → API Tokens → create a new token
  3. Edit Claude Desktop config: %APPDATA%\\Claude\\claude_desktop_config.json
  4. Add EmDash as an MCP server and restart Claude Desktop

⚠️ Pitfall #6: API token must be created on the live URL

A token created at localhost:4321 only works locally. On the live site it returns INVALID_TOKEN.

⚠️ Pitfall #7: EmDash API structure differs from REST conventions

Correct path: /_emdash/api/content/posts. A data wrapper is required for POST requests:

// Correct – 201 Created:
{ data: { title: "Post", content: "..." } }

// Wrong – 400 VALIDATION_ERROR:
{ title: "Post", content: "..." }

⚠️ Pitfall #8: PATCH returns 404 – use PUT

Use PUT for updates and POST /:id/publish to publish. Slugs cannot be changed after publishing.

⚠️ Pitfall #9: npm run deploy does NOT rebuild

Always run both commands:

npm run build && npm run deploy

⚠️ Pitfall #10: Media Upload – R2, CSRF, and the Right Path

The POST /media/upload-url endpoint returns NOT_SUPPORTED. Three-step fix:

# 1. Enable R2 Public Access:
npx wrangler r2 bucket dev-url enable my-emdash-media

# 2. Add publicUrl to astro.config.mjs:
storage: r2({ binding: "MEDIA", publicUrl: "https://pub-XXXXX.r2.dev" })

# 3. Rebuild and deploy:
npm run build && npm run deploy

External POST requests need the CSRF bypass header:

headers: {
  "Authorization": "Bearer TOKEN",
  "X-EmDash-Request": "1",
  ...form.getHeaders()
}

⚠️ Pitfall #11: Astro's built-in CSRF check blocks multipart/form-data

The 403 error comes from Astro's origin check in node_modules/astro/dist/core/app/middlewares.js – not from EmDash itself.

// After: if (SAFE_METHODS.includes(request.method)) { return next(); }
const authHeader = request.headers.get("authorization") || "";
const isApiRoute = url.pathname.startsWith("/_emdash/api/");
if (isApiRoute && authHeader.startsWith("Bearer ec_pat_")) {
  return next();
}

⚠️ This patch is lost on npm install – use the MCP tool apply_astro_patch to reapply it automatically.

⚠️ Pitfall #12: EmDash Pages don't store HTML – PortableText conversion

EmDash automatically converts every PUT request with HTML into PortableText blocks. The HTML is gone after saving – this is a deliberate design decision, not a bug.

The solution: A static Astro template. Write the page directly in src/pages/ for full HTML control with no CMS overhead.

⚠️ Pitfall #13: Post cards on the homepage linked to nowhere

Bug 1 – post.id instead of post.slug: Using post.id (a ULID like 01KN7DX6...) as the URL results in a 404.

Bug 2 – EmDash <Image> component in Collections: Fails with relative paths in collection views.

// Wrong:
<a href={`/posts/${post.id}`}>
  <Image image={post.data.featured_image} />

// Correct:
<a href={`/en/posts/${post.slug}`}>
  <img src={post.data.featured_image.url} alt={post.data.featured_image.alt} />

My Recommendations

After working through all thirteen pitfalls, a few decisions stand out as especially worth getting right from the start.

Register your domain directly at Cloudflare – it eliminates all DNS configuration and keeps everything in one dashboard. Activate the Workers Paid Plan before your first deploy; it is required and there is no workaround. The same logic applies to R2: activate it before you deploy, not after.

When things go wrong, the most common fixes are simpler than they look. KV error 10014 means delete the namespace and re-deploy. A 404 on your API token almost always means it was created on localhost instead of the live URL. Wrapped POST bodies ({ data: { title, content } } instead of flat JSON) and PUT instead of PATCH for updates are the two API patterns that catch most people off guard.

For content management, static Astro pages in src/pages/ give you full HTML control and are more predictable than CMS Pages for anything complex. In card templates, always use post.slug as the URL parameter – never post.id. And whenever you change templates, always run npm run build before npm run deploy, not just the deploy command alone.

FAQ

Do I need programming skills?

Basic terminal skills are enough. Copy and run commands – no need to write real code.

Why does the MCP server return 404 even though the token is correct?

Wrong API path or missing data wrapper. Correct path: /_emdash/api/content/posts.

Can I migrate from WordPress?

EmDash supports WordPress WXR export import. Custom post types require manual work.

What if my site goes viral?

Workers Paid includes 10 million requests per month, then $0.30 per million. Scaling is automatic.

Is EmDash production-ready?

Launched in April 2026 – great for new projects. For migrating critical business sites, waiting another 6–12 months is reasonable.

Conclusion

75 minutes. $6 per month. A modern, AI-native, globally hosted website with a built-in MCP server, passkey authentication, and automatic scaling – no dedicated server required.

This article was published by Claude directly via MCP – after debugging all thirteen pitfalls together.

Hendrik Muth

About the Author

Hendrik Muth

SEO & AI enthusiast from Munich. I experiment with AI tools, automation workflows – and build websites like this one in under 75 minutes.

More about me →

Found another pitfall? Let me know – I update this list as new ones come up.