Headless WordPress with Next.js: how I wire the front

Table of Contents
Table of Contents
I use WordPress as the CMS. Next.js is the site. That is the split.
The visitor hits Next.js. WordPress stores the post. I do not treat WP as the public page.
I’m Aris Setiawan. I ship this on client work and on madebyaris.com. This site is Next.js on the front. The CMS is wp.madebyaris.com. I already wrote when headless WordPress is worth it. That post is the why. This one is the practice: how I wire a headless WordPress Next.js front in App Router.
The hire doors are headless WordPress development and Hire a Next.js developer. This is the wiring, not a sales page.
WP stays the CMS
WordPress holds posts, drafts, media, and the editor the client already knows. That is the job. I do not ask it to paint the public HTML.
A theme is not the site. A page builder is not the site. If a plugin is “the front,” I am not doing headless. I am running two sites and hoping they agree.
On madebyaris.com I write in wp.madebyaris.com. Status, slug, featured image, Gutenberg blocks. The public URL is a Next.js route. WP never sees that request.

Next.js owns the request
App Router. A folder is a URL. page.tsx is a Server Component. It fetches. It tags the result. The visitor never waits on a PHP theme.
I already wrote the App Router I keep in `app/`. The headless case does not invent a new tree. Same layout, same small client boundary, same cache I can walk after a write.
If the request lands on WordPress and Next.js “pretty-prints” it later, that is not the split. Next.js owns the request from the first byte.
How I fetch
REST. GET /wp-json/wp/v2/posts?slug=.... I ask for the fields I use: title, slug, content, excerpt, featured image, status. I do not pull the whole kitchen.
I fetch in the Server Component. I pass next: { tags: ['posts', 'post:' + slug] }. Public list can cache. A cookie or a draft cannot.

I do not open a GraphQL layer unless the product already has one I can name. A mystery GraphQL stack on top of REST I already trust is a second contract I have to debug at 1am.
Preview uses a secret. The public list does not. If I mix those two fetches, a draft leaks or a public page goes no-store for no reason.
Draft and preview
Draft stays in WordPress. I do not flip a post to publish so I can “see it.”
Preview is a Next.js route. It reads the draft with an application password or a preview nonce. no-store. If I cache a draft, the next editor sees yesterday’s sentence.
The public slug route 404s a draft. That is the point. Status is the gate. The front does not invent a second status field.
What I revalidate after a publish
I name the tags before I write the hook.
posts for the blog index. post:{slug} for the single URL. home only if the homepage lists posts.

A publish hook hits a Next.js route. It checks a secret. It calls revalidateTag or revalidatePath. Then the next visitor gets the new HTML.
If I cannot name the tag, the edit sits invisible until someone redeploys. I have already sat in that chair. I will not ship it again.
I do not bust the whole site because one post changed. That is how you turn a publish into a stampede.
What I skip
Rewriting when headless is worth it. That page already owns the why.
Treating WordPress as the site and Next.js as a pretty cache.
A GraphQL schema I cannot walk in one sitting.
Caching a draft so preview “feels fast.”
A weekend “headless rewrite” with no owner for the front. Same failure as a Pages-to-App-Router big bang.
What I will take
I will take a WordPress site that needs a Next.js front, or a Next.js front that still treats WP as the HTML. Fetch, draft, preview, a tag I can bust after publish. I will not take a rewrite that leaves two publics live and hopes they stay in sync.
If you want that owned, that is headless WordPress development or Hire a Next.js developer. Send the CMS URL and the front repo.



