Làm blog cá nhân 0đ: vì sao chọn Next.js + Supabase + Vercel
Làm blog cá nhân 0đ với Next.js, Supabase và Vercel: so sánh với WordPress và site tĩnh, giới hạn gói miễn phí, và 500 MB chứa được bao nhiêu bài.
Bạn muốn làm blog cá nhân 0đ? Viết bài được ngay trên trình duyệt, có ảnh, có SEO thì Next.js + Supabase + Vercel là một lựa chọn hợp lý, vì cả ba đều có gói miễn phí đủ dùng. Bài này kể vì sao Tech Notes chọn bộ ba này và cái "0đ" đó giới hạn ở đâu!
Bắt đầu từ danh sách yêu cầu
Đây là tập đầu của series Xây blog với Next.js + Supabase. Điểm khác của series này: blog được xây trong bài chính là blog bạn đang đọc. Quyết định nào, lỗi nào trong series cũng lấy từ quá trình làm Tech Notes.
Trước khi chọn công nghệ, mình ngồi viết ra mình cần gì. Nghe thì hiển nhiên, nhưng bỏ qua bước này thì thường mọi người sẽ chọn theo thứ đang được nhắc nhiều nhất tuần đó là đừng bắt đầu với framework . Vì vậy Tech Notes liệt kê danh sách theo thứ tự như sau:
- Chi phí 0đ khi còn ít người đọc, chỉ trả tiền khi blog thật sự lớn.
- Viết và đăng bài ngay trên trình duyệt, kể cả trên điện thoại, không phải mở code hay gõ lệnh.
- Bài có ảnh, và không phải lo hết dung lượng.
- SEO tốt: trang tải nhanh, Google đọc được nội dung, chia sẻ lên mạng xã hội có ảnh xem trước.
- Làm chủ dữ liệu và giao diện, muốn thêm bản tin hay ngân hàng câu hỏi phỏng vấn thì tự thêm.
- Học được những kỹ năng dùng trong công việc thật: React, SQL, phân quyền, deploy.
Yêu cầu cuối là lý do rất riêng của một developer. Nếu bạn chỉ muốn viết mà không quan tâm code, câu trả lời ở cuối bài sẽ khác hẳn.
Bốn hướng đã cân nhắc
Hướng đầu tiên là dùng nền tảng có sẵn như Viblo, Medium hay Substack. 0đ, năm phút là có bài đầu tiên, lại có sẵn người đọc. Nhưng giao diện, dữ liệu, tên miền đều không phải của mình, muốn thêm một tính năng riêng là chịu. Hỏng yêu cầu 5 và 6.
Hướng thứ hai là WordPress. Trình soạn thảo tốt, hàng nghìn plugin, không cần biết code. Có điều nó cần một máy chủ chạy PHP và MySQL. Hosting miễn phí thì thường chậm, chèn quảng cáo hoặc giới hạn đủ thứ; hosting ổn định thì tốn tiền hằng tháng. Muốn tùy biến sâu lại phải học PHP và cả hệ thống plugin riêng của WordPress, thứ khá ít giá trị khi bạn đi làm ở một công ty dùng React.
Hướng thứ ba là site tĩnh: mỗi bài là một file Markdown trong Git, build bằng Astro, Hugo hoặc Next.js. 0đ, cực nhanh, SEO rất tốt, và thật lòng mà nói đây là hướng hấp dẫn nhất.
Điểm khiến Tech Notes không chọn nó là quy trình đăng bài: sửa file, commit, push. Thử tưởng tượng phát hiện lỗi chính tả lúc đang ngồi xe buýt, và phải mở laptop để sửa một chữ. Ảnh cũng nằm luôn trong repo, nên repo cứ phình dần theo năm tháng.
Hướng thứ tư là Next.js + Supabase + Vercel. Bài viết nằm trong database nên viết, sửa, đăng đều làm trên trình duyệt. Ảnh nằm ở kho file riêng.
Trang vẫn được tạo sẵn và lưu đệm nên tải nhanh không kém site tĩnh. Cái giá là phải tự làm những thứ WordPress cho sẵn: trình soạn thảo, đăng nhập, phân quyền, SEO. Thật ra đó lại chính là nội dung của series này.
Tech Notes chọn hướng thứ tư. Không phải vì nó tốt nhất, mà vì nó là hướng duy nhất đạt đủ sáu yêu cầu như mình đã đề ra.
Mỗi mảnh làm việc gì
Next.js là framework React để làm website. Ngoài phần giao diện, việc quan trọng nhất nó làm cho một blog là tạo sẵn trang HTML trên máy chủ trước khi gửi cho trình duyệt. Nhờ vậy Google đọc được nội dung ngay, còn người đọc thấy chữ trước khi JavaScript kịp tải xong. Tech Notes dùng App Router, kiểu tổ chức mới của Next.js, nơi mỗi thư mục trong src/app là một đường dẫn của website.
Supabase là một PostgreSQL kèm theo vài dịch vụ. Blog dùng ba thứ của nó. Database chứa bài viết, chủ đề, câu hỏi phỏng vấn, người đăng ký bản tin. Auth cho đăng nhập bởi tác giả, còn người đọc thì không cần tài khoản. Storage là kho file cho ảnh, còn database chỉ giữ đường dẫn tới ảnh.
Vercel lấy code từ GitHub, build và đưa lên mạng. Mỗi lần merge vào nhánh main là có bản mới; mỗi pull request lại có một đường dẫn xem trước riêng, rất tiện để kiểm tra trước khi merge.
Tóm lại, một lượt đọc bài trên Tech Notes được diễn ra như sau. Bạn mở /bai-viet/ten-bai. Ngay lập tức, Vercel trả về trang HTML đã tạo sẵn từ lần trước. Nếu trang đó đã cũ hơn một giờ, bạn vẫn nhận bản cũ, nhưng ở phía sau Vercel gọi Next.js tạo lại trang: đọc bài mới nhất từ Supabase, dựng HTML mới, lưu đệm cho người đọc tiếp theo. Ảnh trong bài thì tải thẳng từ Supabase Storage.
Cơ chế "trả bản cũ, âm thầm làm bản mới" đó gọi là ISR. Nó là lý do một blog đọc dữ liệu từ database vẫn nhanh như site tĩnh.
"0đ" những gì?
Miễn phí thì luôn có giới hạn. Các con số dưới đây mình lấy từ trang giá chính thức, cập nhật ngày 5/11/2026. Chúng thay đổi theo thời gian, nên bạn đọc bài này vào lúc khác thì nhớ kiểm tra lại.
Với Supabase, gói Free: database 500 MB mỗi project, file storage 1 GB, egress (dữ liệu tải ra) 5 GB cộng thêm 5 GB cho dữ liệu đã lưu đệm, tối đa 2 project đang chạy. Có 50.000 người dùng đăng nhập mỗi tháng, nhưng blog chỉ có tác giả đăng nhập nên con số này gần như không đụng tới.
Có một điều khoản của Supabase dễ bị bỏ qua, project miễn phí bị tạm dừng sau 1 tuần không có hoạt động. Blog có người đọc đều đặn thì không lo. Blog bỏ quên một tuần thì phải vào bảng điều khiển bật lại.
Với Vercel, gói Hobby: 100 GB Fast Data Transfer mỗi tháng, 1 triệu lượt gọi function và 1 triệu request qua CDN, 5.000 lần biến đổi ảnh qua Image Optimization và log lúc chạy chỉ giữ 1 giờ.
Điều khoản quan trọng nhất của Vercel không phải con số nào cả mà gói Hobby chỉ dành cho mục đích cá nhân, phi thương mại. Blog cá nhân thì được. Gắn quảng cáo, bán khóa học hay dùng cho công ty thì phải lên gói Pro. Nếu bạn có ý định kiếm tiền từ blog, tính chuyện này từ đầu sẽ đỡ hơn nhiều so với lúc đã có người đọc.
500 MB chứa được bao nhiêu bài?
Câu này ai cũng hỏi, và hay được trả lời bằng cảm giác. Mình thích đo hơn, nên viết một script nhỏ tên uoc-luong.ts. Script làm ba phép tính. Đầu tiên, nó tính một bài viết chiếm bao nhiêu byte. Sau đó, nó chia giới hạn của gói miễn phí cho con số đó, theo ba cách lưu: chỉ lưu chữ, lưu cả ảnh gốc trong database, và để ảnh đã nén ở kho file riêng.
// Ước lượng: gói Supabase Free (database 500 MB, file storage 1 GB) chứa được bao nhiêu bài?
// node uoc-luong.ts # dùng số mặc định
// node uoc-luong.ts bai-mau.md # đo một bài Markdown thật của bạn
import { readFileSync } from "node:fs";
const MB = 1024 * 1024;
const LIMITS = { databaseMb: 500, storageMb: 1024 }; // supabase.com/pricing, gói Free (11/2026)
// Số đo thật từ bài nháp của Tech Notes: 1.302 chữ ≈ 7.972 byte (tiếng Việt có dấu tốn 2–3 byte/ký tự).
const MEASURED = { words: 1302, bytes: 7972 };
const file = process.argv[2];
const bytesPerWord = file
? Buffer.byteLength(readFileSync(file, "utf8")) / readFileSync(file, "utf8").split(/\s+/).filter(Boolean).length
: MEASURED.bytes / MEASURED.words;
// Giả định (đổi tùy blog của bạn):
const WORDS_PER_POST = 3000; // bài dài kiểu hướng dẫn
const OVERHEAD = 2; // nhân đôi cho tiêu đề, mô tả, chỉ mục, phiên bản cũ… (ước lượng dè dặt)
const IMAGES_PER_POST = 3;
const PHOTO_MB = 4; // ảnh chụp điện thoại chưa nén (ví dụ)
const COMPRESSED_KB = 300; // cùng ảnh sau khi nén WebP, rộng 1600px (ví dụ)
const postBytes = bytesPerWord * WORDS_PER_POST * OVERHEAD;
const fmt = (n: number) => Math.floor(n).toLocaleString("vi-VN");
console.log(`Một chữ ≈ ${bytesPerWord.toFixed(1)} byte → một bài ${WORDS_PER_POST} chữ ≈ ${fmt(postBytes / 1024)} KB (đã tính phụ phí x${OVERHEAD})`);
console.log(`\n1) Chỉ lưu chữ trong database 500 MB: ~${fmt((LIMITS.databaseMb * MB) / postBytes)} bài`);
const rawImages = IMAGES_PER_POST * PHOTO_MB * MB;
console.log(`2) Nhét luôn ${IMAGES_PER_POST} ảnh gốc (${PHOTO_MB} MB/ảnh) vào database: ~${fmt((LIMITS.databaseMb * MB) / (postBytes + rawImages))} bài`);
const compressed = IMAGES_PER_POST * COMPRESSED_KB * 1024;
console.log(`3) Ảnh nén ${COMPRESSED_KB} KB để ở Storage 1 GB, database chỉ lưu đường dẫn:`);
console.log(` database: ~${fmt((LIMITS.databaseMb * MB) / postBytes)} bài · storage: đủ ảnh cho ~${fmt((LIMITS.storageMb * MB) / compressed)} bài`);Số byte mỗi chữ được đo từ một bài nháp thật của blog. Tiếng Việt tốn byte hơn tiếng Anh, vì mỗi chữ có dấu chiếm 2–3 byte trong UTF-8. Mấy con số trong mục "Giả định" là ước lượng của mình, bạn sửa cho khớp với blog của bạn.
Script chạy thẳng bằng Node.js 22.18 trở lên, không cần cài thêm gì. Kết quả với số mặc định như sau:
Một chữ ≈ 6.1 byte → một bài 3000 chữ ≈ 35 KB (đã tính phụ phí x2)
1) Chỉ lưu chữ trong database 500 MB: ~14.271 bài
2) Nhét luôn 3 ảnh gốc (4 MB/ảnh) vào database: ~41 bài
3) Ảnh nén 300 KB để ở Storage 1 GB, database chỉ lưu đường dẫn:
database: ~14.271 bài · storage: đủ ảnh cho ~1.165 bàiDòng thứ hai là dòng đáng nhìn kỹ nhất. Chữ thì gần như không tốn chỗ, viết mỗi ngày một bài dài, phải vài chục năm mới đầy 500 MB. Nhưng đưa ảnh gốc vào database thì khoảng 41 bài là hết. Ảnh là thứ tốn dung lượng chứ không phải chữ.
Từ đó ra luôn một quyết định thiết kế, ảnh phải nằm ngoài database, phải được nén trước khi tải lên, và database chỉ giữ đường dẫn. Làm vậy thì giới hạn thật chuyển sang 1 GB Storage, đủ ảnh cho hơn nghìn bài, và còn nới thêm được nếu nén mạnh tay hơn.
Bạn muốn đo theo bài của chính mình thì lưu một bài viết bất kỳ thành file Markdown, ví dụ bai-cua-ban.md, rồi chạy node uoc-luong.ts bai-cua-ban.md. Lúc đó, script đếm số chữ và số byte của file này thay cho số đo mặc định của Tech Notes, rồi tính lại cả ba dòng.
Mỗi tháng năm phút để khỏi bị bất ngờ
Ước lượng chỉ là bước đầu. Khi blog đã chạy, nên xem số liệu thật mỗi tháng một lần. Bên Supabase là trang Usage của tổ chức, cho biết database, storage và egress đã dùng bao nhiêu trong kỳ. Với blog, con số đáng để ý nhất là storage. Bên Vercel là tab Usage, cho biết dữ liệu truyền đi, số lần gọi function và số lần biến đổi ảnh. Blog ít người đọc thì mấy con số này thường chỉ chạm một góc nhỏ của hạn mức.
Khi một con số vượt khoảng nửa hạn mức, đó là lúc đi tìm nguyên nhân, chưa phải lúc nâng gói. Thủ phạm thường là ảnh tải lên không nén, ảnh cũ không dùng nhưng vẫn nằm trong kho, hoặc một trang nào đó đang gọi database ở mỗi lượt xem thay vì dùng trang tạo sẵn. Cả ba đều sửa được bằng code, và đều có tập riêng trong series.
Một thói quen nhỏ nữa: ghi lại ngày bạn kiểm tra giới hạn, như mình làm ở phần trên. Gói miễn phí thay đổi liên tục. Con số đúng hôm nay có thể sai sau một thời gian và người đọc cần biết nó được kiểm tra lúc nào.
Ba chuyện không có trong kế hoạch
Danh sách yêu cầu ở đầu bài trông gọn gàng vậy nhưng khi làm thật thì sẽ có những phát sinh khác.Dưới đây, mình liệt kê về 3 việc đã xảy ra khi làm Tech Notes và đều đến từ chính lựa chọn stack này.
Thứ nhất: ảnh phải được nén ngay trên trình duyệt. Sau khi ước lượng như trên, rõ ràng ảnh chụp điện thoại vài MB sẽ tốn hết 1 GB rất nhanh. Nên trình soạn thảo bây giờ thu nhỏ và chuyển ảnh sang WebP trước khi tải lên, Storage chỉ nhận bản đã nén.
Sao không nén trên máy chủ cho đơn giản? Vì như vậy ảnh gốc vẫn phải đi qua mạng và chạy qua function của Vercel trước đã, tốn cả băng thông lẫn thời gian chạy function, đúng hai thứ mà gói miễn phí giới hạn.
Thứ hai: ảnh chia sẻ chỉ có ở trang bài viết. Mỗi bài có ảnh xem trước riêng khi dán link lên mạng xã hội. Nhưng deploy xong mới phát hiện: dán link trang chủ hay trang câu hỏi phỏng vấn thì chẳng có ảnh nào.
Không lỗi, không cảnh báo, việc thiếu ảnh chia sẻ không làm ảnh hưởng tới bài viết tuy nhiên thiếu tính mô tả trực quan sẽ làm bài viết trở nên "thô". Từ đó mình rút ra là những thứ "không có cũng chẳng báo lỗi" phải được kiểm tra có chủ đích, trên mọi loại trang. Sẽ có chủ đề chi tiết hơn cho nội dung này trong các bài viết sau.
Thứ ba: công cụ đo lượt xem của Vercel chỉ chạy trên Vercel. Thêm Vercel Analytics xong, chạy thử trên máy thì trình duyệt báo lỗi 404 khi tải script đo lường, vì script đó chỉ được phục vụ trên hạ tầng của Vercel. Cách xử lý đơn giản: chỉ chèn script khi code đang chạy trên Vercel, tức là khi có biến môi trường VERCEL. Chuyện nhỏ, nhưng nhắc mình một điều: dựa vào dịch vụ của nền tảng nào thì môi trường trên máy và môi trường thật sẽ khác nhau ở những chỗ không ngờ.
Không chuyện nào là lý do để bỏ stack này. Nhưng chúng là lý do để mình luôn dành vài phút kiểm tra sau mỗi lần deploy, thay vì tin "chạy trên máy được là xong".
Khi nào thì đừng chọn stack này
Nếu bạn chỉ muốn viết, đừng tự xây gì cả. Substack, Viblo hay WordPress sẽ cho bạn thêm rất nhiều thời gian để viết bài, thứ quan trọng hơn mọi lựa chọn công nghệ.
Nếu blog có mục đích thương mại, như đã nói, gói Hobby của Vercel không cho phép. Tính chi phí gói Pro hoặc chọn nơi deploy khác từ đầu.
Nếu bạn không muốn lo vận hành thì cũng nên cân nhắc. Database, phân quyền, sao lưu đều là việc của bạn, và một lỗi phân quyền có thể làm lộ bản nháp hoặc cho người lạ sửa bài. Còn nếu bạn chưa biết React, theo mình nên học React bằng một dự án nhỏ hơn trước.
Đang ôn phỏng vấn thì câu "vì sao không lưu ảnh trong database" rất hay gặp, có sẵn đáp án trong ngân hàng câu hỏi phỏng vấn.
Cuối cùng là dựng project sao cho gọn
Stack đã chọn xong. Câu hỏi tiếp theo là bắt đầu từ đâu: tạo project Next.js thế nào, thư mục nào để làm gì, trang chủ đầu tiên viết ra sao để sau này thêm vài chục trang mà không rối.
Tập 2 khởi tạo project với App Router, giải thích cấu trúc thư mục Tech Notes đang dùng và đưa trang đầu tiên lên Vercel. Cả series tại Xây blog với Next.js + Supabase.