ISR và revalidate theo yêu cầu trong Next.js 15
Cách website dailerc.com vừa phục vụ trang tĩnh nhanh, vừa cập nhật ngay khi quản trị viên sửa nội dung: fetch có tag, revalidateTag sau mỗi thay đổi và một route revalidate được bảo vệ cho lúc deploy.
Website công ty là loại trang mà ai cũng muốn vừa nhanh như trang tĩnh, vừa cập nhật ngay khi có bài viết mới. Với dailerc.com, chúng tôi dùng Incremental Static Regeneration (ISR) của Next.js 15 kết hợp revalidate theo yêu cầu. Bài này mô tả đúng cách chúng tôi làm trong mã nguồn, kèm những điểm dễ sai.
Điểm khác biệt quan trọng của Next.js 15
Từ Next.js 15, fetch trong Server Component không còn được cache mặc định. Nếu không khai báo gì, mỗi request sẽ gọi lại API. Vì vậy mọi lời gọi tới API công khai của chúng tôi đều khai báo rõ thời gian revalidate và một cache tag:
const res = await fetch(API_URL + '/api/v1/posts?locale=' + locale, {
next: { revalidate: 3600, tags: ['posts'] },
})
Ở cấp trang, các route công khai khai báo export const revalidate = 3600 và dùng generateStaticParams để dựng sẵn các trang chi tiết (dự án, bài viết, dịch vụ) cho cả hai ngôn ngữ vi và en. Kết quả là trang được phục vụ từ cache, và tối đa một giờ sau sẽ được dựng lại ở nền khi có người truy cập.
Cache tag: một danh sách cố định
Một giờ là quá lâu nếu quản trị viên vừa sửa lỗi chính tả. Giải pháp là gắn tag cho dữ liệu và xóa đúng tag khi dữ liệu đổi. Chúng tôi giữ danh sách tag ở một chỗ duy nhất:
export const PUBLIC_TAGS = ['settings', 'service-categories', 'services', 'projects', 'posts'] as const
export type PublicTag = (typeof PUBLIC_TAGS)[number]
export function revalidatePublic(...tags: PublicTag[]): void {
for (const tag of new Set(tags)) revalidateTag(tag)
}
Kiểu PublicTag giúp TypeScript bắt lỗi gõ sai tên tag ngay khi biên dịch. Hàm revalidatePublic loại trùng để không gọi revalidateTag hai lần cho cùng một tag.
Revalidate sau mỗi thao tác trong trang quản trị
Trang quản trị của dailerc.com chạy trong cùng ứng dụng Next.js. Mỗi Server Action tạo, sửa hoặc xóa nội dung sẽ gọi API, rồi gọi revalidatePublic với các tag liên quan. Ví dụ sửa một danh mục dịch vụ thì xóa cả service-categories và services, vì trang danh mục cũng liệt kê các dịch vụ của nó. Lần truy cập kế tiếp sẽ thấy nội dung mới mà không phải chờ hết một giờ.
Lưu ý rằng revalidateTag chỉ đánh dấu dữ liệu là cũ. Trang được dựng lại ở request tiếp theo, nên người truy cập đầu tiên có thể vẫn thấy bản cũ trong lúc bản mới đang được tạo.
Hai lớp cache phải được xóa cùng nhau
Phía API (NestJS) cũng cache phản hồi công khai trong Redis. Khi dữ liệu đổi, service ở API tự xóa khóa cache tương ứng trước khi trả kết quả. Nếu chỉ xóa cache của Next.js mà quên lớp Redis, Next.js sẽ dựng lại trang bằng dữ liệu cũ từ API. Thứ tự đúng là: API ghi dữ liệu và xóa cache Redis, sau đó web gọi revalidateTag.
Route revalidate cho lúc deploy
Có một trường hợp Server Action không xử lý được: khi build image Docker, API chưa chạy, nên các trang dựng sẵn lúc build không có dữ liệu thật. Vì vậy chúng tôi có route POST /api/revalidate:
- Chỉ hoạt động khi biến môi trường
REVALIDATE_SECRETđược đặt; nếu không, trả về 404. - So sánh header
x-revalidate-secretbằngtimingSafeEqualđể không lộ thông tin qua thời gian phản hồi. - Chỉ chấp nhận tag nằm trong
PUBLIC_TAGS, nên không ai dùng route này để xóa cache tùy ý.
const disallowed = tags.filter((t) => !PUBLIC_TAGS.includes(t))
if (disallowed.length) return json({ error: 'Tags not allowed' }, 400)
for (const tag of new Set(tags)) revalidateTag(tag)
Thêm một lớp bảo vệ: Caddy trả 404 cho /api/revalidate từ bên ngoài. Script deploy gọi route này từ bên trong container web, sau khi mọi dịch vụ đã khỏe, nên các trang dựng sẵn lấy được dữ liệu thật ngay sau khi phát hành.
Những điều nên nhớ
- Trong Next.js 15, hãy khai báo
next.revalidatevànext.tagscho mọi fetch cần cache. - Giữ danh sách tag ở một chỗ và dùng kiểu TypeScript để tránh gõ sai.
- Xóa cache theo thứ tự từ nguồn dữ liệu ra ngoài: API trước, Next.js sau.
- Route revalidate cần secret, danh sách tag cho phép, và tốt nhất là không truy cập được từ Internet.
Với cách làm này, phần lớn lượt truy cập được phục vụ từ cache, còn người biên tập vẫn thấy thay đổi của mình xuất hiện gần như ngay lập tức.
Bài viết liên quan
Nhận thanh toán chuyển khoản VietQR và xác nhận qua webhook
Thiết kế luồng thanh toán VietQR cho Dailerc Tools Store: mã đơn trong nội dung chuyển khoản, webhook được xác thực, kiểm tra số tiền chính xác, chống xử lý trùng và phương án xác nhận thủ công.
Đọc tiếpTự host với Docker Compose, Caddy và GitHub self-hosted runner
Cách dailerc.com chạy trên một máy chủ riêng: một origin duy nhất sau Caddy, các dịch vụ trong Docker Compose có healthcheck, và mỗi commit lên main được deploy tự động bởi một runner tự host, có backup và rollback.
Đọc tiếpGiới hạn tần suất và khóa đăng nhập trong NestJS với Redis
Hai lớp bảo vệ cho API: giới hạn request theo IP bằng @nestjs/throttler lưu trong Redis, và khóa tạm thời theo email sau nhiều lần đăng nhập sai, không làm lộ tài khoản nào tồn tại.
Đọc tiếp