---
title: Headless WordPress
description: WordPress as an editor and a locked-down WPGraphQL API, with a vinext front-end on Workers that stays cached until an edit is published.
---

Two apps make up this reference: [`cms-wordpress-headless`](https://github.com/patronage/launch/tree/main/apps/cms-wordpress-headless), WordPress in Docker as the editor and API, and [`web-wordpress`](https://github.com/patronage/launch/tree/main/apps/web-wordpress), the vinext front-end that deploys as a Worker.

## How it fits together

| Concern | WordPress | Front-end |
| --- | --- | --- |
| Content | WPGraphQL at `/graphql` | Typed queries generated from a committed schema snapshot |
| Locked queries | WPGraphQL Smart Cache runs only registered queries | Requests send a query ID, never query text |
| Revalidation | A Patronage mu-plugin sends the paths and tags that changed | A route handler calls `revalidatePath` and `revalidateTag` |
| Preview | The Preview button links to the front-end with a signed, short-lived token | Draft mode, reading the latest revision with the `X-GraphQL-Preview` header |

Pages are cached in KV until WordPress says they changed, with an hourly fallback. Each page carries cache tags for every post, term and menu it read, so editing one page expires that page and the pages that show it, and leaves the rest cached. Public pages stay cacheable because draft mode only runs on the preview route.

## Editing

The fields live in PHP and are shared with the [Timber install](/references/timber-wordpress) (see [ADR 0010](https://github.com/patronage/launch/blob/main/docs/adr/0010-wordpress-fields-in-php.md) and [ADR 0015](https://github.com/patronage/launch/blob/main/docs/adr/0015-shared-wordpress-fields.md)):

- **Hero**, on pages and posts, in the same five variants as Payload.
- **Page Builder**, on pages: one flexible content layout per section in the content model, each with a Section options tab. Pages hide the block editor; their content is their sections.
- **Rich text** takes the content model's blocks as shortcodes, such as `[launch_button]` and `[launch_embed]`.
- **Forms** are [Formidable Forms](https://wordpress.org/plugins/formidable/) (free). A form section exposes its form to WPGraphQL, and the front-end posts submissions to a REST route that only it can call. WordPress checks each value and saves the entry.

## What the research settled

- Install WPGraphQL from its GitHub release zips through Composer. Its Packagist package stopped at an old version.
- Preview with the `X-GraphQL-Preview` header. The `asPreview` argument is deprecated.
- Read drafts with a WordPress application password, so no JWT plugin is needed.
- Sign preview links instead of putting a shared secret in the URL.
- Lock WPGraphQL to the front-end's own queries, and keep that setting in git.

The detail is in [vinext on headless WordPress](/research/vinext-on-headless-wordpress).

## Run it

```bash
pnpm --filter cms-wordpress-headless run setup   # composer install, Docker, WordPress and demo content
pnpm --filter web-wordpress dev              # http://localhost:3000
```

The admin is at `http://localhost:8080/wp-admin/`. The local login is in the app's README. The install deploys to its host with the WordPress deploy kit; see [Deploying WordPress](/deploy/wordpress).

Both WordPress installs include the `patronage-logging` mu-plugin. It stays off until you configure it. See [Logging](/references/logging).
