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.zipfrom 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
2.1 Turn on pretty permalinks
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:
- General tab: Singular label Book, Plural label Books, Key / slug
book. Keep the Supports boxes Title, Editor and Featured image ticked. - Visibility tab: check that Block editor / REST is on (the default). Leave REST base empty,
so the address is
/wp-json/wp/v2/book. - URLs tab (optional): URL slug
books. - Close the popup.
2.4 Create the Genre taxonomy
Still on Unyson+ → Post Types, click Add Taxonomy:
- Singular Genre, Plural Genres, Key
genre, Post types: Book, Hierarchical on (so genres work like categories). - Visibility & URLs: check that Show in REST is on (the default).
- 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:
- Group title: Book Details. Show on post types: Book.
- Turn on Show in REST API ("Expose this group's field values under unysonplus_fields"). Without it the fields never appear in the API.
- Under Fields, Add Field for each:
| Field label | Field name | Field type |
|---|---|---|
| Subtitle | subtitle | Text |
| Author | book_author | Text |
| Pages | pages | Number |
| Cover image | cover_image | Image |
| Buy link | buy_url | URL |
The Field name is the key the API uses, so type it exactly as shown.
- 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.
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 plainhttp://; 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.htmlhas an empty list and a<template>for one book card;book.htmlhas an empty article. - One file talks to WordPress.
js/wordpress.jsholds the fetch helper, one function per query andabsoluteUrl(), which gives the//image URLs your site's scheme. - Each page script fills its page.
js/books.jscopies the card template once per book;js/book.jsreads?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 see | Why | Fix |
|---|---|---|
A WordPress "Page not found" page, or a 404, at /wp-json/… | Permalinks are set to Plain | Settings → Permalinks: choose Post name, Save Changes |
{"code":"rest_no_route", …} | The post type is not in the REST API, or its slug is not book | Check 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 books | Publish at least one book; drafts are not public |
No acf object | The field group's Show in REST API is off | ACF → Field Groups → Book Details → Group Settings → Show in REST API on |
"cover_image": 3659 (a number) with ACF | acf_format=standard is missing from the request | Keep acf_format=standard in the request, and the Image field's Return Format on Image Array |
"acf": [] or fields missing | Field names differ from the code | Field names must be exactly subtitle, book_author, pages, cover_image, buy_url |
No unysonplus_fields object | The field group's Show in REST API is off, or Custom Fields is not active | Unyson+ → 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 covers | The site is on http:// and the front end on https:// (mixed content), or no cover was set | Put 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 address | Serve the folder with a web server; check WP_URL; read the error in the browser console (F12) |
WordPress returned 401 or 403 | A security plugin or host rule blocks the REST API for visitors | Allow public GET requests to /wp-json/wp/v2/ in that plugin or rule |
Next.js: Set WP_URL in .env.local | The environment variable is missing | Create .env.local locally, or add WP_URL in the host's settings |
| Next.js: a new book does not appear | The page is cached for up to five minutes | Wait, or lower revalidate in lib/wordpress.ts |
What changes from the ACF version
| ACF + CPT UI | Unyson+ | |
|---|---|---|
| Plugins | Two | One (two extensions) |
| Fields in the response | acf | unysonplus_fields |
| Extra query parameter | acf_format=standard | None |
pages | String with acf_format=standard | Number |
| Cover image | Object with URL, sizes, alt | ID and // URL (sizes and alt planned) |
The rest of the code is identical: compare the two js/wordpress.js files.