WordPress headless with WP GraphQL + Laravel
WordPress as a headless CMS plus Laravel as the frontend or API consumer gives you the best of both worlds: WordPress’ content editing power and Laravel’s developer ergonomics. In this guide I’ll walk you through the architecture, core steps, code snippets, caching tips and deployment notes so you can build a robust headless site fast.
Headless WordPress with WPGraphQL + Laravel: A Practical Guide
Why go headless?
A headless approach separates content management from presentation. You keep WordPress for editing and content modeling while using a framework like Laravel to render pages, handle complex business logic, or provide a single-page app experience. Benefits include:
- Faster frontends and better UX control
- Easier multi-channel publishing (web, mobile)
- Cleaner, decoupled codebases that scale independently
Architecture overview
At a high level the flow looks like:
- WordPress (admin) — content stored, WPGraphQL exposes data.
- WPGraphQL — GraphQL endpoint served by WordPress.
- Laravel — consumes WPGraphQL, renders HTML or offers its own API to clients.
Use the featured image above to illustrate this flow in your post.
What you need
- A WordPress install with the WPGraphQL plugin active.
- Laravel (latest stable) for the frontend/consumer.
- Optional: Vite, Tailwind, or a frontend framework (Vue/React) if you want SPA behavior.
- Redis or memcached for server-side caching (recommended).
Step 1 – Prepare WordPress
- Install and activate:
- WPGraphQL
- WPGraphiQL (optional, great for testing queries)
- Custom Post Type UI or register post types via code for any content types you need
- Expose fields you want in GraphQL. If you use ACF, install WPGraphQL for ACF to expose custom fields.
- Test queries with GraphiQL or an HTTP client:
# Example: fetch recent posts
query RecentPosts {
posts(first: 10) {
nodes {
id
title
date
uri
excerpt
featuredImage {
node {
sourceUrl
altText
}
}
}
}
}
Step 2 – Set up Laravel to consume WPGraphQL
Use a small GraphQL client in Laravel. You can use softonic/graphql-client, mghoneimy/php-graphql-client, or a simple HTTP POST with Guzzle.
Example using Guzzle in a service class:
// app/Services/WpGraphqlService.php
namespace App\Services;
use GuzzleHttp\Client;
class WpGraphqlService
{
protected $client;
protected $endpoint;
public function __construct()
{
$this->endpoint = config('services.wpgraphql.endpoint'); // set in .env
$this->client = new Client();
}
public function query(string $query, array $variables = [])
{
$response = $this->client->post($this->endpoint, [
'json' => [
'query' => $query,
'variables' => $variables,
],
'timeout' => 10,
]);
return json_decode((string) $response->getBody(), true);
}
}
Add the config in .env:
WPGRAPHQL_ENDPOINT=https://your-wordpress-site.com/graphql
Step 3 – Fetch content and cache it
GraphQL responses are great to cache server-side to avoid hitting WordPress for every request. Use Laravel cache (Redis) and invalidate intelligently.
Example controller:
// app/Http/Controllers/BlogController.php
namespace App\Http\Controllers;
use App\Services\WpGraphqlService;
use Illuminate\Support\Facades\Cache;
class BlogController extends Controller
{
protected $wp;
public function __construct(WpGraphqlService $wp)
{
$this->wp = $wp;
}
public function index()
{
return Cache::remember('wp:recent-posts', 300, function () {
$query = <<<'GRAPHQL'
query RecentPosts {
posts(first: 10) {
nodes {
id
title
excerpt
uri
}
}
}
GRAPHQL;
$res = $this->wp->query($query);
return $res['data']['posts']['nodes'] ?? [];
});
}
}
Cache TTL depends on how often your content changes. For editorial sites, 5–15 minutes is common. For landing pages, longer caches are fine.
Step 4 – Routing and rendering
You can either:
- Render server-side views in Laravel (Blade) using the GraphQL data, or
- Build an SPA (Vue/React) that requests Laravel endpoints which in turn fetch GraphQL data.
Example Blade usage:
<!-- resources/views/blog/index.blade.php -->
@extends('layouts.app')
@section('content')
<h1>Latest posts</h1>
@foreach($posts as $post)
<article>
<h2><a href="{{ $post['uri'] }}">{{ $post['title'] }}</a></h2>
<div>{!! $post['excerpt'] !!}</div>
</article>
@endforeach
@endsection
If you prefer SPA, return JSON from a Laravel route and let the frontend library render it.
Step 5 – Authentication and previews
If you need previewing in WordPress (logged-in editors seeing draft content), there are a few strategies:
- Temporarily pass cookies/auth headers from the browser to Laravel when previewing.
- Use a preview token: WordPress returns a preview token that Laravel can forward to WPGraphQL with the proper cookie to fetch drafts.
- Keep a preview route in Laravel that proxies the request to WPGraphQL including the editor’s cookies.
Implementing previews securely requires care — avoid exposing admin cookies to third parties.
Step 6 – Image handling and performance
For images, either:
- Use WordPress URLs directly (fast and simple), or
- Pull WP image URLs into Laravel and optionally proxy them for resizing or optimization (e.g., use Glide or an image CDN).
If you proxy images, cache the optimized results and set proper cache headers.
Dev tips and best practices
- Use strong caching (Redis) and cache-by-query for GraphQL results.
- Use incremental revalidation: clear cache for affected entries when a post is updated via WordPress webhook.
- Add a small health-check endpoint in Laravel to ensure WPGraphQL is reachable.
- Limit fields in GraphQL queries to only what’s needed to reduce payload size.
- Monitor errors and latency; GraphQL can hide expensive joins, so watch the response time.
Example: clearing cache via WP action webhook
On WordPress side, add a webhook trigger when a post is published or updated (use a plugin or custom code). In Laravel, create a controller that accepts the webhook and invalidates the relevant cache key(s).
// routes/web.php
Route::post('/webhook/wp-publish', [WebhookController::class, 'wpPublish']);
// app/Http/Controllers/WebhookController.php
public function wpPublish(Request $request)
{
$postId = $request->input('post_id');
// Invalidate list cache and single post cache
Cache::forget('wp:recent-posts');
Cache::forget("wp:post:{$postId}");
return response('ok', 200);
}
Secure the webhook with a secret header or token.
Deployment notes
- Keep WordPress and Laravel on separate servers or containers for better scaling.
- Use an image CDN in front of WordPress for media.
- Use HTTPS and proper CORS rules when Laravel and WordPress live on different domains.
- Automate the cache purge webhook to run as part of your content pipeline.
Final checklist before going live
- WPGraphQL is exposing only needed fields.
- Laravel caches GraphQL responses and respects cache invalidation.
- Preview flow is tested for drafts and editors.
- Images are optimized (CDN or proxy).
- Secrets for webhooks are set and validated.
- Monitoring and logs are in place for GraphQL latency and errors.
Summary
A headless WordPress + WPGraphQL + Laravel stack gives you powerful editing tools with flexible rendering options. With the right caching, preview handling and security, you can build fast, maintainable and scalable sites that authors love and developers enjoy working on.