Astro và triết lý content-first: vì sao mình không dùng Next.js cho site nội dung
Framework web có cần ship 2MB JavaScript cho một trang blog không? Astro trả lời là không, và đó là lý do mình chọn nó cho arealisticdreamer.com.
Mình nghĩ web modern đang mang một cái nghịch lý ít ai chịu gọi ra: site nội dung — blog, docs, marketing page — đang bị build bằng công cụ thiết kế cho app phức tạp. Kết quả là một landing page với năm section text và vài nút load 2MB JavaScript, trong khi người đọc chỉ cần đọc chữ.
Nguyên nhân không phải vì dev lười. Nguyên nhân là Next.js, Nuxt, SvelteKit đã trở thành mặc định của ngành, nên khi cần build site, người ta reach cho cái gần nhất trong tay, kể cả khi nó thừa cho mục đích. Astro đi ngược hướng đó, và sau khi dùng cho arealisticdreamer.com mình thấy đây là lựa chọn đúng cho dòng site content-heavy.

Astro là gì, nói ngắn
Astro là framework web hướng content-first. SSR (server-side rendering) là mặc định, nghĩa là HTML được sinh ra ở server trước rồi gửi về browser, khác với SPA (single-page app) kiểu React thuần nơi browser nhận một cái HTML rỗng rồi JavaScript mới vẽ nội dung lên.
File .astro là superset của HTML — HTML hợp lệ thì cũng là Astro template hợp lệ. Thêm vào đó là một đoạn frontmatter script ở đầu file giống Markdown, một vài JSX expression, và scoped CSS. Một người quen HTML có thể học syntax này trong nửa tiếng.
Nhưng cái đáng nói của Astro không phải syntax, mà là năm nguyên tắc design từ docs chính thức: content-driven (build cho site nhiều content), server-first (render ở server), islands architecture (component tương tác là “đảo” trên HTML tĩnh), zero JS mặc định, và UI-agnostic (cắm React, Vue, Svelte, Preact, HTMX đều được). Năm nguyên tắc này nghe thì đơn giản, nhưng mỗi cái là một quyết định đi ngược trend hiện tại.
Quyết định quan trọng nhất: MPA chứ không phải SPA
Next.js, Nuxt, SvelteKit đều là SPA framework. Browser load một JavaScript bundle, bundle chạy, app dựng UI ra, fetch data, rồi mới hiển thị. Cách này hợp lý cho Figma, Linear, Notion — những app có state phức tạp và nhiều interaction. Nhưng nó không hợp lý cho một cái blog.
Astro chọn MPA (multi-page app) một cách có chủ đích. Mỗi route là một HTML page riêng, render ở server, gửi về browser dưới dạng HTML đã ready. Browser nhận HTML là thấy content ngay, không phải đợi JavaScript parse, execute, rồi hydrate. Thời gian chờ bị cắt đi vì bước render không còn nằm ở client.
Hệ quả đi theo lựa chọn này có ba điểm mình thấy rõ nhất. Một là Time to Interactive thấp vì browser không phải đợi hydration trước khi user bấm được. Hai là SEO dễ vì Google crawler nhận HTML trực tiếp, không cần chạy JavaScript để thấy content. Ba là site tự nhiên thân thiện với agent — một con agent curl URL là có toàn bộ content, không cần headless browser hay Puppeteer.
Astro tự mô tả lựa chọn này: “SPA đi kèm với complexity và performance tradeoff ảnh hưởng Time to Interactive — Astro chọn MPA cho site content có chủ đích.” Câu này ngắn nhưng thẳng, và nó là kim chỉ nam cho toàn bộ framework.
Islands architecture: chỗ Astro thắng mấy framework khác
Mặc định, một .astro component ship zero JavaScript sang browser. HTML thuần. Nếu bạn muốn một phần tử tương tác — ví dụ một component React có filter và search — bạn đánh dấu nó bằng một directive, và chỉ phần đó được hydrate, phần còn lại vẫn là HTML tĩnh.
---
import ProductList from '../components/ProductList.jsx';
---
<h1>Store</h1>
<ProductList client:load />
Có nhiều directive tương ứng với nhiều chiến lược load: client:load hydrate ngay khi page load, client:idle đợi browser rảnh CPU, client:visible chỉ hydrate khi component scroll vào viewport, client:media="(max-width: 768px)" theo media query, và client:only="react" skip server render hoàn toàn chỉ chạy client. Mỗi directive là một cách để quyết định khi nào tương tác thực sự cần.

Ý tưởng cốt lõi là bạn không ship JavaScript “phòng khi cần”, bạn ship JavaScript ở chính xác chỗ cần. Một blog có duy nhất một component comment interactive thì chỉ component đó hydrate, còn header, footer, toàn bộ body text vẫn là HTML tĩnh đứng yên. Đặt cạnh Next.js vốn ship full page bundle mặc định rồi mới tách qua dynamic import, Astro đảo chiều — mặc định không, thêm vào khi có lý do.
Astro 6 trên Cloudflare Workers: local giống hệt production
Astro 6 (current, Q1 2026) có một thay đổi về runtime mà mình thấy đáng nói. astro dev và astro preview giờ chạy trên workerd — chính xác cái engine Cloudflare Workers dùng khi site chạy production. Nghĩa là khi bạn gõ npm run dev ở máy local, bạn đang chạy trên cùng một runtime sẽ phục vụ người dùng thật.
Trước đây, local dev chạy Node còn production chạy Workers, và giữa hai môi trường có khác biệt ở nhiều API nhỏ — fetch behavior, env variable access, binding. Hệ quả là cái cảnh “local chạy ngon, deploy xong break” xảy ra không hiếm. Astro 6 dùng workerd cho cả hai đầu nên code viết ra local có behavior giống hệt production, đỡ phải debug mấy bug chỉ xuất hiện sau khi deploy.
Một thay đổi nữa cần chú ý là Cloudflare Pages đã bị remove khỏi Astro 6. Các project cũ dùng @astrojs/cloudflare với Pages phải migrate sang Workers. Tutorial cũ nào hướng dẫn deploy lên Pages không chạy được với Astro 6. Nếu project của bạn vẫn ở Astro 5 thì chưa cần làm gì, nhưng khi upgrade là bắt buộc phải đổi adapter config.
Config pattern Astro 6 + Cloudflare Workers tương đối ngắn:
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
output: 'server',
adapter: cloudflare({
imageService: 'cloudflare-binding',
}),
});
Access bindings (D1 là database, R2 là object storage, KV là key-value store — tất cả đều là service của Cloudflare):
import { env } from 'cloudflare:workers';
const db = env.DB;
Content Collections: content layer built-in
Astro có một hệ thống quản lý content typed built-in tên Content Collections, cho phép bạn định nghĩa schema (nghĩa là mô tả cấu trúc dữ liệu: có field gì, kiểu gì, bắt buộc hay không) cho content của mình rồi để framework validate tự động.
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const blog = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
pubDate: z.date()
})
});
export const collections = { blog };
Schema Zod cho bạn type-safe frontmatter. Khi bạn quên field title hoặc gõ nhầm pubDate thành pubdate, build fail ngay tại build time thay vì chờ đến runtime mới vỡ. Đây là kiểu bảo hiểm mình thấy hữu ích với các site có nhiều content contributor, ai cũng có thể nhầm mà không cần đọc schema.
Một lưu ý quan trọng: MDX không render được ở runtime trong live collection. Nếu content của bạn lấy từ CMS qua API, Content Collections không cover được trường hợp động; chỉ Markdown cộng custom component là OK. Đây chính là lý do EmDash (CMS mình đang dùng) không dùng Content Collections mà dùng Portable Text — để render được content đến từ database. Mình đã viết bài riêng về Portable Text giải thích kỹ hơn lý do.
Khi nào nên dùng Astro, khi nào không
Astro hợp với site nội dung: blog, docs, marketing page, portfolio, landing page cần load nhanh, hoặc site có một vài component interactive nhưng phần lớn là static. Nếu mục tiêu của bạn là SEO tốt, Core Web Vitals tốt, deploy edge (Cloudflare Workers, Vercel Edge), Astro cover được gần như mọi trường hợp.
Astro không hợp với app có state phức tạp kiểu dashboard real-time, collab tool, hay admin panel với nhiều interaction lồng nhau. Những trường hợp này bản chất là SPA và sẽ khó chịu khi phải tự tay cắm từng island. Ngoài ra nếu team đã quen Next.js và không muốn đổi mindset sang MPA, cost học lại có khi lớn hơn lợi ích ship ít JS.
Rule of thumb đơn giản: nếu trang của bạn render được lần đầu bằng HTML tĩnh thì Astro là lựa chọn tự nhiên; nếu bắt buộc phải chạy JavaScript mới hiển thị được content thì một SPA framework sẽ hợp hơn.
Liên quan tới agent-first

Đây là điểm mình thấy Astro ăn khớp nhất với hướng agent-first builder mình đang push. Khi site render HTML ở server, một agent AI chỉ cần gửi request HTTP là nhận về toàn bộ content — không cần headless browser, không cần Puppeteer, không cần đợi JavaScript execute xong rồi mới extract DOM. Cost chạy agent giảm đáng kể khi backend trả HTML sẵn sàng.
Ngược lại với SPA, agent phải khởi động một browser đầy đủ, chạy JS, đợi hydration, rồi mới lấy được content ra. Tốn tài nguyên, tốn thời gian, và fail rate cao hơn nhiều (animation chưa xong, element chưa mount, race condition). Mỗi lần scale lên hàng nghìn trang là bài toán đội cost lên theo cấp số nhân.
Content-first framework tự nhiên là agent-friendly framework. Không phải trùng hợp mà Astro chọn MPA và EmDash — một CMS build cho agent-first — chọn Astro làm base. Khi triết lý framework trùng với nhu cầu của một lớp người dùng mới (agent), cả hai tăng cường cho nhau.
Session 3 workshop (2026-04-21)
Workshop Agent-First Builder có ba session, và Session 3 vào thứ Ba 2026-04-21 lúc 8PM sẽ demo live build một corporate site bằng Astro 6 cộng EmDash cộng Cloudflare Workers.
Tham gia không cần nắm sâu Astro trước. Mental model đủ dùng gồm bốn điểm: file .astro là HTML kèm frontmatter, mặc định zero JS, thêm client:* directive khi cần tương tác, deploy edge trên Cloudflare Workers. Content Collections, Zod schema, middleware có thể bỏ qua cho workshop này vì EmDash không dùng Content Collections mà dùng DB-backed content riêng.
Vài điểm cần biết trước khi cài
Astro 6 yêu cầu Node 20.19 trở lên, kiểm tra phiên bản Node trước khi cài để tránh lỗi mơ hồ lúc build. Config output: 'server' là bắt buộc cho các site dùng EmDash vì content động phải SSR, không SSG (static site generation) được. Các project Astro cũ đang deploy trên Cloudflare Pages cần migrate sang Workers khi upgrade lên Astro 6. Package @astrojs/cloudflare phiên bản 13 trở lên là version tương thích với Astro 6; version cũ hơn chỉ chạy với Astro 5.
Web không bắt buộc phải nặng. Cái nặng đến từ mặc định của công cụ, không phải từ nhu cầu của người đọc. Astro nhắc mình nhớ điều đó mỗi lần build một trang mới — và mỗi lần deploy xong nhìn Lighthouse score, mình thấy lựa chọn này không phải chuyện sở thích, mà là chuyện tôn trọng người dùng bên kia màn hình.
Tham khảo: