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
- Open the Admin on the live URL (not localhost)
- Settings → API Tokens → create a new token
- Edit Claude Desktop config:
%APPDATA%\\Claude\\claude_desktop_config.json - 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.
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.
Found another pitfall? Let me know – I update this list as new ones come up.