Skip to main content

Headless WordPress with JavaScript and Unyson+

The same plain-JavaScript front end as JavaScript + ACF, with one plugin instead of two: Unyson+'s Post Types extension registers the content type and its Custom Fields extension adds the fields and puts them in the REST API. The front end is two HTML pages that fetch everything in the browser, with no framework and no build step.

With Next.js instead: Next.js + Unyson+. The WordPress side in more detail, including a version defined in code: Headless with Unyson+.

1. Before you start​

You need:

  • A WordPress site you can log in to as an administrator: on any host, or on your own computer (LocalWP, XAMPP). It needs to be reachable from wherever the front end runs.
  • Unyson+ installed and active (download unysonplus.zip from the installation guide and upload it under Plugins → Add New → Upload Plugin).
  • A text editor, and a way to serve a folder over HTTP for testing: Node.js (npx serve) or Python (python -m http.server).

About 15 minutes for WordPress, and 10 for the front end.

2. Set up WordPress​

Go to Settings → Permalinks and choose anything except Plain (for example Post name), then Save Changes. The REST API lives at /wp-json/, and that address only works with pretty permalinks. (With Plain permalinks the same data is at /?rest_route=/wp/v2/book, which the examples do not use.)

2.2 Activate the two extensions​

Unyson+ must be installed and active (Plugins). Then go to Unyson+ → Extensions, and Download (if needed) and Activate Post Types and Custom Fields. Both then appear in the Unyson+ menu.

2.3 Create the Book post type​

Unyson+ → Post Types, then Add Post Type:

  1. General tab: Singular label Book, Plural label Books, Key / slug book. Keep the Supports boxes Title, Editor and Featured image ticked.
  2. Visibility tab: check that Block editor / REST is on (the default). Leave REST base empty, so the address is /wp-json/wp/v2/book.
  3. URLs tab (optional): URL slug books.
  4. Close the popup.

2.4 Create the Genre taxonomy​

Still on Unyson+ → Post Types, click Add Taxonomy:

  1. Singular Genre, Plural Genres, Key genre, Post types: Book, Hierarchical on (so genres work like categories).
  2. Visibility & URLs: check that Show in REST is on (the default).
  3. Close the popup, then Save Changes at the bottom of the page. The overview table at the top should now show both as Registered.

2.5 Create the fields​

Unyson+ → Custom Fields → Add Field Group:

  1. Group title: Book Details. Show on post types: Book.
  2. Turn on Show in REST API ("Expose this group's field values under unysonplus_fields"). Without it the fields never appear in the API.
  3. Under Fields, Add Field for each:
Field labelField nameField type
SubtitlesubtitleText
Authorbook_authorText
PagespagesNumber
Cover imagecover_imageImage
Buy linkbuy_urlURL

The Field name is the key the API uses, so type it exactly as shown.

  1. Close the popup and Save Changes.

2.6 Add some content​

Books → Genres: add one or two (Fiction, Non-fiction). Then Books → Add New for two or three books: a title, some text, a genre, and every Book Details field (the box below the editor), including a cover image. Publish each one (drafts are not in the public API).

2.7 Check the API​

Open this address in your browser, with your own domain:

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

You should see your books as JSON, each with a unysonplus_fields object holding the five fields. If you see something else, go to Troubleshooting.

What the API returns​

Unyson+ puts the fields under unysonplus_fields on each post. No extra query parameter is needed:

curl "https://your-site.com/wp-json/wp/v2/book?slug=the-quiet-garden&_fields=id,slug,genre,unysonplus_fields"
[
{
"id": 3629,
"slug": "the-quiet-garden",
"genre": [258],
"unysonplus_fields": {
"subtitle": "A year among the weeds",
"book_author": "Sam Rivera",
"pages": 248,
"cover_image": {
"attachment_id": 3630,
"url": "//your-site.com/wp-content/uploads/2026/10/quiet-garden-cover.jpg"
},
"buy_url": "https://example.com/books/the-quiet-garden"
}
}
]

Compared with ACF: pages stays a number, and the image is its ID plus a URL that starts with //, so the code adds your site's scheme. Image sizes and alt text in the response are on the Headless extension roadmap.

3. The code​

Click any file to open it, or download the whole project.

headless-books (JavaScript + Unyson+)Download
index.html

The books list page. The list is empty in the HTML; js/books.js fills it from the API using the <template> card.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Books</title>
<link rel="stylesheet" href="css/style.css">
<script type="module" src="js/books.js"></script>
</head>
<body>
<main>
<h1>Books</h1>
<!-- js/books.js fills this list from the WordPress REST API. -->
<p id="status">Loading books…</p>
<ul id="books" class="books"></ul>
</main>

<!-- One book card. The script copies it once per book and fills in the slots. -->
<template id="book-card">
<li class="book">
<a class="book-link">
<img class="book-cover" alt="">
<h2 class="book-title"></h2>
</a>
<p class="book-subtitle"></p>
<p class="book-meta"></p>
</li>
</template>
</body>
</html>

4. Set up the front end​

4.1 Get the code​

Click Download on the code window above and unzip it anywhere on your computer. You get one folder with index.html, book.html, js/ and css/. There is nothing to install.

4.2 Point it at your site​

Open js/config.js in a text editor and set your WordPress address, with no slash at the end:

export const WP_URL = 'https://your-site.com';

4.3 Run it on your computer​

The pages must be opened through a web server, not by double-clicking index.html (browsers only run JavaScript modules from a web address). Any small server works. From inside the folder, run one of:

npx serve . # needs Node.js; prints an address such as http://localhost:3000
python -m http.server 8000 # needs Python; then open http://localhost:8000

Open the address it prints. You should see your books, with their covers, and each one opens its own page.

4.4 Put it online​

The front end is just files, so any static host can serve it, for free in most cases: Netlify (drag the folder onto its dashboard), Cloudflare Pages, GitHub Pages, or the public folder of any web host. Upload the whole folder as it is.

  • Use HTTPS on both sides. A front end on https:// cannot load content from a WordPress site on plain http://; the browser blocks it as mixed content.
  • No CORS setup is needed. WordPress lets other sites read its public REST API.
  • Updates are instant. The browser fetches from WordPress on every visit, so a book you publish shows up on the next page load.

How it fits together​

  • The HTML is a shell. index.html has an empty list and a <template> for one book card; book.html has an empty article.
  • One file talks to WordPress. js/wordpress.js holds the fetch helper, one function per query and absoluteUrl(), which gives the // image URLs your site's scheme.
  • Each page script fills its page. js/books.js copies the card template once per book; js/book.js reads ?slug= from the address and fills in the article.
  • Titles and content arrive as HTML. Titles are converted to plain text before they go into textContent; the post body is your own site's HTML and is inserted as it is.

Troubleshooting​

What you seeWhyFix
A WordPress "Page not found" page, or a 404, at /wp-json/…Permalinks are set to PlainSettings → Permalinks: choose Post name, Save Changes
{"code":"rest_no_route", …}The post type is not in the REST API, or its slug is not bookCheck Show in REST API on the post type, and its slug. Then Settings → Permalinks → Save Changes to refresh the addresses
[] (an empty list)No published booksPublish at least one book; drafts are not public
No acf objectThe field group's Show in REST API is offACF → Field Groups → Book Details → Group Settings → Show in REST API on
"cover_image": 3659 (a number) with ACFacf_format=standard is missing from the requestKeep acf_format=standard in the request, and the Image field's Return Format on Image Array
"acf": [] or fields missingField names differ from the codeField names must be exactly subtitle, book_author, pages, cover_image, buy_url
No unysonplus_fields objectThe field group's Show in REST API is off, or Custom Fields is not activeUnyson+ → Custom Fields → Book Details → Show in REST API on, then Save Changes
"unysonplus_fields": []The request's _fields leaves out type (needed by Custom Fields before 0.1.21)Keep type in _fields, as the examples do, or update Unyson+
Books show but no coversThe site is on http:// and the front end on https:// (mixed content), or no cover was setPut WordPress on HTTPS; check each book has a cover
The page stays on "Loading…" (plain JavaScript)Opened from the disk, or js/config.js has the wrong addressServe the folder with a web server; check WP_URL; read the error in the browser console (F12)
WordPress returned 401 or 403A security plugin or host rule blocks the REST API for visitorsAllow public GET requests to /wp-json/wp/v2/ in that plugin or rule
Next.js: Set WP_URL in .env.localThe environment variable is missingCreate .env.local locally, or add WP_URL in the host's settings
Next.js: a new book does not appearThe page is cached for up to five minutesWait, or lower revalidate in lib/wordpress.ts

What changes from the ACF version​

ACF + CPT UIUnyson+
PluginsTwoOne (two extensions)
Fields in the responseacfunysonplus_fields
Extra query parameteracf_format=standardNone
pagesString with acf_format=standardNumber
Cover imageObject with URL, sizes, altID and // URL (sizes and alt planned)

The rest of the code is identical: compare the two js/wordpress.js files.