{post.title}
Published: {post.date}
{featuredImage && (Combining WordPress as a headless CMS with Gatsby.js gives you the best of both worlds: WordPress’s content management capabilities and Gatsby’s blazing-fast static site generation. This approach lets you leverage WordPress’s familiar interface for content creators while delivering lightning-fast websites with modern React components and GraphQL queries. You’ll learn how to set up this architecture, handle authentication, optimize performance, and troubleshoot common issues that arise when decoupling WordPress from its frontend.
The headless WordPress approach separates content management from presentation completely. WordPress serves as a backend API that delivers content via REST API or GraphQL, while Gatsby fetches this data during build time to generate static HTML files. This JAMstack architecture eliminates database queries on the frontend, resulting in faster load times and better security.
Here’s the technical flow:
The key advantage is that your frontend becomes completely independent of WordPress’s PHP stack, allowing for deployment on fast CDNs while maintaining WordPress’s content editing experience.
First, you’ll need a WordPress installation configured for headless operation. Whether you’re using a VPS or dedicated server, the setup process remains consistent.
Install and configure the WPGraphQL plugin for better query capabilities:
wp plugin install wp-graphql --activate
wp plugin install wp-graphql-acf --activate # If using Advanced Custom Fields
Add this to your WordPress theme’s functions.php to expose additional post data:
function enable_cors_headers() {
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
}
add_action('init', 'enable_cors_headers');
// Enable featured images in REST API
add_theme_support('post-thumbnails');
// Add custom fields to REST API
function add_custom_fields_to_rest() {
register_rest_field('post', 'custom_fields', array(
'get_callback' => function($post) {
return get_post_meta($post['id']);
}
));
}
add_action('rest_api_init', 'add_custom_fields_to_rest');
Configure permalink structure to “Post name” in WordPress admin for cleaner URLs, and ensure your GraphQL endpoint is accessible at /graphql
.
Initialize a new Gatsby project with the necessary plugins:
npx gatsby new wordpress-gatsby-site
cd wordpress-gatsby-site
npm install gatsby-source-wordpress gatsby-plugin-image gatsby-plugin-sharp gatsby-transformer-sharp
Configure gatsby-config.js to connect with your WordPress installation:
module.exports = {
siteMetadata: {
title: "WordPress Gatsby Site",
siteUrl: "https://yoursite.com"
},
plugins: [
{
resolve: "gatsby-source-wordpress",
options: {
url: "https://your-wordpress-site.com/graphql",
verbose: true,
develop: {
hardCacheMediaFiles: true,
},
production: {
hardCacheMediaFiles: false,
},
debug: {
graphql: {
writeQueriesToDisk: true,
},
},
html: {
useGatsbyImage: true,
},
},
},
"gatsby-plugin-image",
"gatsby-plugin-sharp",
"gatsby-transformer-sharp",
],
};
Create dynamic pages by adding gatsby-node.js:
const path = require('path');
exports.createPages = async ({ graphql, actions, reporter }) => {
const { createPage } = actions;
const result = await graphql(`
query GetWordPressData {
allWpPost(filter: {status: {eq: "publish"}}) {
nodes {
id
slug
uri
}
}
allWpPage(filter: {status: {eq: "publish"}}) {
nodes {
id
slug
uri
}
}
}
`);
if (result.errors) {
reporter.panicOnBuild("Error loading WordPress data", result.errors);
return;
}
const { allWpPost, allWpPage } = result.data;
// Create post pages
allWpPost.nodes.forEach(post => {
createPage({
path: post.uri,
component: path.resolve('./src/templates/post.js'),
context: {
id: post.id,
},
});
});
// Create static pages
allWpPage.nodes.forEach(page => {
createPage({
path: page.uri,
component: path.resolve('./src/templates/page.js'),
context: {
id: page.id,
},
});
});
};
Create a post template at src/templates/post.js:
import React from "react";
import { graphql } from "gatsby";
import { GatsbyImage, getImage } from "gatsby-plugin-image";
const PostTemplate = ({ data }) => {
const post = data.wpPost;
const featuredImage = getImage(post.featuredImage?.node?.localFile);
return (
{post.title}
Published: {post.date}
{featuredImage && (
)}
);
};
export const query = graphql`
query GetPost($id: String!) {
wpPost(id: { eq: $id }) {
title
content
date(formatString: "MMMM DD, YYYY")
featuredImage {
node {
altText
localFile {
childImageSharp {
gatsbyImageData(
width: 800
placeholder: BLURRED
formats: [AUTO, WEBP, AVIF]
)
}
}
}
}
categories {
nodes {
name
slug
}
}
tags {
nodes {
name
slug
}
}
}
}
`;
export default PostTemplate;
For handling custom fields and ACF data, extend your queries:
export const query = graphql`
query GetPostWithACF($id: String!) {
wpPost(id: { eq: $id }) {
title
content
acfFields {
customHeading
customContent
customImage {
localFile {
childImageSharp {
gatsbyImageData(width: 600)
}
}
}
}
}
}
`;
Here’s a performance comparison between traditional WordPress and the headless approach:
Metric | Traditional WordPress | Headless WordPress + Gatsby | Improvement |
---|---|---|---|
Time to First Byte (TTFB) | 800ms – 1.2s | 50ms – 150ms | 85-90% faster |
Lighthouse Performance Score | 60-75 | 90-100 | 30-40% improvement |
Database Queries per Page | 20-50+ | 0 | 100% reduction |
CDN Compatibility | Limited | Full static assets | Complete |
Implement these optimization strategies:
Add this caching configuration to your WordPress .htaccess:
# API Caching
Header set Cache-Control "public, max-age=300"
Header set Access-Control-Allow-Origin "*"
# GraphQL Caching
Header set Cache-Control "public, max-age=300"
Header set Access-Control-Allow-Origin "*"
For member-only content or authenticated areas, you’ll need a hybrid approach since Gatsby generates static files. Implement client-side authentication:
// src/components/ProtectedContent.js
import React, { useState, useEffect } from "react";
const ProtectedContent = ({ children }) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
const checkAuth = async () => {
try {
const token = localStorage.getItem('wp_token');
if (!token) {
setLoading(false);
return;
}
const response = await fetch('https://your-wp-site.com/wp-json/wp/v2/users/me', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
setIsAuthenticated(true);
}
} catch (error) {
console.error('Auth check failed:', error);
}
setLoading(false);
};
checkAuth();
}, []);
if (loading) return Loading...;
if (!isAuthenticated) return Please log in to view this content.;
return children;
};
export default ProtectedContent;
This architecture excels in several scenarios:
A practical example for an e-commerce integration:
// gatsby-node.js - Creating product pages from WordPress + external API
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions;
// Get WordPress posts
const wpData = await graphql(`
query {
allWpPost {
nodes {
id
slug
acfFields {
productId
}
}
}
}
`);
// Fetch additional product data from e-commerce API
const productsWithDetails = await Promise.all(
wpData.data.allWpPost.nodes.map(async (post) => {
if (post.acfFields?.productId) {
const productResponse = await fetch(
`https://api.shopify.com/products/${post.acfFields.productId}`
);
const productData = await productResponse.json();
return { ...post, productData };
}
return post;
})
);
// Create pages with combined data
productsWithDetails.forEach((post) => {
createPage({
path: `/products/${post.slug}`,
component: path.resolve('./src/templates/product.js'),
context: {
id: post.id,
productData: post.productData,
},
});
});
};
Here are the most frequent problems and their solutions:
Build failures due to missing images:
// Add error handling in gatsby-node.js
exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions;
createTypes(`
type WpMediaItem implements Node {
localFile: File
}
`);
};
// Handle missing images in components
const featuredImage = post.featuredImage?.node?.localFile
? getImage(post.featuredImage.node.localFile)
: null;
CORS issues during development:
# Add to WordPress .htaccess
Header always set Access-Control-Allow-Origin "*"
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header always set Access-Control-Allow-Headers "Authorization, Content-Type"
# Handle preflight requests
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=200,L]
Slow build times with large content volumes:
// Enable incremental builds in gatsby-config.js
module.exports = {
flags: {
INCREMENTAL_BUILD: true,
FAST_DEV: true,
},
plugins: [
{
resolve: "gatsby-source-wordpress",
options: {
// Limit content during development
develop: {
nodeUpdateInterval: 5000,
hardCacheMediaFiles: true,
},
production: {
hardCacheMediaFiles: false,
},
},
},
],
};
Memory issues during builds:
# Increase Node.js memory limit
export NODE_OPTIONS="--max-old-space-size=4096"
gatsby build
# Or add to package.json scripts
"scripts": {
"build": "NODE_OPTIONS='--max-old-space-size=4096' gatsby build"
}
While WordPress + Gatsby is powerful, consider these alternatives based on your requirements:
Solution | Best For | Pros | Cons |
---|---|---|---|
WordPress + Gatsby | Content-heavy sites, familiar CMS | WordPress ecosystem, fast frontend | Complex setup, build times |
Strapi + Gatsby | Custom content structures | Modern API, flexible | Learning curve, smaller ecosystem |
Contentful + Gatsby | Teams wanting hosted CMS | Hosted, developer-friendly | Cost scales with usage |
WordPress + Next.js | Server-side rendering needs | Dynamic rendering, API routes | More complex deployment |
For maximum flexibility, consider implementing webhook-triggered builds for near real-time content updates:
// WordPress webhook function
function trigger_gatsby_build($post_id) {
if (wp_is_post_revision($post_id)) return;
$webhook_url = 'https://api.netlify.com/build_hooks/your-build-hook-id';
wp_remote_post($webhook_url, array(
'method' => 'POST',
'body' => json_encode(array(
'trigger' => 'content_update',
'post_id' => $post_id
)),
'headers' => array(
'Content-Type' => 'application/json',
),
));
}
add_action('save_post', 'trigger_gatsby_build');
add_action('delete_post', 'trigger_gatsby_build');
This setup provides the content management benefits of WordPress with the performance advantages of static site generation, while maintaining the flexibility to scale and adapt as your requirements evolve. The key is starting simple and adding complexity only when needed, ensuring your content creators can work efficiently while your frontend delivers optimal user experience.
For more advanced implementations, explore Gatsby’s official WordPress documentation and the WPGraphQL documentation for deeper integration possibilities.
This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.
This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.