Skip to main content

Headless WordPress with Unyson+

This tutorial builds the content side of a headless site using only Unyson+: a Book post type with a Genre taxonomy and five custom fields, available over the WordPress REST API, then fetched from a front end in plain JavaScript or Next.js. No ACF and no custom post types plugin are needed.

New to headless? Start with How headless WordPress works.

:::info What works today, and what is coming Everything on this page works with the current Post Types and Custom Fields extensions. The planned Headless extension adds image sizes and alt text, settings and menus endpoints, preview, rebuild webhooks and Page Builder content. See the roadmap. :::

1. Activate the two extensions​

Go to Unyson+ → Extensions and activate Post Types and Custom Fields (download them first if they are not installed).

2. Create the post type and taxonomy​

Go to Unyson+ → Post Types (Post Types documentation).

  1. In Post Types, click Add Post Type: Singular Book, Plural Books, Key book. On the Visibility tab, leave Block editor / REST on (it is on by default). On the URLs tab you can set the URL slug to books.
  2. In Taxonomies, click Add Taxonomy: Singular Genre, Plural Genres, Key genre, attached to Book. Leave Show in REST on.
  3. Save.

The type is now in the API at /wp-json/wp/v2/book, and the taxonomy at /wp-json/wp/v2/genre.

tip

For common content (team members, events, testimonials, FAQs and more), the Blueprints tab creates the post type, its taxonomies and a matching field group in one click.

3. Create the field group​

Go to Unyson+ → Custom Fields and click Add Field Group (Custom Fields documentation).

  1. Group title: Book Details. Show on post types: Book.
  2. Turn on Show in REST API. Without it, the values stay out of the API.
  3. Add the fields:
Field labelField nameField type
SubtitlesubtitleText
Authorbook_authorText
PagespagesNumber
Cover imagecover_imageImage
Buy linkbuy_urlURL
  1. Save, then add a book (Books → Add New) and fill in the fields.

4. Read it from the REST API​

Request the books:

curl "https://your-site.com/wp-json/wp/v2/book?_fields=id,slug,title,genre,unysonplus_fields"

The field values come back in unysonplus_fields:

[
{
"id": 3629,
"slug": "the-quiet-garden",
"title": { "rendered": "The Quiet Garden" },
"genre": [258],
"unysonplus_fields": {
"subtitle": "A year among the weeds",
"book_author": "Sam Rivera",
"pages": 248,
"cover_image": {
"attachment_id": 3624,
"url": "//your-site.com/wp-content/uploads/2026/10/quiet-garden-cover.jpg"
},
"buy_url": "https://example.com/books/the-quiet-garden"
}
}
]

Things to know about the values:

  • Each field returns its stored value shape. Text is a string, Number a number, and an Image is an object with attachment_id and url. The URL starts with // (no scheme), so add your site's scheme in the front end. Other common shapes: a Gallery is an array of those image objects, a Repeater is an array of row objects, Checkboxes is an object of the ticked choices ({ "red": true }), and Related posts is an array of post IDs.
  • Taxonomies return term IDs ("genre": [258]). Get the names from /wp-json/wp/v2/genre, or add _embed to the request.
  • The REST field is read-only: the API returns values but does not save them.
  • unysonplus_fields requires Unyson+ Custom Fields 0.1.21 or later when you trim the response with _fields, as above. Earlier versions returned it empty unless type was also requested.

5. Build the front end​

The books are now one request away from any front end. Two complete, tested front ends for exactly this Book post type, with every file to browse and download:

  • Plain JavaScript: two HTML pages and a few JavaScript modules that fetch the books in the browser. No framework, no build step.
  • Next.js: an App Router site with a page per book, built ahead of time and refreshed from WordPress.

The same requests work from Nuxt, Astro, SvelteKit or a mobile app; only the fetching code changes.

Or define it all in code​

Steps 2 and 3 can also be code in a child theme instead of admin screens. Use this route when the definitions should live in version control, or ship with a theme you distribute. It is the same Book, with one difference: the REST field is your own, so you choose its shape. Here the cover comes back ready for an <img> tag, with its size and alt text.

headless-books (child theme)Download
inc/includes/headless-books-rest.php

Adds the field values to each book's REST response, and resolves the cover image to a URL, size and alt text.

<?php
/**
* Adds the Book Details values to the REST API as `book_fields`.
*
* Doing this in code lets you shape the output for the front end: here the
* cover comes back with its size and alt text instead of the stored
* { attachment_id, url } pair.
*/

if ( ! defined( 'FW' ) ) {
die( 'Forbidden' );
}

add_action( 'rest_api_init', 'headless_books_register_rest_fields' );

function headless_books_register_rest_fields() {
register_rest_field( 'book', 'book_fields', array(
'get_callback' => 'headless_books_rest_fields',
'schema' => array(
'description' => __( 'Book Details values.', 'headless-books' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
) );
}

/**
* @param array $post The post as prepared for the response.
*
* @return array
*/
function headless_books_rest_fields( $post ) {
$id = (int) $post['id'];

return array(
'subtitle' => (string) fw_get_db_post_option( $id, 'subtitle', '' ),
'book_author' => (string) fw_get_db_post_option( $id, 'book_author', '' ),
'pages' => (int) fw_get_db_post_option( $id, 'pages', 0 ),
'cover_image' => headless_books_rest_image( fw_get_db_post_option( $id, 'cover_image' ) ),
'buy_url' => esc_url_raw( (string) fw_get_db_post_option( $id, 'buy_url', '' ) ),
);
}

/**
* Turn a stored upload value into what a front end needs for an <img>.
*
* @param mixed $value { attachment_id, url } as saved by the upload option.
*
* @return array|null
*/
function headless_books_rest_image( $value ) {
$attachment_id = is_array( $value ) && ! empty( $value['attachment_id'] ) ? (int) $value['attachment_id'] : 0;
$src = $attachment_id ? wp_get_attachment_image_src( $attachment_id, 'large' ) : false;

if ( ! $src ) {
return null;
}

return array(
'id' => $attachment_id,
'url' => $src[0],
'width' => (int) $src[1],
'height' => (int) $src[2],
'alt' => (string) get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
);
}

How the files are loaded follows the Unyson+ theme conventions:

  • inc/includes/ — the parent theme includes every PHP file in a child theme's inc/includes/ folder automatically, so functions.php needs no require lines. Prefix the file names (headless-books-*) so they never share a name with one of the parent's.
  • framework-customizations/theme/options/posts/book.php — Unyson+ shows these options on the Book edit screen and saves each value under its option id, which fw_get_db_post_option() reads back.
  • Not straight in inc/. A child file there named like a parent file (inc/post-types.php, inc/menus.php, inc/static.php, inc/hooks.php) replaces the parent's file instead of adding to it.

The response then carries book_fields:

{
"id": 3642,
"slug": "the-quiet-garden",
"title": { "rendered": "The Quiet Garden" },
"genre": [258],
"book_fields": {
"subtitle": "A year among the weeds",
"book_author": "Sam Rivera",
"pages": 248,
"cover_image": {
"id": 3624,
"url": "https://your-site.com/wp-content/uploads/2026/10/quiet-garden-cover.jpg",
"width": 1024,
"height": 1024,
"alt": "The Quiet Garden cover"
},
"buy_url": "https://example.com/books/the-quiet-garden"
}
}

If you use this route with the Next.js example, read book_fields instead of unysonplus_fields in lib/wordpress.ts, and use the cover's url as it is (it already has its scheme).

Compared with ACF and a custom post types plugin​

ACF + CPT pluginUnyson+
Post types and taxonomies in RESTYesYes (on by default)
Custom fields in RESTPer field groupPer field group, under unysonplus_fields
Repeaters, galleries, relationshipsACF ProIncluded
Global settings in RESTACF Pro options pageComing in the Headless extension
Menus, preview, rebuild webhooksOther plugins or custom codeComing in the Headless extension
GraphQLWPGraphQL for ACFBeing considered

Current limits​

  • Images return the URL and ID only. For sizes and alt text, request the attachment from /wp-json/wp/v2/media/<id>. The Headless extension will add them directly.
  • Fields on posts only. Field groups on taxonomy terms and users are not yet in the API.
  • No settings or menus endpoints yet, and Page Builder content comes through only as content.rendered, without the CSS it needs.

All of these are on the Headless roadmap.