Skip to content

Headless

Use WordPress as a headless CMS. This repository contains two components:

  • 🐘 WordPress Plugin (wp-plugin/) β€” extends the WP REST API with additional fields, custom routes, preview support, and ISR cache revalidation
  • πŸ“¦ npm Package (npm-package/) β€” TypeScript types, Zod schemas and request builders for frontend consumption

🐘 WordPress Plugin

Install the plugin from WordPress.org or via the admin dashboard.

Authentication

All headless REST endpoints require the ?headless=true query parameter. That is a routing flag, not authentication β€” anyone can set it. To actually restrict access, require an API key header:

// wp-config.php
define('HEADLESS_API_KEY_HEADER_KEY', 'X-Headless-Token');
define('HEADLESS_API_KEY_HEADER_VALUE', 'your-secret');

If both constants are left empty (default), no header restriction is applied and the plugin's routes and added fields are readable by anyone who can reach the REST API. The comparison is done with hash_equals().

HEADLESS_SECRET_TOKEN is a different thing: one shared secret, sent to the frontend's /api/preview and /api/revalidate. The admin pages put it into the page so the editor can open a preview, so every user with edit_posts can read it β€” Contributor upwards. Rate-limit those endpoints on the frontend and do not treat the token as a per-user credential.

The plugin no longer forces WordPress application passwords to be available. Core offers them over HTTPS or in a local environment; to override that, use the headless_application_passwords_available filter.

REST API Extensions

The plugin adds fields to existing WP REST responses (posts, revisions, comments):

Field Description
content.headless_blocks Parsed Gutenberg blocks with prepared data
content.headless_attachment_ids Attachment IDs referenced in content
featured_media_url Full URL of the featured image
featured_media_src Image source as [url, width, height, resized]
featured_media_sizes All registered image sizes
featured_media_caption/description/alt Featured image meta
[taxonomy] Term IDs for all REST-visible taxonomies
author_user Comment author's display_name (if WP user). nickname only for requests that may list_users, since it defaults to the login name

Add ?headless_variant=teaser to strip heavy fields (block content, rendered HTML) for list views.

Custom Routes

Namespace: headless/v1

Route Description
GET /menus All registered nav menus, keyed by slug
GET /menus/{slug} Single nav menu
GET /settings Reading settings: front_page, page_on_front, home_url

Preview

Redirects the WordPress "Preview" button to your headless frontend:

define('HEADLESS_HEAD_BASE_URL', 'https://your-frontend.com');
define('HEADLESS_SECRET_TOKEN', 'your-preview-secret');

The frontend receives a request to /api/preview?secret_token=...&post={id}&post_type={type}.

Cache Revalidation (ISR)

Automatically triggers cache revalidation on your frontend when posts or comments are saved. The plugin calls your frontend's revalidation endpoint with a path or cache tag:

GET {HEADLESS_HEAD_BASE_URL}/api/revalidate?secret_token=...&tag=post-{id}
GET {HEADLESS_HEAD_BASE_URL}/api/revalidate?secret_token=...&path=/your-slug

You can also trigger revalidation programmatically:

headless_revalidate_by_post_id($post_id);
headless_revalidate_by_path('/some/path');

Extensibility

Register custom block preparations or extend route responses via WordPress actions:

add_action('headless_register_block_preparation_extensions', function($preparations) { ... });
add_action('headless_register_post_route_extensions', function($extensions) { ... });
add_action('headless_register_comment_route_extensions', function($extensions) { ... });

Two filters control what the query parameters are allowed to reach:

// Allow application passwords where core would not (e.g. plain HTTP). Off by default.
add_filter('headless_application_passwords_available', '__return_true');

// Decide per key whether hl_meta_* may query it. Protected keys (leading underscore)
// are queryable only for requests that may edit posts.
add_filter('headless_meta_key_is_queryable', function($queryable, $key) {
    return $key === '_my_public_key' ? true : $queryable;
}, 10, 2);

πŸ“¦ npm Package

npm install @palasthotel/headless

Provides TypeScript types, Zod v4 schemas and URL builders that match the plugin's REST API.

Request Builders

import {
  getPostsWithBlocksRequest,
  getPostWithBlocksRequest,
  getMenusRequest,
  getMenuRequest,
  getSettingsRequest,
} from '@palasthotel/headless';

const baseUrl = 'https://cms.example.com';

const posts = getPostsWithBlocksRequest({ baseUrl });
const menu = getMenuRequest({ baseUrl, slug: 'main-menu' });

All builders return a URL and append ?headless=true automatically. An optional second argument renames that parameter: getMenusRequest({ baseUrl }, { name: 'api', value: 'yes' }).

Schemas

import { postWithBlocksResponseSchema, settingsResponseSchema } from '@palasthotel/headless';

const post = postWithBlocksResponseSchema.parse(data);

// settingsResponseSchema is a discriminated union on front_page: "posts" | "page"
const settings = settingsResponseSchema.parse(data);

Use sustainingParse instead of .parse() to log schema errors without throwing:

import { sustainingParse, isParseError } from '@palasthotel/headless';

const result = sustainingParse(data, postWithBlocksResponseSchema);
if (isParseError(result)) { /* handle gracefully */ }

Types

import type {
  HeadlessPostResponse,
  HeadlessCommentResponse,
  MenusResponse,
  SettingsResponse,
  Block,
} from '@palasthotel/headless';

Repository layout

wp-plugin/public/ is exactly what ships to WordPress.org. Everything outside it is repository-only.

Path Description
wp-plugin/public/ the released plugin β€” plugin header, classes/, vendor/ autoloader, README.txt, plus the generated dist/
wp-plugin/src/ editor assets, built into public/dist/ by wp-scripts β€” dist/ itself is generated and gitignored
wp-plugin/headless.php DEV wrapper, loads public/headless.php when the repository is checked out into wp-content/plugins/
npm-package/ the published @palasthotel/headless package
bin/ release helper scripts
.github/workflows/ CI/CD β€” see .github/WORKFLOWS.md

Development

cd wp-plugin  && npm ci && npm run build   # editor assets β†’ public/dist/
cd npm-package && npm ci && npm run build  # tsdown β†’ dist/
npm test                                   # jest (npm-package)
npm run lint                               # tsc --noEmit (npm-package)

bash bin/pack.sh stages the payload in build/headless/ and zips it to headless.zip β€” the same payload the release deploys. It needs composer, because the packed copy gets a freshly generated --no-dev autoloader and the composer files are dropped from it.

Releasing

Both components are released by release-please with separate pull requests and version lines: npm-v* publishes the npm package, plugin-v* deploys to WordPress.org. There is nothing to bump by hand β€” commit with conventional commits and merge the release PR. Details in .github/WORKFLOWS.md, commit conventions in CONTRIBUTING.md.

License

GNU General Public License v3.0 or later β€” see LICENSE.

Versions of @palasthotel/headless up to and including 3.0.7 were published under the MIT license; those releases remain MIT. Everything from the next release on is GPL-3.0-or-later.

About

Use WordPress as a headless CMS.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

8 watching

Forks

Releases

Packages

Used by

Contributors

Languages