← Tất cả bài viết
Headless WordPress Với Frontend Next.js Hiện Đại
Tách rời WordPress và render frontend Next.js nhanh — lấy bài/trang qua REST API hoặc WPGraphQL, gắn ISR, media, SEO, preview, và né các bẫy auth/CORS/cache.

Đa số site WordPress không cần một CMS mới — chúng cần một frontend mới. Nếu khách của bạn đã quen dashboard WordPress nhưng theme PHP thì chậm, cứng nhắc và gần như không thể làm chuyển động, nước đi là chuyển sang headless WordPress với frontend Next.js hiện đại: giữ WordPress thuần làm back end nội dung cho editor và render site thật bằng Next.js. Đây là con đường "bỏ theme" tôi đã triển khai trên dự án khách thật — trong đó có Iventions, làm cùng studio SERIOUS.BUSINESS và nhà thiết kế Huy Phan. Bên dưới là toàn bộ pipeline: lấy bài và trang, ISR và revalidate theo tag, media và SEO, preview, cùng các bẫy auth/CORS/cache khiến hầu hết bản dựng đầu tiên vấp ngã.
Một lưu ý về công sức: ở Iventions tôi là developer; phần thiết kế và art direction là của Huy Phan + SERIOUS.BUSINESS. Mọi thứ ở đây là góc kỹ thuật của bản dựng đó.
Vì sao chọn headless WordPress (và bỏ theme vào sọt rác)
Bản năng của nhiều dev là chọn một headless CMS bóng bẩy (Sanity, Payload) rồi không đụng vào WordPress nữa. Nhưng trong dự án thật, yếu tố quyết định hiếm khi là gu của dev — mà là ai sẽ sửa nội dung sau khi ra mắt. Cực kỳ nhiều đội marketing, agency, founder đang sống trong WordPress mỗi ngày, và đào tạo lại họ sang công cụ mới là một chi phí thật.
"Headless" nghĩa là WordPress giữ dashboard, thư viện media, phân quyền và quy trình biên tập — nhưng bạn bỏ hoàn toàn lớp theme. Bản thân site trở thành một app Next.js đọc nội dung qua API. Sự tách bạch đó chính là toàn bộ giá trị:
- Editor giữ WordPress. Không đào tạo lại, phân quyền trưởng thành, hệ sinh thái plugin khổng lồ cho SEO, form, custom field.
- Người dùng nhận frontend nhanh, riêng biệt. React Server Components, caching chi tiết, tương tác được code-split, và một hệ thống motion bạn thật sự sở hữu.
- Hai bên tách rời. Thiết kế lại frontend mà không cần di trú nội dung, hoặc đổi CMS sau này mà không đụng UI. Đó là phần "future-proof" có thật.
Đây là một trong các mô hình giao hàng tôi nói trong một fullstack creative developer làm những gì — làm chủ cả back end của editor lẫn frontend của người dùng, từ một đối tác.
Hai cách đọc nội dung đã được tài liệu hóa: REST vs WPGraphQL
WordPress có sẵn REST API, còn WPGraphQL thêm một endpoint GraphQL có kiểu qua plugin. Cả hai đều là đường ổn định, có tài liệu — chọn tùy mức độ chính xác từng field của component.
| Cách | Là gì | Hợp khi nào |
|---|---|---|
| WP REST API | Endpoint /wp-json có sẵn | Không thêm plugin, URL dễ cache, bài/trang chuẩn |
| WPGraphQL | Endpoint GraphQL trên WP + ACF | Muốn lấy đúng field một component cần trong một request |
Với các build dạng block, nặng art direction, tôi chọn WPGraphQL vì frontend hướng component ăn
rơ tự nhiên với query chính xác từng field — một block Hero xin tiêu đề, media và config của nó
trong một lượt. Với site kiểu blog-cộng-trang tiêu chuẩn thì REST API hoàn toàn ổn và bớt một thứ
phải lo. Phần còn lại của bài dùng REST vì không cần plugin và URL rất dễ cache.
Lấy bài và trang từ WordPress REST API
Trong App Router, việc fetch là chuyện của server — thông tin đăng nhập WordPress và trọng lượng query không bao giờ chạm tới trình duyệt. Đây là một danh sách bài có phân trang. Bẫy nhiều người bỏ lỡ: phân trang nằm ở header phản hồi, không phải body.
// lib/wp.ts — chỉ chạy trên server
const WP = process.env.WP_URL // vd https://cms.example.com
export async function getPosts(page = 1) {
const res = await fetch(`${WP}/wp-json/wp/v2/posts?per_page=12&page=${page}&_embed`, {
// ISR: trả HTML đã cache, làm mới ngầm mỗi 5 phút
next: { revalidate: 300, tags: ['posts'] },
})
if (!res.ok) throw new Error('WP posts fetch failed')
return {
posts: await res.json(),
totalPages: Number(res.headers.get('X-WP-TotalPages') ?? 1),
}
}
Cờ &_embed âm thầm gánh phần nặng — nó nhét sẵn featured image, tác giả và các term taxonomy vào
một object _embedded để bạn không phải bắn thêm ba request mỗi card. Lấy một bài hay trang theo slug
cũng cùng dáng:
export async function getPost(slug: string) {
const res = await fetch(`${WP}/wp-json/wp/v2/posts?slug=${slug}&_embed`, {
next: { revalidate: 300, tags: [`post:${slug}`] },
})
const [post] = await res.json() // query theo slug trả về một mảng
return post ?? null
}
// trang nằm ở /wp-json/wp/v2/pages?slug=...
Dòng next: { revalidate, tags } đó là phần quan trọng nhất của cả kiến trúc — chính nó giúp một site
chạy nền WordPress đạt Core Web Vitals ngang site tĩnh hoàn toàn. Chi tiết ở dưới.
Media, featured image và các field SEO
Với _embed, featured image và các kích thước responsive của nó trả về ngay trong payload. Hãy ánh
xạ dáng REST thô thành một object gọn mà component dùng, và lấy các field SEO mà plugin của khách phơi
ra — Yoast và Rank Math đều đẩy SEO có cấu trúc vào phản hồi REST (Yoast là object
yoast_head_json) khi được bật:
function mapPost(p: WpPost) {
const media = p._embedded?.['wp:featuredmedia']?.[0]
return {
title: p.title.rendered,
html: p.content.rendered, // render qua sanitizer, đừng đổ thẳng bằng lòng tin
cover: media?.source_url,
coverAlt: media?.alt_text ?? '',
sizes: media?.media_details?.sizes, // thumbnail / medium / large / full
seo: p.yoast_head_json, // title, description, og_image, canonical…
}
}
Các field SEO đó nạp thẳng vào generateMetadata của Next.js, nên cấu hình Yoast của editor vẫn điều
khiển <title>, description và Open Graph trên frontend headless:
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost((await params).slug)
const seo = post?.yoast_head_json
return {
title: seo?.title ?? post?.title?.rendered,
description: seo?.description,
openGraph: { images: seo?.og_image ?? [] },
alternates: { canonical: seo?.canonical },
}
}
Với chính các ảnh, hãy cho URL media WordPress đi qua next/image cùng một mục remotePatterns trỏ
host CMS, để frontend vẫn tự resize và dùng định dạng ảnh hiện đại.
ISR và revalidate theo tag: nội dung nhanh mà cập nhật trong vài giây
Lời hứa của headless chỉ đúng nếu chỉnh sửa thật sự xuất hiện mà không cần deploy lại. Mô hình: cache
mạnh tay với next: { revalidate, tags }, rồi để một webhook WordPress xóa đúng tag khi đăng bài.
Tài liệu caching của Next.js chứng minh điều này — phục
vụ HTML tĩnh cache trên CDN mà vẫn làm mới nội dung theo yêu cầu.
// app/api/revalidate/route.ts — WordPress gọi khi save_post
import { revalidateTag } from 'next/cache'
export async function POST(req: Request) {
const { secret, type, slug } = await req.json()
if (secret !== process.env.WP_REVALIDATE_SECRET) {
return new Response('Unauthorized', { status: 401 })
}
revalidateTag(type === 'page' ? `page:${slug}` : `post:${slug}`)
revalidateTag('posts') // làm mới mọi listing có chứa nó
return Response.json({ revalidated: true })
}
Ở phía WordPress, nối một hook save_post nhỏ (hoặc plugin như WP Webhooks) POST slug kèm một secret
dùng chung. Giờ editor bấm Publish là thay đổi lên sóng trong vài giây — cùng trải nghiệm sửa bài,
tốc độ site tĩnh. Đây cũng là kỷ luật theo tag mà CMS của chính site này dùng; tôi viết lại kiểu hỏng
khi làm sai trong vì sao một fetch CMS không cache âm thầm ngốn
tiền.
Preview: hiện bản nháp với Draft Mode + auth
Một bản nháp headless không còn "tự hiện ra" — than phiền lớn nhất của editor ở các build gấp. Bạn cần
một route preview vào Draft Mode của Next.js và lấy nội dung không cache, có xác thực. Trỏ link
preview của WordPress vào đó (qua filter preview_post_link) kèm một secret dùng chung:
// app/api/preview/route.ts
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
export async function GET(req: Request) {
const { searchParams } = new URL(req.url)
if (searchParams.get('secret') !== process.env.WP_PREVIEW_SECRET) {
return new Response('Invalid token', { status: 401 })
}
;(await draftMode()).enable()
redirect(`/${searchParams.get('slug')}`)
}
Rồi trong fetch, rẽ nhánh theo Draft Mode: khi bật, xin trạng thái draft kèm header auth và bỏ
cache để editor thấy đúng ký tự vừa gõ. Từ Next.js 15 trở đi, fetch không còn cache mặc định,
nên nói rõ cả hai chiều đều quan trọng:
import { draftMode } from 'next/headers'
export async function getPostForRoute(slug: string) {
const { isEnabled } = await draftMode()
const status = isEnabled ? 'draft,publish' : 'publish'
return fetch(`${WP}/wp-json/wp/v2/posts?slug=${slug}&status=${status}&_embed`,
isEnabled
? { cache: 'no-store', headers: { Authorization: wpAuthHeader() } }
: { next: { revalidate: 300, tags: [`post:${slug}`] } },
).then((r) => r.json())
}
const wpAuthHeader = () =>
'Basic ' + Buffer.from(`${process.env.WP_USER}:${process.env.WP_APP_PASSWORD}`).toString('base64')
Cái WP_APP_PASSWORD là một Application Password theo từng user (WordPress 5.6+) — cách được tài
liệu hóa để xác thực request REST qua HTTPS mà không lộ mật khẩu đăng nhập. Với preview WPGraphQL bạn
dùng plugin JWT Authentication thay thế.
Các bẫy thật: auth, CORS và caching
Ba thứ làm hỏng gần như mọi build headless đầu tiên. Không cái nào khó khi đã vấp qua:
- CORS thường là tự chuốc. Fetch WordPress từ server (RSC, route handler) là không có CORS
nào cả — server-to-server. Lỗi CORS nghĩa là bạn đang gọi
/wp-jsontừ trình duyệt; dời lời gọi sang server là nó biến mất. Tiện thể giữ luôn thông tin đăng nhập khỏi client. - Caching đã lật ở Next.js 15.
fetchgiờ không cache mặc định, nên port cẩu thả sẽ dội bom WordPress mỗi request. Luôn nói rõ:next: { revalidate, tags }cho nội dung,cache: 'no-store'chỉ cho preview. Tôi mổ xẻ cái giá của việc làm sai trong bài fetch CMS không cache và giữ database dưới giới hạn connection. - Plugin giả định có theme. Vài plugin WordPress chèn markup không bao giờ tới frontend headless. Kiểm tra plugin SEO/form xem có hỗ trợ headless/REST trước khi cam kết — Yoast và Rank Math an toàn vì phơi field REST; một page-builder chỉ xuất template PHP thì không.
Khi nào headless WordPress đáng — và không đáng
Headless là công cụ đúng nhiều hơn bạn tưởng, nhưng không miễn phí. Nói thẳng:
Đáng khi: khách cam kết dùng WordPress làm editor, có lượng biên tập thật, và frontend quá tùy biến, quá nhanh hoặc quá nhiều chuyển động cho một theme — một site flagship thương hiệu hoặc giàu nội dung. Đó đúng là Iventions.
Thừa khi: một site brochure nhỏ hiếm khi đổi nội dung. Chạy hosting WordPress và deploy Next.js là hai hệ thống, hai nhịp cập nhật, hai bề mặt bảo mật. Build tĩnh từ Markdown, hoặc một CMS có cấu trúc nhẹ, ít phải bảo trì hơn. Nếu bạn chọn CMS từ đầu thay vì thừa kế WordPress, hãy cân nhắc trong so sánh headless CMS hoặc con đường editor hiện đại trong xây website Next.js với Sanity.
Điểm cộng: frontend chuyển động phủ lên trên
Vì Next.js sở hữu render, frontend có thể art-directed tùy thích mà WordPress không cần biết. Ở
Iventions điều đó nghĩa là choreography GSAP và một lớp Three.js — giữ ở 60fps bằng cách chỉ animate
transform/opacity, khởi tạo WebGL lười gần viewport, và giới hạn device pixel ratio ở
Math.min(devicePixelRatio, 2). Tôi đi sâu vào các ngân sách đó trong Core Web Vitals cho web nhiều
chuyển động. Điều này đúng cả với site nội dung
thuần: một frontend WordPress nhanh chủ yếu là đừng đẩy việc xuống client — cache mạnh, hydrate ít.
Bằng chứng nó hoạt động
Iventions — dựng headless trên WordPress với frontend Next.js — đoạt CSS Design Awards Website of the Month và Awwwards Site of the Day + Developer Award, trong khi đội biên tập quản lý nội dung bằng WordPress suốt quá trình. Toàn bộ câu chuyện nằm trong case study Iventions, và thêm nhiều dự án đã ra mắt trong kho dự án.
Câu hỏi thường gặp
Nên dùng WordPress REST API hay WPGraphQL với Next.js?
Dùng REST API cho bài/trang tiêu chuẩn khi muốn không thêm plugin và URL dễ cache. Dùng WPGraphQL khi muốn query chính xác từng field cho mỗi component — lý tưởng cho build dạng block, nặng ACF, nhiều art direction, nơi mỗi section lấy đúng thứ nó cần.
Làm sao preview bản nháp WordPress trên frontend Next.js headless?
Thêm một route preview gọi draftMode().enable(), trỏ link preview của WordPress vào đó kèm một
secret dùng chung, và trong Draft Mode fetch với cache: 'no-store' cộng header auth Application
Password để editor thấy nội dung chưa đăng mới nhất.
Vì sao tôi bị lỗi CORS từ WordPress REST API?
Vì bạn đang fetch /wp-json từ trình duyệt. Dời lời gọi sang server — RSC, một route handler, hay
generateMetadata — là server-to-server, không CORS gì cả. Tiện thể thông tin đăng nhập WordPress
không bao giờ chạm client.
Headless WordPress có hại SEO hay hiệu năng không?
Không, nếu cache đúng. Với ISR của Next.js (revalidate + invalidate theo tag) bạn phục vụ HTML tĩnh
cache trên CDN và làm mới khi đăng bài qua webhook — nhanh ngang site tĩnh hoàn toàn. Field SEO của
Yoast hay Rank Math vẫn điều khiển metadata qua generateMetadata.
Có thể chuyển một site WordPress hiện có sang frontend headless mà không dời nội dung không?
Được — đó là điểm hấp dẫn chính. Nội dung, user và media vẫn nằm nguyên trong WordPress; bạn chỉ thay theme bằng một app Next.js đọc cùng API REST/GraphQL. Bạn thậm chí có thể chạy frontend mới trên một subdomain trong lúc cắt chuyển.
Cùng dựng nhé
Nếu bạn có một site WordPress mà theme đang kìm chân — hoặc bạn đang đặt làm một site mới và muốn nó nhanh, có chuyển động, và sửa được — tách nó thành một frontend Next.js chính là việc tôi làm với vai trò fullstack creative developer.
- Xem cách tôi làm việc và các gói hợp tác ở trang dịch vụ.
- Khám phá các dự án đã ra mắt và đoạt giải trong kho dự án.
- Sẵn sàng trao đổi? Cùng nói chuyện →
Viết bởi Hon Tran — creative developer, sáng lập hontran.dev, và giám khảo Awwwards. Hơn 11 năm xây dựng trải nghiệm web đoạt giải, ưu tiên hiệu năng (Next.js, GSAP, Three.js / WebGL) cho khách hàng toàn cầu. hontran.dev · Behance.


