Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6a9509c
local env
josephfusco Jun 3, 2025
39b8e5e
Consolidate clean up scripts
josephfusco Jun 9, 2025
d5cdde9
Hide default CPT since we use our own UI
josephfusco Jun 9, 2025
e3e5e55
Webhooks admin UI
josephfusco Jun 9, 2025
9cc8176
Add missing file
josephfusco Jun 9, 2025
660de70
Fix headers
josephfusco Jun 9, 2025
55cd087
Update WebhookEventManager.php
josephfusco Jun 9, 2025
a4b20ca
Merge branch 'main' into feat-webhooks-admin-ui
josephfusco Jun 9, 2025
eaf9b53
Merge branch 'feat-webhooks-isr-example' into feat-webhooks-admin-ui
josephfusco Jun 11, 2025
85322c8
Resolve merge conflicts
josephfusco Jun 11, 2025
3e1c4ee
Smart cache integration
josephfusco Jun 11, 2025
35afecf
Move smart cache into it's own class
josephfusco Jun 11, 2025
9f0b2ba
Update SmartCacheEventHandler.php
josephfusco Jun 11, 2025
3ed40e5
Remove type
josephfusco Jun 11, 2025
ded1de5
Add wpgraphql-smart-cache to plugin env
josephfusco Jun 11, 2025
64b9324
Use native WordPress UI patterns
josephfusco Jun 11, 2025
a615ec4
Remove test file
josephfusco Jun 11, 2025
8623cf3
Refactor test webhook
josephfusco Jun 11, 2025
5bef67c
Improve response notification
josephfusco Jun 12, 2025
b5044e0
Fix ability to delete
josephfusco Jun 12, 2025
166eae6
Add native WordPress filters
josephfusco Jun 12, 2025
a593781
Leverage native WP styles
josephfusco Jun 12, 2025
ce91587
Only use smart cache events by default
josephfusco Jun 12, 2025
9e8b16b
top align content within webhooks table
josephfusco Jun 14, 2025
8c4b4c4
Setup example
josephfusco Jun 16, 2025
4b41af3
Dismissable admin notice
josephfusco Jun 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/next/webhooks-isr/.wp-env.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"phpVersion": "7.4",
"phpVersion": "8.0",
"plugins": [
"https://github.com/wp-graphql/wp-graphql/releases/latest/download/wp-graphql.zip",
"https://downloads.wordpress.org/plugin/code-snippets.3.6.8.zip",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,48 +1,74 @@
import crypto from 'crypto';

export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' });
}

try {
console.log('[Webhook] Received revalidation request');
// Log the full webhook payload
console.log('\n========== WEBHOOK RECEIVED ==========');
console.log('Timestamp:', new Date().toISOString());
console.log('Headers:', JSON.stringify(req.headers, null, 2));
console.log('Payload:', JSON.stringify(req.body, null, 2));
console.log('=====================================\n');

// Verify secret
const secret = req.headers['x-webhook-secret'];
const expectedSecret = process.env.WEBHOOK_REVALIDATE_SECRET;

console.log('[Webhook] Secret from header:', secret ? 'Provided' : 'Missing');
console.log('[Webhook] Expected secret is set:', expectedSecret ? 'Yes' : 'No');

// Securely compare secrets
if (
!secret ||
!expectedSecret ||
secret.length !== expectedSecret.length ||
!crypto.timingSafeEqual(Buffer.from(secret), Buffer.from(expectedSecret))
) {
console.warn('[Webhook] Invalid secret token');
return res.status(401).json({ message: 'Invalid token' });

console.log('[Webhook] Secret header present:', !!secret);
console.log('[Webhook] Expected secret present:', !!expectedSecret);

if (!secret || !expectedSecret) {
console.log('[Webhook] Missing secret configuration');
return res.status(401).json({ message: 'Unauthorized' });
}
console.log('[Webhook] Secret token validated successfully');

if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method Not Allowed' });
// Use timing-safe comparison
const secretBuffer = Buffer.from(secret);
const expectedBuffer = Buffer.from(expectedSecret);

if (secretBuffer.length !== expectedBuffer.length ||
!crypto.timingSafeEqual(secretBuffer, expectedBuffer)) {
console.log('[Webhook] Invalid secret');
return res.status(401).json({ message: 'Unauthorized' });
}

const body = req.body;
console.log('[Webhook] Request body parsed:', body);
console.log('[Webhook] Secret validated successfully');

const path = body.path;
// Extract path from various possible locations in the payload
let path = req.body?.path ||
req.body?.post?.path ||
req.body?.post?.uri ||
req.body?.uri ||
req.query?.path;

if (!path || typeof path !== 'string') {
console.warn('[Webhook] Invalid or missing path in request body');
if (!path) {
console.log('[Webhook] No path found in payload');
return res.status(400).json({ message: 'Path is required' });
}
console.log('[Webhook] Path to revalidate:', path);

console.log('\n========== ISR REVALIDATION ==========');
console.log('Path to revalidate:', path);
console.log('Starting at:', new Date().toISOString());

// Perform revalidation
await res.revalidate(path);
console.log('[Webhook] Successfully revalidated path:', path);

console.log('✅ SUCCESS: Revalidated path:', path);
console.log('Completed at:', new Date().toISOString());
console.log('=====================================\n');

return res.status(200).json({ message: `Revalidated path: ${path}` });
return res.status(200).json({
message: `Revalidated path: ${path}`,
revalidatedAt: new Date().toISOString(),
info: 'Only this specific page was regenerated, not the entire site'
});
} catch (error) {
console.error('[Webhook] Revalidation error:', error);
console.error('\n========== REVALIDATION ERROR ==========');
console.error('Error:', error);
console.error('=======================================\n');
return res.status(500).json({ message: 'Error during revalidation' });
}
}
211 changes: 116 additions & 95 deletions examples/next/webhooks-isr/example-app/src/pages/index.js
Original file line number Diff line number Diff line change
@@ -1,102 +1,123 @@
import Image from "next/image";
import Link from "next/link";
import { getApolloClient } from "@/lib/client";
import { gql } from "@apollo/client";

export default function Home() {
export default function Home({ posts }) {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
src/pages/index.js
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
<div className="min-h-screen p-8 pb-20 sm:p-20">
<main className="max-w-4xl mx-auto">
<h1 className="text-4xl font-bold mb-8">WordPress Webhooks ISR Demo</h1>
<p className="text-gray-600 mb-8">
This example demonstrates Next.js ISR (Incremental Static Regeneration) with WordPress webhooks.
When you update a post in WordPress, the webhook triggers revalidation of the specific page.
</p>

<h2 className="text-2xl font-semibold mb-6">Recent Posts</h2>

{posts && posts.length > 0 ? (
<div className="space-y-6">
{posts.map((edge) => {
const post = edge.node;
return (
<article key={post.id} className="border rounded-lg p-6 hover:shadow-lg transition-shadow">
<Link href={`/${post.uri}`}>
<h3 className="text-xl font-semibold mb-2 text-blue-600 hover:text-blue-800">
{post.title}
</h3>
</Link>
<p className="text-gray-600 text-sm mb-2">
By {post.author?.node?.name || 'Unknown'} on {new Date(post.date).toLocaleDateString()}
</p>
{post.excerpt && (
<div
className="text-gray-700 line-clamp-3"
dangerouslySetInnerHTML={{ __html: post.excerpt }}
/>
)}
<Link
href={`/${post.uri}`}
className="inline-block mt-4 text-blue-600 hover:text-blue-800 font-medium"
>
Read more →
</Link>
</article>
);
})}
</div>
) : (
<p className="text-gray-600">No posts found. Create some posts in WordPress admin.</p>
)}

<div className="mt-12 p-6 bg-gray-100 rounded-lg">
<h3 className="font-semibold mb-2">Quick Links:</h3>
<ul className="space-y-2 text-sm">
<li>
<a
href="http://localhost:8888/wp-admin/"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800"
>
WordPress Admin →
</a> (username: admin, password: password)
</li>
<li>
<a
href="http://localhost:8888/wp-admin/options-general.php?page=graphql-webhooks"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800"
>
Webhooks Settings →
</a>
</li>
</ul>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org →
</a>
</footer>
</div>
);
}

const GET_POSTS = gql`
query GetPosts {
posts(first: 10) {
edges {
node {
id
title
uri
date
excerpt
author {
node {
name
}
}
}
}
}
}
`;

export async function getStaticProps() {
try {
const { data } = await getApolloClient().query({
query: GET_POSTS,
});

return {
props: {
posts: data?.posts?.edges || [],
},
revalidate: 60, // ISR: revalidate every 60 seconds
};
} catch (error) {
console.error("Error fetching posts:", error);
return {
props: {
posts: [],
},
revalidate: 60,
};
}
}
25 changes: 25 additions & 0 deletions plugins/wp-graphql-headless-webhooks/.wp-env.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"core": null,
"phpVersion": "8.0",
"plugins": [
"https://downloads.wordpress.org/plugin/wp-graphql.latest-stable.zip",
"https://downloads.wordpress.org/plugin/wpgraphql-smart-cache.latest-stable.zip",
"."
],
"port": 8889,
"testsPort": 8890,
"config": {
"WP_DEBUG": true,
"WP_DEBUG_LOG": true,
"WP_DEBUG_DISPLAY": false,
"WP_ENVIRONMENT_TYPE": "development"
},
"mappings": {
"wp-content/plugins/wp-graphql-headless-webhooks": "."
},
"env": {
"tests": {
"port": 8890
}
}
}
2 changes: 1 addition & 1 deletion plugins/wp-graphql-headless-webhooks/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
}
],
"require": {
"php": "^7.4 || ^8.0",
"php": "^7.4 || ^8.0 || ^8.4",
"axepress/wp-graphql-plugin-boilerplate": "^0.1.0"
},
"require-dev": {
Expand Down
Loading
Loading