EmDash: Kiến trúc CMS cho kỷ nguyên Agent-First
Một CMS năm 2026 phải trả lời được câu hỏi: ai sẽ sửa content? Câu trả lời không còn là 'con người' nữa — và đó là chỗ EmDash khác.
Nói ngắn gọn về CMS cho ai chưa quen từ này: CMS (Content Management System) là phần “quản trị” của một cái website. Nơi bạn đăng bài, sửa tiêu đề, đổi ảnh, thêm trang mới mà không phải đụng code. WordPress là cái CMS nổi tiếng nhất — bạn đang đọc một blog WordPress, khả năng cao là chủ blog viết bài qua cái dashboard /wp-admin.
Mình nghĩ CMS là một trong những thể loại công cụ đã đứng yên lâu nhất. WordPress ra năm 2003. Drupal cùng thời. Sanity, Contentful ra giữa 2010s và đến giờ vẫn cùng một cách nghĩ: bên sửa content là con người, bên code cũng là con người, content là thứ hai bên đẩy qua đẩy lại.
Nhưng năm 2026, ai sửa content không chỉ có con người nữa.
Mình có agent (hiểu đơn giản là AI có thể tự thao tác, không phải chỉ trả lời câu hỏi) chạy trong Claude Code sửa content cho mình. Có script chạy tự động theo giờ, pull dữ liệu về đổ vào website. Có người khác dùng Cursor, dùng n8n workflow. Khi mình build một cái site năm 2026, mình không build cho một “editor” ngồi gõ nữa — mình build cho ba lớp người dùng: con người, developer, và agent.
Và khi nhìn kiểu đó, WordPress đuối. Sanity đỡ hơn nhưng cũng chưa đủ. EmDash là CMS đầu tiên mình thấy cách nghĩ khớp với ba lớp này.

Tích hợp tư duy Agent-First vào cấu trúc dự án
Mình scaffold marketing template bằng giget. Chưa tới một phút là có project chạy. Cấu trúc bên trong là Astro thuần — không template language lạ, không framework bespoke. Hệ thống giữ được sự thanh lịch vì biết Astro là có thể đọc code được ngay.
Tuy nhiên, điểm thay đổi thực sự về mặt kiến trúc lại nằm ở một chi tiết nhỏ: folder .agents/skills/.
.agents/skills/
├── building-emdash-site/
│ ├── SKILL.md
│ └── references/
│ ├── configuration.md
│ ├── schema-and-seed.md
│ ├── querying-and-rendering.md
│ └── site-features.md
├── creating-plugins/
└── emdash-cli/
Mấy file này là hướng dẫn viết cho agent, không phải cho người. Claude Code chạy trong project tự pick up, biết chính xác cách EmDash hoạt động, biết gotcha ở đâu, biết convention gì phải theo. Mình không cần giải thích từ đầu.
Lần đầu sửa theme WordPress, CC phải tự mò PHP, guess plugin structure, đọc docs bên ngoài, đoán rồi thử. EmDash thì project dạy CC cách sửa chính nó, ngay từ lúc scaffold.
Đây mới là điểm khác biệt thật. Không phải tech stack. Là ai được coi là người dùng.

Content không sống trong file
Nói nhanh về “schema” cho ai mới nghe: schema là bản thiết kế của content. Kiểu như “bài blog gồm title, ảnh, body, ngày đăng” — đó là schema của bài blog. Khác nhau là cái bản thiết kế đó sống ở đâu.
Các CMS “file-based” (Astro Content Collections, Next.js Contentlayer) đặt schema trong code. Developer viết file TypeScript, commit vào Git, rồi mới có chỗ để thêm content. Content lưu ở file Markdown. Đổi content, rebuild site. Hợp cho blog của dev, không hợp cho client không biết Git.
EmDash ngược lại. Schema sống trong database — tức là nằm trong kho dữ liệu mà site đang dùng, sửa được từ admin UI giống như sửa bài blog. Editor thêm collection, thêm field, edit content — tất cả từ giao diện web. Không commit code, không rebuild.
Nghe không mới. WordPress làm vậy từ 20 năm trước. Khác ở cách EmDash tổ chức content bên trong.
Nhìn một file seed.json của template marketing:
{
"content": {
"pages": {
"home": {
"title": "Home",
"content": [
{
"_type": "marketing.hero",
"headline": "Build products people actually want",
"primaryCta": { "label": "Start Free Trial", "url": "/signup" }
},
{ "_type": "marketing.features", "items": [...] }
]
}
}
}
}
Home page chỉ có một field content: portableText. Mọi section — Hero, Features, Pricing, Testimonials — đều là block trong mảng đó, mỗi block có _type riêng. Không phải mỗi section là một collection.
Editor mở admin, vào Home, thấy một editor block-based. Kéo Hero lên trên. Xóa Testimonials. Thêm FAQ. Không developer, không deploy, không sửa schema. Content đổi là site đổi.
Mình đã viết riêng một bài về format Portable Text, vì nó là cái trục của cả câu chuyện này.

Schema sống, type tự cập nhật
Có một lệnh mà mình thấy hay mà ít ai nói tới:
npx emdash types
Nó đọc schema đang chạy trong database, sinh ra emdash-env.d.ts chứa TypeScript interface cho mọi collection. getEmDashCollection("posts") return object đã typed sẵn. Autocomplete hoạt động.
Ý tưởng giống OpenAPI sinh TS type, source là DB schema live. Edit schema trong admin UI, chạy lại emdash types, code biết ngay có field mới. Schema sống, type theo kịp, không bao giờ lệch.
Đây là thứ mà chỉ người từng viết TypeScript trên CMS “truyền thống” mới cảm nhận được độ tiện. Sanity cũng có (sanity typegen), nhưng phải chạy qua Sanity CLI và schema phải sync về local trước. EmDash lôi thẳng từ DB live, đỡ một bước.
Form liên hệ chỉ là một POST request
Khi mở pages/contact.astro trong template, mình thấy nó chỉ console.log submission ra terminal, kèm một dòng comment ghi thẳng là “TODO: Replace with actual email/webhook integration.” Ban đầu mình tưởng đây là thiếu sót của template, nhưng sau vài phút đọc kỹ mình nhận ra đó là chủ đích — form không được đóng gói thành một “tính năng của CMS”, mà chỉ là một Astro page bình thường, nên việc bạn muốn gửi nó đi đâu (qua Resend để có email, xuống D1 để lưu vào database, hay bắn JSON sang một webhook Discord chẳng hạn) hoàn toàn là quyết định của bạn trong từng dự án.
Tám dòng code:
await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${import.meta.env.RESEND_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "Contact <onboarding@resend.dev>",
to: ["you@example.com"],
subject: `New contact: ${name}`,
text: `${name} <${email}>\n\n${message}`,
}),
});
wrangler secret put RESEND_API_KEY cho production. Hết.
Mình thích cách EmDash không cố gắng đóng gói form thành một tính năng riêng của CMS, vì thực tế đa số site chỉ cần đẩy nội dung form sang một dịch vụ khác là xong — không cần thêm lớp trừu tượng nào ở giữa. EmDash có plugin form riêng cho ai thật sự cần nhiều thứ hơn (form tự dựng trong admin, có Turnstile chống spam, lưu submission, cron dọn dẹp), nhưng mình nghĩ đa số website chỉ cần gửi một email về đúng hộp thư là đủ rồi.
Ba lớp người dùng được phục vụ ngang nhau
EmDash chạy trên Cloudflare Workers cộng với D1 (database) và R2 (lưu file), deploy chỉ bằng một lệnh pnpm deploy. Lần đầu setup tốn chừng năm lệnh (đăng nhập Cloudflare, tạo database, tạo bucket lưu ảnh, gắn secret, rồi deploy), xong thì mỗi lần push code là site tự cập nhật — cái đó không có gì đặc biệt, Astro với Cloudflare ai làm cũng ra được.
Cái mình thấy đặc biệt chỉ xuất hiện khi ngồi lại nghĩ vài phút về việc ai đang dùng hệ thống này. Client bình thường chỉ cần mở admin UI sửa content là site cập nhật theo, không phải gọi điện nhờ developer. Mình ngồi prompt Claude Code “thêm giùm mình một collection services” thì nó tự đọc file SKILL.md trong project và biết chính xác phải sửa file nào, theo convention nào. Agent bên ngoài — ví dụ một script Python muốn đọc content từ site để đưa vào newsletter — thì đã có sẵn API với type rõ ràng, không phải parse HTML. Và cuối cùng, site chạy trên edge của Cloudflare nên nhanh và rẻ.
Ba lớp người dùng — client, developer, và agent — được phục vụ ngang nhau trong cùng một hệ thống, chứ không ai phải “ngồi sau” ai. Đây là cảm giác mình không có được với WordPress (nơi client là trung tâm còn agent gần như không tồn tại), cũng không có với các giải pháp kiểu Astro Content Collections (nơi developer là trung tâm còn client thì phải commit Git mới sửa được content). Nó khớp với mô hình AHI stack mà mình từng brainstorm mấy tháng trước, chỉ khác ở chỗ lần này mình không phải tự build — có người build sẵn rồi.

Những thứ cần biết trước khi lao vào
EmDash vẫn còn trong giai đoạn beta — phiên bản hiện tại là 0.5.0 ship ngày 14/04/2026 và template repo cập nhật ngày 18/04, nên nếu bạn build cho production thì nên pin chặt version trong package.json để tránh trường hợp một bản minor mới release mà breaking (theo maintainer thì minor version vẫn được phép break trong giai đoạn beta).
Một điểm quan trọng nữa là EmDash không tạo site tĩnh (SSG) cho content, mà mỗi request sẽ gọi đến một Worker để render tại edge. Điều này đồng nghĩa chi phí phụ thuộc vào lượt truy cập, không phải chi phí cố định — Cloudflare Workers free tier cho 100k request mỗi ngày, nếu vượt thì trả tiền cũng chỉ cỡ vài cent cho mỗi chục nghìn request, nên thực tế rẻ hơn hosting tĩnh truyền thống trong đa số trường hợp. Chỉ cần lưu ý đây là mô hình edge-SSR chứ không phải kiểu static hosting Jekyll/Hugo, nên khi bạn quen cách nghĩ “deploy là xong, không ai chạm gì nữa” thì hơi khác một chút.
Về plugin sandbox, EmDash dùng worker_loaders để isolate plugin chạy trong Worker con, nhưng binding này yêu cầu Cloudflare Workers paid (5 đô/tháng). Nếu bạn đang ở free tier, comment dòng worker_loaders trong wrangler.jsonc thì plugin vẫn chạy bình thường trong cùng một process với core (gọi là “safe mode”) — cách này kém an toàn hơn về mặt cô lập, nhưng cho demo hoặc production nhỏ thì hoàn toàn chấp nhận được.
Cuối cùng, admin UI của EmDash đủ tốt cho khoảng 80% trường hợp phổ biến, nhưng nếu client của bạn cần workflow phức tạp như collab đa ngôn ngữ, review draft theo quy trình, hay scheduling phức tạp thì nên cân nhắc kỹ — Sanity Studio trưởng thành hơn nhiều về mảng này, và việc chuyển đổi giữa các CMS khi site đã chạy là chuyện không đơn giản.
Chỗ mình chưa biết
EmDash có thành hay không, mình chưa biết. Cloudflare gọi nó là “spiritual successor to WordPress” trong bài launch, nhưng WordPress lớn vì ecosystem plugin khổng lồ — thứ EmDash còn rất lâu mới có.
Nhưng hướng đi của nó đúng. Khi mình build platform năm 2026 cho con người lẫn agent đều sửa content, EmDash là CMS đầu tiên mà mental model khớp.
Session 3 workshop Agent-First Builder (thứ Ba 21/04, 8PM) mình sẽ scaffold live một corporate site bằng EmDash, từ giget đến deploy Cloudflare và test form gửi email. Bảy prompt Claude Code, sáu mươi phút. Nếu câu chuyện này khớp với gì bạn đang suy nghĩ, ghé nghe.
Bài liên quan: