Headless WordPress + GraphQL
Launch a headless WordPress stack that feeds this site through WPGraphQL, Apollo Client, and the pagination helpers we reuse across our blog examples.
When to use
- You need marketing or editorial teams to keep using WordPress while shipping the front end in Next.js.
- You want GraphQL schemas with type safety and control rather than REST payloads.
- You have content-heavy sections (blog, resources, changelog) that rely on cursor-based pagination.
Checklist
- WordPress instance with WPGraphQL enabled and public schema accessible.
- Authentication strategy defined (JWT or Application Passwords) and tokens scoped to queries/mutations we need.
- Environment variables configured locally and in deployment targets for endpoint, auth, and preview secrets.
- Apollo Client configured in Next.js with persistent caching and pagination helpers.
- Pagination fragments implemented so list pages reuse the same cursor logic as the blog starter.
Step-by-step
1. Prepare WordPress
- Install the WPGraphQL plugin.
- If you need preview or private content, also install WPGraphQL JWT Authentication or enable Application Passwords (WordPress 5.6+).
- Under GraphQL > Settings, make sure “Public Introspection” is enabled so the schema can be fetched during build-time type generation.
- Create at least one user with the appropriate capabilities (
Editorfor content management,Administratorfor schema changes).
2. Configure authentication
- For JWT: generate a secret key in
wp-config.php(define( 'GRAPHQL_JWT_AUTH_SECRET_KEY', 'super-secret' );). - For Application Passwords: generate a password for an API-only user and note the base64 encoded
user:passwordstring. - Decide which flows will require auth headers: drafts, preview, or mutations (comments, reactions).
- Document the header shape (
Authorization: Bearer <token>for JWT,Authorization: Basic <base64>for App Passwords).
3. Define environment variables
Add the following entries to .env.local and to your deployment provider:
NEXT_PUBLIC_WORDPRESS_URL=https://your-site.com
WORDPRESS_GRAPHQL_ENDPOINT=https://your-site.com/graphql
WORDPRESS_AUTH_HEADER="Authorization=Bearer <token>"
WORDPRESS_PREVIEW_SECRET=<random-string>Use NEXT_PUBLIC_WORDPRESS_URL anywhere the front end needs to link back to the canonical WordPress URL. Keep the private auth header and preview secret out of the NEXT_PUBLIC_* namespace.
4. Configure Apollo Client
- Create a singleton Apollo client (for example in
lib/apollo.js) that readsWORDPRESS_GRAPHQL_ENDPOINTandWORDPRESS_AUTH_HEADERfromprocess.env. - Enable
InMemoryCachewithtypePoliciesforRootQueryconnections (mergeuses cursor uniqueness) and for any custom post type connections. - Reuse the pagination helpers from the blog example (
relayStylePaginationfrom@apollo/client/utilities). Pair it with fragments that spreadpageInfo { hasNextPage endCursor }so the UI can request more items. - Wrap your Next.js
AppwithApolloProviderand ensure SSR mode is enabled for static pages (ssrMode: typeof window === 'undefined').
5. Hook up pagination patterns
- Model connection queries with variables
{ first, after }(forward pagination) or{ last, before }(reverse) so they match the blog components. - Expose helper hooks, e.g.
usePostsConnectionthat wraps ApollouseQueryand returns{ nodes, pageInfo, fetchMore }. - In UI components, pass the
endCursorintofetchMore({ variables: { after: pageInfo.endCursor } })and merge results in the cache policy instead of manually concatenating arrays. - For static generation, prefetch the first page in
getStaticProps, then callfetchMoreon the client when the reader clicks “Load more” just like the blog listing.
Gotchas
- Disable GraphQL introspection in production only if your build step already fetched schema metadata; otherwise type generation will fail.
- JWT plugins often require HTTPS to issue tokens; if using local tunnels, trust the certificate or test with Application Passwords instead.
- When revalidating ISR pages, ensure the preview secret matches the
WORDPRESS_PREVIEW_SECRETotherwise/api/revalidatehandlers will return 401. - Apollo cache keys must include
__typenameandid; missing IDs in custom post types require you to set akeyFieldsoverride. - WordPress stores draft content in revisions. If your GraphQL viewer lacks permission, preview queries will return
nullnodes, so guard components accordingly.
Connection Code
npm install @apollo/client graphqlimport { ApolloClient, InMemoryCache } from '@apollo/client';
const client = new ApolloClient({
uri: `${process.env.NEXT_PUBLIC_WORDPRESS_URL}/?graphql`,
cache: new InMemoryCache(),
headers: {
authorization: 'Basic ' + process.env.NEXT_PUBLIC_WORDPRESS_AUTH_TOKEN,
},
defaultOptions: {
watchQuery: {
fetchPolicy: 'no-cache',
},
query: {
fetchPolicy: 'no-cache',
},
mutate: {
fetchPolicy: 'no-cache',
},
},
});
export default client;How To use
Basic Query Example
import { gql } from '@apollo/client';
export const Post = gql`
query post($slug: String) {
postBy(slug: $slug) {
id
title
date
slug
uri
author {
node {
name
username
avatar {
url
}
}
}
categories {
nodes {
categoryId
name
uri
}
}
content(format: RENDERED)
}
}
`;Using the Query in a Component
// pages/post/[slug].js
import { useQuery } from '@apollo/client';
import { Post } from '../../queries/Post';
export default function PostPage({ slug }) {
const { data, loading, error } = useQuery(Post, {
variables: { slug }
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!data?.postBy) return <div>Post not found</div>;
const post = data.postBy;
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
<div>
<p>By {post.author.node.name}</p>
<p>Published: {post.date}</p>
{post.categories.nodes.map(category => (
<span key={category.categoryId}>{category.name}</span>
))}
</div>
</article>
);
}