Skip to main content

Headless WordPress: how it works and how people build it

In a normal WordPress site, WordPress does two jobs: it stores and edits the content, and it builds the pages visitors see, using a theme. Headless WordPress keeps the first job and hands the second to a separate application. Editors still work in the WordPress admin, but visitors never load a WordPress theme. A front end built with something like Next.js, Nuxt or Astro (or a mobile app) asks WordPress for the content over an API and displays it however it likes.

"Headless" means the CMS has no head, no front end of its own. You will also see it called decoupled WordPress.

When it is worth it​

Headless suits a project when:

  • The front end is already an app. The team builds in React or Vue, and the website is one part of a larger product.
  • The same content goes to several places: a website, a mobile app, a kiosk, an email system.
  • Performance and hosting matter more than convenience. A statically built front end on a CDN is very fast and has very little to attack, since WordPress itself can sit on a private address.
  • Developers want full control of the markup, routing and build pipeline.

It is usually not worth it when:

  • The site is a brochure or blog that a normal theme handles well.
  • Editors rely on a visual page builder, plugins that add things to the front end (forms, sliders, pop-ups, SEO tags), or previewing their changes exactly as they will look.
  • There is no developer to maintain a second application. A headless site is two projects, not one.

The usual stack​

Most headless WordPress sites are built from the same four pieces.

1. Custom post types and taxonomies​

The content is modelled as types: products, team members, events, case studies, each with its own categories. People register these in a theme or plugin with register_post_type() and register_taxonomy(), or with a plugin such as CPT UI. For headless use, each type must be registered with show_in_rest => true, or it does not appear in the API.

2. Custom fields​

A post's title and body are rarely enough. A book also has an author, a page count and a cover image. Advanced Custom Fields (ACF) is the most common tool for this. Its fields can be added to the REST API (each field group has a "Show in REST API" setting), and the WPGraphQL for ACF plugin exposes them over GraphQL.

3. The API​

WordPress has two options:

  • The REST API, built into WordPress. Every public post type is available at /wp-json/wp/v2/<type>, and plugins add their own routes. It needs nothing extra, and it is easy to cache. Use ?_fields= to request only the fields you need, and ?_embed to include related objects such as the featured image.
  • GraphQL, through the WPGraphQL plugin. The front end asks for exactly the shape it wants in a single request, and the schema gives you typed queries. Popular with Gatsby, Next.js and Astro sites.

4. The front end​

The front end fetches content and builds pages from it. The main choice is when pages are built:

  • Static generation (SSG): every page is built ahead of time and served from a CDN. Fastest, but content changes need a rebuild.
  • Incremental regeneration (ISR) / revalidation: pages are cached and rebuilt in the background when content changes, usually triggered by a webhook from WordPress.
  • Server rendering (SSR): pages are built on each request. Always current, but every visit depends on WordPress answering quickly.

Next.js is the most common choice; Nuxt (Vue), Astro and SvelteKit are also popular.

What the code looks like​

Whatever the front end, the pattern is the same:

  • One file talks to WordPress. It knows the API's address and has one function per query (getBooks(), getBook(slug)). Pages never build REST URLs themselves.
  • Ask only for the fields you use with ?_fields=, so responses stay small.
  • Pages call those functions and render the result.

Each request is a plain HTTP GET, so this is the whole of "talking to WordPress":

const res = await fetch('https://your-site.com/wp-json/wp/v2/book?_fields=id,slug,title,unysonplus_fields');
const books = await res.json();

Four complete, tested examples build the same small book site, each with every file to browse and download. Pick the front end, then the plugins behind it:

With ACF + CPT UIWith Unyson+
Plain JavaScript (no framework, no build)JavaScript + ACFJavaScript + Unyson+
Next.js (pages built on the server)Next.js + ACFNext.js + Unyson+

The front-end code is nearly identical across each row. What changes is the WordPress side: two plugins or one, and where the fields sit in the response (acf or unysonplus_fields).

Other ways to do headless​

The REST API with your own front end is the most common setup, but not the only one. Here is each approach, and how Unyson+ fits into it.

REST API + any front end​

What the examples above do: WordPress serves JSON at /wp-json/, and any front end (Next.js, Nuxt, Astro, SvelteKit, plain JavaScript, a mobile app) fetches it. Built in, no extra plugins, easy to cache.

With Unyson+: works today. Post Types puts your types in the API (on by default), and Custom Fields adds the values under unysonplus_fields once a field group's Show in REST API is on.

GraphQL (WPGraphQL)​

The WPGraphQL plugin adds a /graphql endpoint. The front end asks for exactly the fields it needs, across related content, in one request, with a typed schema. Popular with Gatsby, Astro and Next.js teams.

With Unyson+: works with one small file of code (there is no GraphQL switch yet): the Post Types filters put the type in the schema, and register_graphql_fields() adds the Custom Fields values. A built-in option is under consideration on the roadmap. Guide: GraphQL (WPGraphQL), with ACF and with Unyson+.

Custom REST endpoints​

Your own address, registered with register_rest_route(), that returns exactly what a page needs in one request: genre names instead of IDs, a ready-to-use cover image, no title.rendered wrappers.

With Unyson+: a file in the child theme's inc/includes/, reading values with fw_get_db_post_option(); the ACF version is a one-file plugin. Both return the same JSON. Guide: Custom REST endpoints.

Framework kits (Faust.js and others)​

Ready-made toolkits that solve preview, routing and login for a WordPress back end: Faust.js for Next.js (built on WPGraphQL), and WordPress loaders for Astro and Nuxt.

With Unyson+: the REST-based loaders work today, the same way as the examples above. Faust.js needs GraphQL, so it inherits the GraphQL notes above.

Static export​

WordPress renders the site with its theme as usual, then a plugin such as Simply Static or WP2Static saves every page as HTML and uploads it to a CDN or static host. Visitors never reach WordPress, which can stay private. It is "headless" in the sense that the live site has no WordPress behind it.

With Unyson+: the best fit for sites built with the Unyson+ theme and page builder, because nothing is rebuilt: every page, layout and animation is already HTML, CSS and JavaScript once WordPress renders it, so the exporter captures it as it is. Anything that needs a server (forms, search, logins, the CRM) needs WordPress or an outside service behind it. Guide: Static export.

Hybrid (partly headless)​

Keep the normal WordPress site and theme, and build one part as an app that reads the API: a store finder, a filterable catalogue, a calculator, a members' dashboard.

With Unyson+: keep building pages with the page builder, and add the app part with a Code Block element (set to run the code, not show it): it holds the HTML, styles and script that fetch from the REST API. Guide: Hybrid (partly headless).

WordPress as a mobile-app back end​

The same REST API feeds an iOS or Android app (Swift, Kotlin, React Native, Flutter), often alongside the website, so editors manage both from one place.

With Unyson+: works today, exactly like the website examples: the app requests /wp-json/wp/v2/<type> and reads unysonplus_fields.

Structured page content (blocks or layouts as data)​

Instead of the finished HTML of a page, the front end receives its structure (each block or element and its settings) and renders every piece with its own component. With the block editor this means parsing the blocks; with a page builder, its layout data.

With Unyson+: not yet. Page Builder content reaches the API as finished HTML (content.rendered), which any front end can show but not take apart. Serving the builder layout as structured JSON, plus the CSS each page needs, is Phase 2 of the Headless extension roadmap.

Things to plan for​

These are the problems most teams run into after the first demo works.

  • Preview. WordPress's Preview button opens the WordPress front end, which no longer exists. You need a preview route on the front end that can fetch drafts with authentication.
  • Rebuilds. Publishing in WordPress has to tell the front end to rebuild or revalidate, usually through a webhook sent on save.
  • Page builders don't carry over. A layout built in a visual builder is stored as that builder's own format and rendered by WordPress. A headless front end gets either the finished HTML (with the CSS it depends on) or nothing useful.
  • Front-end plugins stop working. Anything that adds output to the theme (forms, SEO tags, analytics, cookie banners, related posts) has to be rebuilt in the front end or read from an API.
  • SEO metadata. Titles, descriptions, Open Graph tags and structured data must be fetched and output by the front end. Many SEO plugins add their data to the REST API for this.
  • Menus and global settings. Menus, the logo, footer text and contact details are not in the core REST API by default. Teams add an endpoint, or an ACF options page exposed to the API.
  • Authentication and CORS. Public content needs no login, but drafts, previews and form posts do. Application passwords or JWT are common, and the WordPress server must allow requests from the front end's domain.
  • Images. The front end needs the image URL, sizes and alt text, not just an attachment ID, to build responsive images.

Doing it with Unyson+​

Unyson+ can replace the plugin parts of this stack: its Post Types extension registers types and taxonomies for the API, and its Custom Fields extension puts field values in the REST API without ACF. See Headless WordPress with Unyson+ for a step-by-step example, and the roadmap for the Headless extension that will cover settings, menus, preview and builder content.