pub-d1d7f5f57ea747fcb4c0d5bae0b1d227.r2.dev).
Language: 🇩🇪 Deutsche Version | 🇬🇧 English
This article is a complete, honest account – including every error, debugging session, and API discovery we only found through real trial and error. Nothing is glossed over.
The Starting Point
April 2026. An AI assistant (Claude), a human with a domain idea, and a goal: a modern, fast, AI-native website for an automation magazine called Automated Web – as cheap and low-maintenance as possible.
We chose EmDash, Cloudflare's new open-source CMS launched on April 1, 2026 as the "spiritual successor to WordPress". The promise: serverless, secure, AI-native, deployed on Cloudflare Workers without your own server.
What followed: 75 minutes of productive collaboration, five real errors, and two unexpected 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 not enough |
| R2 Storage (up to 10 GB) | $0 | Free, but credit card 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 config, no nameserver transfers, no propagation delays. The .com was only available with a hyphen (automated-web.com) – a branding no-go. automatedweb.net is clean, tech-affine, and works equally well in German and English.
Step 2: Install EmDash
node --version
# v22.22.0
npm create emdash@latest
Setup wizard choices: project name → Cloudflare Workers (D1 + R2) → Starter template → npm. About 2 minutes total.
⚠️ Pitfall #1: You must explicitly type "y"
npm asks: "Need to install create-emdash@0.1.0 – Ok to proceed? (y)". Pressing Enter without typing y → npm error canceled. Run again, type y explicitly.
Step 3: Test Locally
cd automatedweb
npm run dev
Admin panel at http://localhost:4321/_emdash/admin. Set title, create passkey (no password!), enable sample content for immediate layout preview.
Step 4: Prepare Cloudflare
4a: Activate R2
R2 must be activated once in the Cloudflare dashboard even though the first 10 GB are free. Credit card required for verification.
⚠️ Pitfall #2: worker_loader requires the Paid Plan – no workaround
EmDash uses a worker_loader binding for plugin sandboxing that is only available in the Workers Paid Plan ($5/month). With the free plan:
X [ERROR] binding LOADER of type worker_loader is invalid [code: 10021]
No workaround exists. The Paid Plan is mandatory for any live EmDash installation.
Step 5: Deploy
npm run deploy
First deploy opens Cloudflare OAuth in the browser. After confirming, everything runs automatically.
⚠️ Pitfall #3: KV Namespace already exists [code: 10014]
If a previous failed attempt already created resources:
X [ERROR] a namespace with this account ID and title already exists [code: 10014]
Fix: Cloudflare Dashboard → Workers & Pages → KV → delete namespace my-emdash-site-session → re-run npm run deploy.
⚠️ Pitfall #4: wrangler.jsonc is ignored during provisioning
Manually adding the KV namespace ID to wrangler.jsonc has no effect. npm run deploy uses --experimental-provision internally which overrides the config. Only fix: delete the namespace and let Wrangler recreate it.
🎉 All resources provisioned, continuing with deployment...
Worker Startup Time: 128 ms
Uploaded my-emdash-site (24.67 sec)
Deployed my-emdash-site triggers (6.41 sec)
https://my-emdash-site.deinname.workers.dev
Step 6: Connect Custom Domain
Cloudflare Dashboard → Workers & Pages → project → Settings → Custom Domains → Add. Since domain and hosting are both at Cloudflare: active immediately, zero waiting.
⚠️ Pitfall #5: Production database is empty – run setup twice
Local and production are completely separate databases. When you open your live domain for the first time, the setup form appears again. Run setup twice: locally + on the live site.
Step 7: AI Integration via MCP
EmDash's standout feature: every installation ships with a built-in MCP server. AI assistants like Claude can directly create, edit, and publish content – without touching the admin backend.
- Open Admin on the live URL (not localhost!)
- Settings → API Tokens → create new token
- Edit Claude Desktop config:
%APPDATA%\Claude\claude_desktop_config.json - Add EmDash as MCP server with URL and token
- Restart Claude Desktop
⚠️ Pitfall #6: API token must be created on the live URL
A token created on localhost:4321 only works locally – different databases. On the live site you get:
{"error":{"code":"INVALID_TOKEN","message":"Invalid or expired token"}}
Fix: Always create the token on your live domain, never on localhost.
⚠️ Pitfall #7: EmDash API structure differs from REST conventions
Point 1 – Correct API path: /_emdash/api/content/posts (not /admin/posts). Wrong path → 404.
Point 2 – data wrapper required for POST:
// Falsch – 400 VALIDATION_ERROR:
{ title: "Post", content: "..." }
// Richtig – 201 Created:
{ data: { title: "Post", content: "..." } }
Without the wrapper: 400 VALIDATION_ERROR: Invalid input – no hint that the wrapper is missing.
⚠️ Pitfall #8: PATCH returns 404 – use PUT
// PATCH → immer 404
// PUT → 200 OK (korrektes Update)
// POST /:id/publish → 200 OK (veroeffentlichen)
// POST /content/posts → 201 (erstellen mit data-Wrapper)
Additionally, the slug is auto-generated from the title on creation and cannot be changed via API after publishing. Long title = long ugly slug. Use a short, SEO-friendly title when creating, then update the full title via PUT later.
// Slug wird auto aus Titel generiert – NICHT ueberschreibbar
// Langer Titel = langer Slug
// Kurzerer Titel beim Erstellen verwenden!
await fetch("/_emdash/api/content/posts", {
method: "POST",
body: JSON.stringify({ data: {
title: "Kurzer SEO-Titel", // wird zum Slug
content: "...",
status: "draft" // dann /publish aufrufen
}})
});
⚠️ Pitfall #9: npm run deploy does NOT rebuild – run npm run build first
This is a subtle but important quirk of the EmDash workflow. If you change the theme or Astro templates and then run npm run deploy directly, the old build gets deployed – your changes never go live.
This is because npm run deploy internally just calls wrangler deploy, which uploads the existing dist/ folder without recompiling.
Correct workflow for template changes:
# 1. Build first (~45-50 seconds)
npm run build
# 2. Then deploy
npm run deploy
# NOT: npm run deploy alone – it deploys the old version!
We learned this the hard way: a template fix (HTML renderer for article content) was deployed, but the article still showed the PortableText error – because the build still contained the old code.
Our Recommendations
✅ Register domain at Cloudflare – eliminates all DNS configuration
✅ Activate Workers Paid Plan immediately – deploy fails without it
✅ Activate R2 before first deploy – credit card needed, 10 GB free
✅ KV error 10014: delete namespace, re-deploy
✅ Create API token on live URL only – never on localhost
✅ POST with data wrapper – { data: { title, content, ... } }
✅ Use PUT for updates, not PATCH – PATCH returns 404
✅ POST /:id/publish to publish – separate endpoint
✅ Set slug via short title at creation – cannot be changed after publishing
✅ Run setup twice – local and production are separate databases
✅ For theme changes: npm run build BEFORE npm run deploy – otherwise old code gets deployed
✅ HTML content in theme: use Fragment set:html – PortableText only renders JSON, not HTML strings
FAQ
Do I need programming skills?
Basic terminal skills are enough. Copy and run commands – no need to write code.
Why does the MCP server return 404 even though the token is correct?
Two likely causes: (1) wrong API path – correct is /_emdash/api/content/posts; (2) missing data wrapper on POST/PUT.
Can I migrate from WordPress?
EmDash supports WordPress WXR export import. Custom post types need manual work.
What if my site goes viral?
Workers Paid includes 10M requests/month, then $0.30/million. Automatic scaling, no crashes.
Is EmDash production-ready?
Launched April 2026 – great for new projects. For migrating critical business sites, wait 6–12 more months for the ecosystem to mature.
Does EmDash support multilingual sites?
No built-in i18n yet. Our solution: slugs with language suffix (articlename-de / articlename-en) and language tags.
Conclusion
75 minutes. $6 per month. A modern, AI-native, globally hosted website with built-in MCP server, passkey authentication, and automatic scaling – no own server, no maintenance overhead.
EmDash is young, but EmDash + Cloudflare Workers is the most compelling option for new projects we know of right now.
This article was published by Claude directly via MCP – after we debugged all eight pitfalls together and documented every one of them.
About the Author
Hendrik Muth
SEO specialist and marketing expert at AnalyticaA in Munich, Germany. In his spare time he experiments with AI tools, automation workflows – and builds websites like this one in under 75 minutes.
Found another pitfall? Let us know – we update this list continuously.
Pitfall 10: Media Upload – R2, CSRF, and the Right Path
This one took the longest. Uploading images should be simple — but with EmDash on Cloudflare Workers, it's not out of the box.
The Problem: NOT_SUPPORTED
The POST /media/upload-url endpoint returns NOT_SUPPORTED:
{ "error": { "code": "NOT_SUPPORTED", "message": "Storage does not support signed upload URLs. Use direct upload." } }
Root cause: The native R2 binding (@emdash-cms/cloudflare) doesn't support pre-signed URLs — this is an EmDash design decision. Uploads must go through the Worker.
The Solution: Three Steps
Step 1 – Enable R2 Public Access:
npx wrangler r2 bucket dev-url enable my-emdash-media
# → https://pub-XXXXX.r2.dev
Step 2 – Add publicUrl to astro.config.mjs:
storage: r2({ binding: "MEDIA", publicUrl: "https://pub-XXXXX.r2.dev" })
Step 3 – Rebuild and Deploy:
npm run build && npm run deploy
The CSRF Bypass for API Uploads
External POST requests are blocked by Cloudflare with a 403. The correct header, per EmDash source code (src/api/csrf.ts):
headers: {
"Authorization": "Bearer TOKEN",
"X-EmDash-Request": "1", // ← The magic header
...form.getHeaders()
}
This header tells EmDash the request is legitimate. Browsers can't set it cross-origin — so it's safe as CSRF protection.
This pitfall cost us several hours — the error message was misleading and we had to navigate through Cloudflare WAF, EmDash CSRF middleware, and R2 binding logic.