Skip to content

Commit 8c4b4c4

Browse files
committed
Setup example
1 parent 9e8b16b commit 8c4b4c4

File tree

6 files changed

+265
-126
lines changed

6 files changed

+265
-126
lines changed

examples/next/webhooks-isr/.wp-env.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"phpVersion": "7.4",
2+
"phpVersion": "8.0",
33
"plugins": [
44
"https://github.com/wp-graphql/wp-graphql/releases/latest/download/wp-graphql.zip",
55
"https://downloads.wordpress.org/plugin/code-snippets.3.6.8.zip",
Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,74 @@
11
import crypto from 'crypto';
22

33
export default async function handler(req, res) {
4+
if (req.method !== 'POST') {
5+
return res.status(405).json({ message: 'Method not allowed' });
6+
}
7+
48
try {
5-
console.log('[Webhook] Received revalidation request');
9+
// Log the full webhook payload
10+
console.log('\n========== WEBHOOK RECEIVED ==========');
11+
console.log('Timestamp:', new Date().toISOString());
12+
console.log('Headers:', JSON.stringify(req.headers, null, 2));
13+
console.log('Payload:', JSON.stringify(req.body, null, 2));
14+
console.log('=====================================\n');
615

16+
// Verify secret
717
const secret = req.headers['x-webhook-secret'];
818
const expectedSecret = process.env.WEBHOOK_REVALIDATE_SECRET;
9-
10-
console.log('[Webhook] Secret from header:', secret ? 'Provided' : 'Missing');
11-
console.log('[Webhook] Expected secret is set:', expectedSecret ? 'Yes' : 'No');
12-
13-
// Securely compare secrets
14-
if (
15-
!secret ||
16-
!expectedSecret ||
17-
secret.length !== expectedSecret.length ||
18-
!crypto.timingSafeEqual(Buffer.from(secret), Buffer.from(expectedSecret))
19-
) {
20-
console.warn('[Webhook] Invalid secret token');
21-
return res.status(401).json({ message: 'Invalid token' });
19+
20+
console.log('[Webhook] Secret header present:', !!secret);
21+
console.log('[Webhook] Expected secret present:', !!expectedSecret);
22+
23+
if (!secret || !expectedSecret) {
24+
console.log('[Webhook] Missing secret configuration');
25+
return res.status(401).json({ message: 'Unauthorized' });
2226
}
23-
console.log('[Webhook] Secret token validated successfully');
2427

25-
if (req.method !== 'POST') {
26-
return res.status(405).json({ message: 'Method Not Allowed' });
28+
// Use timing-safe comparison
29+
const secretBuffer = Buffer.from(secret);
30+
const expectedBuffer = Buffer.from(expectedSecret);
31+
32+
if (secretBuffer.length !== expectedBuffer.length ||
33+
!crypto.timingSafeEqual(secretBuffer, expectedBuffer)) {
34+
console.log('[Webhook] Invalid secret');
35+
return res.status(401).json({ message: 'Unauthorized' });
2736
}
2837

29-
const body = req.body;
30-
console.log('[Webhook] Request body parsed:', body);
38+
console.log('[Webhook] Secret validated successfully');
3139

32-
const path = body.path;
40+
// Extract path from various possible locations in the payload
41+
let path = req.body?.path ||
42+
req.body?.post?.path ||
43+
req.body?.post?.uri ||
44+
req.body?.uri ||
45+
req.query?.path;
3346

34-
if (!path || typeof path !== 'string') {
35-
console.warn('[Webhook] Invalid or missing path in request body');
47+
if (!path) {
48+
console.log('[Webhook] No path found in payload');
3649
return res.status(400).json({ message: 'Path is required' });
3750
}
38-
console.log('[Webhook] Path to revalidate:', path);
51+
52+
console.log('\n========== ISR REVALIDATION ==========');
53+
console.log('Path to revalidate:', path);
54+
console.log('Starting at:', new Date().toISOString());
3955

56+
// Perform revalidation
4057
await res.revalidate(path);
41-
console.log('[Webhook] Successfully revalidated path:', path);
58+
59+
console.log('✅ SUCCESS: Revalidated path:', path);
60+
console.log('Completed at:', new Date().toISOString());
61+
console.log('=====================================\n');
4262

43-
return res.status(200).json({ message: `Revalidated path: ${path}` });
63+
return res.status(200).json({
64+
message: `Revalidated path: ${path}`,
65+
revalidatedAt: new Date().toISOString(),
66+
info: 'Only this specific page was regenerated, not the entire site'
67+
});
4468
} catch (error) {
45-
console.error('[Webhook] Revalidation error:', error);
69+
console.error('\n========== REVALIDATION ERROR ==========');
70+
console.error('Error:', error);
71+
console.error('=======================================\n');
4672
return res.status(500).json({ message: 'Error during revalidation' });
4773
}
4874
}
Lines changed: 116 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,102 +1,123 @@
1-
import Image from "next/image";
1+
import Link from "next/link";
2+
import { getApolloClient } from "@/lib/client";
3+
import { gql } from "@apollo/client";
24

3-
export default function Home() {
5+
export default function Home({ posts }) {
46
return (
5-
<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)]">
6-
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
7-
<Image
8-
className="dark:invert"
9-
src="/next.svg"
10-
alt="Next.js logo"
11-
width={180}
12-
height={38}
13-
priority
14-
/>
15-
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
16-
<li className="mb-2 tracking-[-.01em]">
17-
Get started by editing{" "}
18-
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
19-
src/pages/index.js
20-
</code>
21-
.
22-
</li>
23-
<li className="tracking-[-.01em]">
24-
Save and see your changes instantly.
25-
</li>
26-
</ol>
27-
<div className="flex gap-4 items-center flex-col sm:flex-row">
28-
<a
29-
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"
30-
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
31-
target="_blank"
32-
rel="noopener noreferrer"
33-
>
34-
<Image
35-
className="dark:invert"
36-
src="/vercel.svg"
37-
alt="Vercel logomark"
38-
width={20}
39-
height={20}
40-
/>
41-
Deploy now
42-
</a>
43-
<a
44-
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]"
45-
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
46-
target="_blank"
47-
rel="noopener noreferrer"
48-
>
49-
Read our docs
50-
</a>
7+
<div className="min-h-screen p-8 pb-20 sm:p-20">
8+
<main className="max-w-4xl mx-auto">
9+
<h1 className="text-4xl font-bold mb-8">WordPress Webhooks ISR Demo</h1>
10+
<p className="text-gray-600 mb-8">
11+
This example demonstrates Next.js ISR (Incremental Static Regeneration) with WordPress webhooks.
12+
When you update a post in WordPress, the webhook triggers revalidation of the specific page.
13+
</p>
14+
15+
<h2 className="text-2xl font-semibold mb-6">Recent Posts</h2>
16+
17+
{posts && posts.length > 0 ? (
18+
<div className="space-y-6">
19+
{posts.map((edge) => {
20+
const post = edge.node;
21+
return (
22+
<article key={post.id} className="border rounded-lg p-6 hover:shadow-lg transition-shadow">
23+
<Link href={`/${post.uri}`}>
24+
<h3 className="text-xl font-semibold mb-2 text-blue-600 hover:text-blue-800">
25+
{post.title}
26+
</h3>
27+
</Link>
28+
<p className="text-gray-600 text-sm mb-2">
29+
By {post.author?.node?.name || 'Unknown'} on {new Date(post.date).toLocaleDateString()}
30+
</p>
31+
{post.excerpt && (
32+
<div
33+
className="text-gray-700 line-clamp-3"
34+
dangerouslySetInnerHTML={{ __html: post.excerpt }}
35+
/>
36+
)}
37+
<Link
38+
href={`/${post.uri}`}
39+
className="inline-block mt-4 text-blue-600 hover:text-blue-800 font-medium"
40+
>
41+
Read more →
42+
</Link>
43+
</article>
44+
);
45+
})}
46+
</div>
47+
) : (
48+
<p className="text-gray-600">No posts found. Create some posts in WordPress admin.</p>
49+
)}
50+
51+
<div className="mt-12 p-6 bg-gray-100 rounded-lg">
52+
<h3 className="font-semibold mb-2">Quick Links:</h3>
53+
<ul className="space-y-2 text-sm">
54+
<li>
55+
<a
56+
href="http://localhost:8888/wp-admin/"
57+
target="_blank"
58+
rel="noopener noreferrer"
59+
className="text-blue-600 hover:text-blue-800"
60+
>
61+
WordPress Admin →
62+
</a> (username: admin, password: password)
63+
</li>
64+
<li>
65+
<a
66+
href="http://localhost:8888/wp-admin/options-general.php?page=graphql-webhooks"
67+
target="_blank"
68+
rel="noopener noreferrer"
69+
className="text-blue-600 hover:text-blue-800"
70+
>
71+
Webhooks Settings →
72+
</a>
73+
</li>
74+
</ul>
5175
</div>
5276
</main>
53-
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
54-
<a
55-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
56-
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
57-
target="_blank"
58-
rel="noopener noreferrer"
59-
>
60-
<Image
61-
aria-hidden
62-
src="/file.svg"
63-
alt="File icon"
64-
width={16}
65-
height={16}
66-
/>
67-
Learn
68-
</a>
69-
<a
70-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
71-
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
72-
target="_blank"
73-
rel="noopener noreferrer"
74-
>
75-
<Image
76-
aria-hidden
77-
src="/window.svg"
78-
alt="Window icon"
79-
width={16}
80-
height={16}
81-
/>
82-
Examples
83-
</a>
84-
<a
85-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
86-
href="https://nextjs.org?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
87-
target="_blank"
88-
rel="noopener noreferrer"
89-
>
90-
<Image
91-
aria-hidden
92-
src="/globe.svg"
93-
alt="Globe icon"
94-
width={16}
95-
height={16}
96-
/>
97-
Go to nextjs.org →
98-
</a>
99-
</footer>
10077
</div>
10178
);
10279
}
80+
81+
const GET_POSTS = gql`
82+
query GetPosts {
83+
posts(first: 10) {
84+
edges {
85+
node {
86+
id
87+
title
88+
uri
89+
date
90+
excerpt
91+
author {
92+
node {
93+
name
94+
}
95+
}
96+
}
97+
}
98+
}
99+
}
100+
`;
101+
102+
export async function getStaticProps() {
103+
try {
104+
const { data } = await getApolloClient().query({
105+
query: GET_POSTS,
106+
});
107+
108+
return {
109+
props: {
110+
posts: data?.posts?.edges || [],
111+
},
112+
revalidate: 60, // ISR: revalidate every 60 seconds
113+
};
114+
} catch (error) {
115+
console.error("Error fetching posts:", error);
116+
return {
117+
props: {
118+
posts: [],
119+
},
120+
revalidate: 60,
121+
};
122+
}
123+
}

plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventHandler.php

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,18 +124,57 @@ public function process_buffer() {
124124
continue;
125125
}
126126

127-
// Simple payload with just the essential information
127+
// Decode cache keys to get actual post IDs
128+
$decoded_items = [];
129+
$paths = [];
130+
131+
foreach ( $data['keys'] as $key ) {
132+
// WPGraphQL cache keys are base64 encoded global IDs
133+
// Format is typically: base64("post:123") or base64("page:456")
134+
$decoded = base64_decode( $key );
135+
if ( $decoded && strpos( $decoded, ':' ) !== false ) {
136+
list( $type, $id ) = explode( ':', $decoded, 2 );
137+
138+
// Get the post data
139+
if ( $type === 'post' || $type === 'page' ) {
140+
$post = get_post( $id );
141+
if ( $post ) {
142+
$uri = str_replace( home_url(), '', get_permalink( $post ) );
143+
$path = '/' . trim( $uri, '/' ) . '/';
144+
145+
$decoded_items[] = [
146+
'id' => $post->ID,
147+
'title' => $post->post_title,
148+
'slug' => $post->post_name,
149+
'uri' => $uri,
150+
'path' => $path,
151+
'type' => $post->post_type,
152+
'status' => $post->post_status,
153+
];
154+
155+
$paths[] = $path;
156+
}
157+
}
158+
}
159+
}
160+
161+
// Enhanced payload with decoded post data
128162
$payload = [
129163
'post_type' => $data['post_type'],
130164
'action' => $data['action'],
131165
'graphql_endpoint' => $data['graphql_endpoint'],
132-
'cache_keys' => array_unique( $data['keys'] ), // Remove duplicates
166+
'cache_keys' => array_unique( $data['keys'] ), // Original cache keys
133167
'cache_keys_count' => count( array_unique( $data['keys'] ) ),
168+
'posts' => $decoded_items, // Decoded post data
169+
'paths' => array_unique( $paths ), // Paths for ISR revalidation
134170
'timestamp' => current_time( 'c' ),
135171
];
136172

137-
// Let webhook consumers decode the keys if they need to
138-
// They already have access to WPGraphQL and can use Relay::fromGlobalId()
173+
// If there's only one post, add it as the primary post/path for compatibility
174+
if ( count( $decoded_items ) === 1 ) {
175+
$payload['post'] = $decoded_items[0];
176+
$payload['path'] = $decoded_items[0]['path'];
177+
}
139178

140179
call_user_func( $this->webhook_trigger_callback, $webhook_event, $payload );
141180
}

0 commit comments

Comments
 (0)