claude-plugin-wordpress-manager 1.5.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/CHANGELOG.md +97 -0
  3. package/README.md +27 -13
  4. package/agents/wp-accessibility-auditor.md +206 -0
  5. package/agents/wp-content-strategist.md +18 -0
  6. package/agents/wp-deployment-engineer.md +34 -2
  7. package/agents/wp-performance-optimizer.md +12 -0
  8. package/agents/wp-security-auditor.md +20 -0
  9. package/agents/wp-security-hardener.md +266 -0
  10. package/agents/wp-site-manager.md +14 -0
  11. package/agents/wp-test-engineer.md +207 -0
  12. package/docs/guides/INDEX.md +46 -0
  13. package/docs/guides/wp-blog.md +590 -0
  14. package/docs/guides/wp-design-system.md +976 -0
  15. package/docs/guides/wp-ecommerce.md +786 -0
  16. package/docs/guides/wp-landing-page.md +762 -0
  17. package/docs/guides/wp-portfolio.md +713 -0
  18. package/docs/plans/2026-02-27-design-system-guide-design.md +30 -0
  19. package/docs/plans/2026-02-27-site-type-guides-design.md +44 -0
  20. package/package.json +2 -2
  21. package/skills/wordpress-router/references/decision-tree.md +12 -2
  22. package/skills/wp-accessibility/SKILL.md +170 -0
  23. package/skills/wp-accessibility/references/a11y-audit-tools.md +248 -0
  24. package/skills/wp-accessibility/references/a11y-testing.md +222 -0
  25. package/skills/wp-accessibility/references/block-a11y.md +247 -0
  26. package/skills/wp-accessibility/references/interactive-a11y.md +272 -0
  27. package/skills/wp-accessibility/references/media-a11y.md +254 -0
  28. package/skills/wp-accessibility/references/theme-a11y.md +309 -0
  29. package/skills/wp-audit/SKILL.md +4 -0
  30. package/skills/wp-block-development/SKILL.md +5 -0
  31. package/skills/wp-block-themes/SKILL.md +4 -0
  32. package/skills/wp-e2e-testing/SKILL.md +186 -0
  33. package/skills/wp-e2e-testing/references/ci-integration.md +174 -0
  34. package/skills/wp-e2e-testing/references/jest-wordpress.md +114 -0
  35. package/skills/wp-e2e-testing/references/phpunit-wordpress.md +141 -0
  36. package/skills/wp-e2e-testing/references/playwright-wordpress.md +108 -0
  37. package/skills/wp-e2e-testing/references/test-data-generation.md +127 -0
  38. package/skills/wp-e2e-testing/references/visual-regression.md +107 -0
  39. package/skills/wp-e2e-testing/references/wp-env-setup.md +97 -0
  40. package/skills/wp-e2e-testing/scripts/test_inspect.mjs +375 -0
  41. package/skills/wp-headless/SKILL.md +168 -0
  42. package/skills/wp-headless/references/api-layer-choice.md +160 -0
  43. package/skills/wp-headless/references/cors-config.md +245 -0
  44. package/skills/wp-headless/references/frontend-integration.md +331 -0
  45. package/skills/wp-headless/references/headless-auth.md +286 -0
  46. package/skills/wp-headless/references/webhooks.md +277 -0
  47. package/skills/wp-headless/references/wpgraphql.md +331 -0
  48. package/skills/wp-headless/scripts/headless_inspect.mjs +321 -0
  49. package/skills/wp-i18n/SKILL.md +170 -0
  50. package/skills/wp-i18n/references/js-i18n.md +201 -0
  51. package/skills/wp-i18n/references/multilingual-setup.md +219 -0
  52. package/skills/wp-i18n/references/php-i18n.md +196 -0
  53. package/skills/wp-i18n/references/rtl-support.md +206 -0
  54. package/skills/wp-i18n/references/translation-workflow.md +178 -0
  55. package/skills/wp-i18n/references/wpcli-i18n.md +177 -0
  56. package/skills/wp-i18n/scripts/i18n_inspect.mjs +330 -0
  57. package/skills/wp-interactivity-api/SKILL.md +4 -0
  58. package/skills/wp-plugin-development/SKILL.md +6 -0
  59. package/skills/wp-rest-api/SKILL.md +4 -0
  60. package/skills/wp-security/SKILL.md +179 -0
  61. package/skills/wp-security/references/api-restriction.md +147 -0
  62. package/skills/wp-security/references/authentication-hardening.md +105 -0
  63. package/skills/wp-security/references/filesystem-hardening.md +105 -0
  64. package/skills/wp-security/references/http-headers.md +105 -0
  65. package/skills/wp-security/references/incident-response.md +144 -0
  66. package/skills/wp-security/references/user-capabilities.md +115 -0
  67. package/skills/wp-security/references/wp-config-security.md +129 -0
  68. package/skills/wp-security/scripts/security_inspect.mjs +393 -0
@@ -0,0 +1,286 @@
1
+ # Headless Authentication
2
+
3
+ Use this file when implementing authentication for decoupled WordPress frontends.
4
+
5
+ ## Authentication methods comparison
6
+
7
+ | Method | Use case | Security | Complexity |
8
+ |--------|----------|----------|-----------|
9
+ | Application Passwords | Server-to-server, CI/CD | Good | Low |
10
+ | JWT (plugin) | SPA authentication | Good | Medium |
11
+ | Cookie-based (same domain) | Subdomain frontends | Good | Low |
12
+ | OAuth 2.0 | Third-party apps | Best | High |
13
+ | Nonce + Cookie | Same-origin AJAX | Good | Low (but not for headless) |
14
+
15
+ ## Application Passwords (WordPress 5.6+)
16
+
17
+ Best for: server-side requests, build-time data fetching, CI/CD pipelines.
18
+
19
+ ### Setup
20
+
21
+ Users → Profile → Application Passwords → Add New
22
+
23
+ ### Usage
24
+
25
+ ```bash
26
+ # Basic Auth with application password
27
+ curl -u "username:xxxx xxxx xxxx xxxx xxxx xxxx" \
28
+ https://site.com/wp-json/wp/v2/posts
29
+
30
+ # Base64 encoded
31
+ curl -H "Authorization: Basic $(echo -n 'username:xxxx xxxx xxxx xxxx' | base64)" \
32
+ https://site.com/wp-json/wp/v2/posts
33
+ ```
34
+
35
+ ### In Next.js (server-side)
36
+
37
+ ```js
38
+ // lib/wordpress.js
39
+ const WP_URL = process.env.WORDPRESS_URL;
40
+ const WP_AUTH = Buffer.from(
41
+ `${process.env.WP_USER}:${process.env.WP_APP_PASSWORD}`
42
+ ).toString('base64');
43
+
44
+ export async function fetchWP(endpoint, options = {}) {
45
+ const res = await fetch(`${WP_URL}/wp-json/wp/v2/${endpoint}`, {
46
+ ...options,
47
+ headers: {
48
+ 'Authorization': `Basic ${WP_AUTH}`,
49
+ 'Content-Type': 'application/json',
50
+ ...options.headers,
51
+ },
52
+ });
53
+ if (!res.ok) throw new Error(`WP API error: ${res.status}`);
54
+ return res.json();
55
+ }
56
+ ```
57
+
58
+ ### Restrict application passwords
59
+
60
+ ```php
61
+ // Only allow application passwords for specific roles
62
+ add_filter('wp_is_application_passwords_available_for_user', function($available, $user) {
63
+ return in_array('administrator', $user->roles);
64
+ }, 10, 2);
65
+ ```
66
+
67
+ ## JWT Authentication
68
+
69
+ Best for: SPA (React/Vue) with user login, client-side authenticated requests.
70
+
71
+ ### Plugin setup
72
+
73
+ ```bash
74
+ wp plugin install jwt-authentication-for-wp-rest-api --activate
75
+ ```
76
+
77
+ Add to `wp-config.php`:
78
+ ```php
79
+ define('JWT_AUTH_SECRET_KEY', 'your-secret-key-at-least-32-chars-long');
80
+ define('JWT_AUTH_CORS_ENABLE', true);
81
+ ```
82
+
83
+ Add to `.htaccess`:
84
+ ```apache
85
+ RewriteEngine On
86
+ RewriteCond %{HTTP:Authorization} ^(.*)
87
+ RewriteRule .* - [E=HTTP_AUTHORIZATION:%1]
88
+ ```
89
+
90
+ ### Token flow
91
+
92
+ ```js
93
+ // 1. Get token
94
+ const loginRes = await fetch('https://site.com/wp-json/jwt-auth/v1/token', {
95
+ method: 'POST',
96
+ headers: { 'Content-Type': 'application/json' },
97
+ body: JSON.stringify({
98
+ username: 'user@example.com',
99
+ password: 'password',
100
+ }),
101
+ });
102
+ const { token, user_display_name } = await loginRes.json();
103
+
104
+ // 2. Use token for authenticated requests
105
+ const postsRes = await fetch('https://site.com/wp-json/wp/v2/posts', {
106
+ headers: {
107
+ 'Authorization': `Bearer ${token}`,
108
+ },
109
+ });
110
+
111
+ // 3. Validate token (optional)
112
+ const validateRes = await fetch('https://site.com/wp-json/jwt-auth/v1/token/validate', {
113
+ method: 'POST',
114
+ headers: {
115
+ 'Authorization': `Bearer ${token}`,
116
+ },
117
+ });
118
+ ```
119
+
120
+ ### Token storage (frontend)
121
+
122
+ ```js
123
+ // Store in httpOnly cookie (recommended for SSR frameworks)
124
+ // Set via API route, not client-side JavaScript
125
+
126
+ // For SPAs: store in memory (most secure for client-side)
127
+ let authToken = null;
128
+
129
+ function setToken(token) {
130
+ authToken = token; // Lost on page refresh — that's OK for security
131
+ }
132
+
133
+ function getToken() {
134
+ return authToken;
135
+ }
136
+
137
+ // AVOID: localStorage (XSS vulnerable)
138
+ // localStorage.setItem('token', token); // DON'T DO THIS
139
+ ```
140
+
141
+ ### Refresh token pattern
142
+
143
+ ```js
144
+ class AuthManager {
145
+ constructor() {
146
+ this.token = null;
147
+ this.refreshToken = null;
148
+ }
149
+
150
+ async login(username, password) {
151
+ const res = await fetch('/wp-json/jwt-auth/v1/token', {
152
+ method: 'POST',
153
+ headers: { 'Content-Type': 'application/json' },
154
+ body: JSON.stringify({ username, password }),
155
+ });
156
+ const data = await res.json();
157
+ this.token = data.token;
158
+ return data;
159
+ }
160
+
161
+ async authenticatedFetch(url, options = {}) {
162
+ let res = await fetch(url, {
163
+ ...options,
164
+ headers: {
165
+ ...options.headers,
166
+ 'Authorization': `Bearer ${this.token}`,
167
+ },
168
+ });
169
+
170
+ // Token expired — re-authenticate
171
+ if (res.status === 403) {
172
+ // Redirect to login or use refresh token
173
+ throw new Error('Session expired. Please log in again.');
174
+ }
175
+
176
+ return res;
177
+ }
178
+ }
179
+ ```
180
+
181
+ ## WPGraphQL authentication
182
+
183
+ ```graphql
184
+ # Login mutation (with wp-graphql-jwt-authentication plugin)
185
+ mutation Login($username: String!, $password: String!) {
186
+ login(input: { username: $username, password: $password }) {
187
+ authToken
188
+ refreshToken
189
+ user {
190
+ id
191
+ name
192
+ email
193
+ }
194
+ }
195
+ }
196
+
197
+ # Refresh token
198
+ mutation RefreshToken($token: String!) {
199
+ refreshJwtAuthToken(input: { jwtRefreshToken: $token }) {
200
+ authToken
201
+ }
202
+ }
203
+ ```
204
+
205
+ ## Next.js authentication pattern
206
+
207
+ ### API route for login
208
+
209
+ ```js
210
+ // pages/api/login.js (Next.js)
211
+ export default async function handler(req, res) {
212
+ if (req.method !== 'POST') return res.status(405).end();
213
+
214
+ const { username, password } = req.body;
215
+
216
+ const wpRes = await fetch(`${process.env.WP_URL}/wp-json/jwt-auth/v1/token`, {
217
+ method: 'POST',
218
+ headers: { 'Content-Type': 'application/json' },
219
+ body: JSON.stringify({ username, password }),
220
+ });
221
+
222
+ if (!wpRes.ok) {
223
+ return res.status(401).json({ error: 'Invalid credentials' });
224
+ }
225
+
226
+ const { token } = await wpRes.json();
227
+
228
+ // Set httpOnly cookie (not accessible via JavaScript)
229
+ res.setHeader('Set-Cookie', [
230
+ `wp_token=${token}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=86400`,
231
+ ]);
232
+
233
+ return res.status(200).json({ success: true });
234
+ }
235
+ ```
236
+
237
+ ### Middleware for protected routes
238
+
239
+ ```js
240
+ // middleware.js (Next.js)
241
+ import { NextResponse } from 'next/server';
242
+
243
+ export function middleware(request) {
244
+ const token = request.cookies.get('wp_token')?.value;
245
+
246
+ if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
247
+ return NextResponse.redirect(new URL('/login', request.url));
248
+ }
249
+
250
+ return NextResponse.next();
251
+ }
252
+
253
+ export const config = {
254
+ matcher: ['/dashboard/:path*', '/profile/:path*'],
255
+ };
256
+ ```
257
+
258
+ ## Security checklist
259
+
260
+ - [ ] Never store JWT in localStorage (XSS risk)
261
+ - [ ] Use httpOnly cookies for token storage when possible
262
+ - [ ] Set short token expiry (15 min) with refresh tokens
263
+ - [ ] Use HTTPS for all API requests
264
+ - [ ] Validate tokens server-side on every request
265
+ - [ ] Implement CORS properly (see `cors-config.md`)
266
+ - [ ] Rate-limit login endpoints
267
+ - [ ] Use strong JWT secret key (32+ characters)
268
+ - [ ] Disable XML-RPC if not needed
269
+
270
+ ## Verification
271
+
272
+ ```bash
273
+ # Test application password
274
+ curl -u "admin:xxxx xxxx xxxx xxxx" https://site.com/wp-json/wp/v2/users/me
275
+
276
+ # Test JWT token
277
+ TOKEN=$(curl -s -X POST https://site.com/wp-json/jwt-auth/v1/token \
278
+ -H "Content-Type: application/json" \
279
+ -d '{"username":"admin","password":"password"}' | jq -r '.token')
280
+
281
+ curl -H "Authorization: Bearer $TOKEN" https://site.com/wp-json/wp/v2/users/me
282
+
283
+ # Verify unauthenticated access is blocked
284
+ curl -s -o /dev/null -w "%{http_code}" https://site.com/wp-json/wp/v2/users
285
+ # Should return 401 if REST API is restricted
286
+ ```
@@ -0,0 +1,277 @@
1
+ # Content Webhooks
2
+
3
+ Use this file when implementing webhooks to keep a headless frontend in sync with WordPress content changes.
4
+
5
+ ## Why webhooks
6
+
7
+ In a headless setup, the frontend caches content (SSG/ISR). When editors publish or update content in WordPress, the frontend must be notified to rebuild or revalidate the affected pages.
8
+
9
+ ```
10
+ Editor publishes post → WordPress fires webhook → Frontend revalidates page
11
+ ```
12
+
13
+ ## WordPress webhook implementation
14
+
15
+ ### Using action hooks
16
+
17
+ ```php
18
+ // mu-plugins/headless-webhooks.php
19
+
20
+ add_action('transition_post_status', 'headless_notify_frontend', 10, 3);
21
+ add_action('edited_term', 'headless_notify_frontend_term', 10, 3);
22
+ add_action('wp_update_nav_menu', 'headless_notify_frontend_menu');
23
+
24
+ function headless_notify_frontend($new_status, $old_status, $post) {
25
+ // Only fire for published/updated/unpublished content
26
+ $trigger_statuses = ['publish', 'trash'];
27
+ if (!in_array($new_status, $trigger_statuses) && !in_array($old_status, $trigger_statuses)) {
28
+ return;
29
+ }
30
+
31
+ // Skip revisions and autosaves
32
+ if (wp_is_post_revision($post) || wp_is_post_autosave($post)) {
33
+ return;
34
+ }
35
+
36
+ $path = get_permalink_path($post);
37
+ send_revalidation_webhook($path, [
38
+ 'action' => 'post_updated',
39
+ 'post_id' => $post->ID,
40
+ 'post_type' => $post->post_type,
41
+ 'slug' => $post->post_name,
42
+ 'status' => $new_status,
43
+ ]);
44
+ }
45
+
46
+ function headless_notify_frontend_term($term_id, $tt_id, $taxonomy) {
47
+ $term = get_term($term_id, $taxonomy);
48
+ send_revalidation_webhook("/category/{$term->slug}", [
49
+ 'action' => 'term_updated',
50
+ 'term_id' => $term_id,
51
+ 'taxonomy' => $taxonomy,
52
+ 'slug' => $term->slug,
53
+ ]);
54
+ }
55
+
56
+ function headless_notify_frontend_menu($menu_id) {
57
+ send_revalidation_webhook('/', [
58
+ 'action' => 'menu_updated',
59
+ 'menu_id' => $menu_id,
60
+ ]);
61
+ }
62
+
63
+ function get_permalink_path($post) {
64
+ $permalink = get_permalink($post);
65
+ $parsed = wp_parse_url($permalink);
66
+ return $parsed['path'] ?? '/';
67
+ }
68
+
69
+ function send_revalidation_webhook($path, $payload = []) {
70
+ $webhook_url = defined('HEADLESS_WEBHOOK_URL')
71
+ ? HEADLESS_WEBHOOK_URL
72
+ : '';
73
+
74
+ $webhook_secret = defined('HEADLESS_WEBHOOK_SECRET')
75
+ ? HEADLESS_WEBHOOK_SECRET
76
+ : '';
77
+
78
+ if (!$webhook_url) return;
79
+
80
+ $body = array_merge($payload, ['path' => $path]);
81
+
82
+ wp_remote_post($webhook_url, [
83
+ 'timeout' => 5,
84
+ 'headers' => [
85
+ 'Content-Type' => 'application/json',
86
+ 'X-Revalidate-Secret' => $webhook_secret,
87
+ ],
88
+ 'body' => wp_json_encode($body),
89
+ ]);
90
+ }
91
+ ```
92
+
93
+ ### wp-config.php constants
94
+
95
+ ```php
96
+ define('HEADLESS_WEBHOOK_URL', 'https://app.example.com/api/revalidate');
97
+ define('HEADLESS_WEBHOOK_SECRET', 'your-shared-secret-here');
98
+ ```
99
+
100
+ ## Frontend webhook receivers
101
+
102
+ ### Next.js (App Router)
103
+
104
+ ```js
105
+ // app/api/revalidate/route.js
106
+ import { revalidatePath, revalidateTag } from 'next/cache';
107
+
108
+ export async function POST(request) {
109
+ const secret = request.headers.get('x-revalidate-secret');
110
+ if (secret !== process.env.REVALIDATE_SECRET) {
111
+ return Response.json({ error: 'Unauthorized' }, { status: 401 });
112
+ }
113
+
114
+ const body = await request.json();
115
+ const { action, path, post_type, slug } = body;
116
+
117
+ // Revalidate the specific page
118
+ if (path) {
119
+ revalidatePath(path);
120
+ }
121
+
122
+ // Revalidate related pages
123
+ switch (action) {
124
+ case 'post_updated':
125
+ revalidatePath('/blog'); // blog listing
126
+ if (post_type === 'post') {
127
+ revalidatePath(`/blog/${slug}`);
128
+ }
129
+ break;
130
+ case 'term_updated':
131
+ revalidatePath('/blog'); // categories affect listings
132
+ break;
133
+ case 'menu_updated':
134
+ revalidatePath('/', 'layout'); // menus are in layout
135
+ break;
136
+ }
137
+
138
+ return Response.json({
139
+ revalidated: true,
140
+ path,
141
+ action,
142
+ timestamp: Date.now(),
143
+ });
144
+ }
145
+ ```
146
+
147
+ ### Nuxt 3
148
+
149
+ ```js
150
+ // server/api/revalidate.post.js
151
+ export default defineEventHandler(async (event) => {
152
+ const secret = getHeader(event, 'x-revalidate-secret');
153
+ if (secret !== process.env.REVALIDATE_SECRET) {
154
+ throw createError({ statusCode: 401, message: 'Unauthorized' });
155
+ }
156
+
157
+ const body = await readBody(event);
158
+
159
+ // Clear Nuxt cache for the path
160
+ // For Nitro with built-in caching:
161
+ const storage = useStorage('cache');
162
+ await storage.clear(); // or selectively clear by path
163
+
164
+ return { revalidated: true, path: body.path };
165
+ });
166
+ ```
167
+
168
+ ### Astro (with SSR adapter)
169
+
170
+ ```js
171
+ // src/pages/api/revalidate.js
172
+ export async function POST({ request }) {
173
+ const secret = request.headers.get('x-revalidate-secret');
174
+ if (secret !== import.meta.env.REVALIDATE_SECRET) {
175
+ return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 });
176
+ }
177
+
178
+ // Astro SSG: trigger rebuild via deployment hook
179
+ const body = await request.json();
180
+
181
+ // Example: trigger Vercel deploy hook
182
+ if (import.meta.env.VERCEL_DEPLOY_HOOK) {
183
+ await fetch(import.meta.env.VERCEL_DEPLOY_HOOK, { method: 'POST' });
184
+ }
185
+
186
+ return new Response(JSON.stringify({ revalidated: true }));
187
+ }
188
+ ```
189
+
190
+ ## Deployment platform hooks
191
+
192
+ ### Vercel
193
+
194
+ ```php
195
+ // WordPress: trigger Vercel deploy on major changes
196
+ function trigger_vercel_deploy() {
197
+ $hook_url = defined('VERCEL_DEPLOY_HOOK') ? VERCEL_DEPLOY_HOOK : '';
198
+ if (!$hook_url) return;
199
+
200
+ wp_remote_post($hook_url, ['timeout' => 5]);
201
+ }
202
+ add_action('save_post', function($post_id, $post) {
203
+ if ($post->post_status === 'publish' && !wp_is_post_revision($post_id)) {
204
+ trigger_vercel_deploy();
205
+ }
206
+ }, 10, 2);
207
+ ```
208
+
209
+ ### Netlify
210
+
211
+ ```php
212
+ define('NETLIFY_BUILD_HOOK', 'https://api.netlify.com/build_hooks/YOUR_HOOK_ID');
213
+
214
+ function trigger_netlify_build() {
215
+ wp_remote_post(NETLIFY_BUILD_HOOK, ['timeout' => 5]);
216
+ }
217
+ ```
218
+
219
+ ## WP-CLI webhook testing
220
+
221
+ ```bash
222
+ # Simulate a webhook fire
223
+ wp eval "do_action('transition_post_status', 'publish', 'draft', get_post(1));"
224
+
225
+ # Test webhook endpoint manually
226
+ curl -X POST https://app.example.com/api/revalidate \
227
+ -H "Content-Type: application/json" \
228
+ -H "X-Revalidate-Secret: your-shared-secret" \
229
+ -d '{"action":"post_updated","path":"/blog/hello-world","slug":"hello-world"}'
230
+ ```
231
+
232
+ ## Using WP Webhooks plugin (alternative)
233
+
234
+ For non-developers or complex workflows:
235
+
236
+ ```bash
237
+ wp plugin install wp-webhooks --activate
238
+ ```
239
+
240
+ The plugin provides a UI for configuring webhook endpoints, triggers, and authentication.
241
+
242
+ ## Security
243
+
244
+ 1. **Always use a shared secret** — validate on the receiver side
245
+ 2. **Use HTTPS** — webhook payloads may contain content data
246
+ 3. **Rate limit** — debounce rapid-fire saves (WordPress autosave triggers frequently)
247
+ 4. **Timeout** — set short timeouts (5s) to avoid blocking WordPress
248
+ 5. **Async processing** — use `wp_schedule_single_event` for non-critical webhooks
249
+
250
+ ### Debouncing
251
+
252
+ ```php
253
+ function send_revalidation_webhook_debounced($path, $payload = []) {
254
+ $key = 'webhook_debounce_' . md5($path);
255
+
256
+ // Skip if webhook was sent for this path in the last 10 seconds
257
+ if (get_transient($key)) return;
258
+
259
+ set_transient($key, true, 10);
260
+ send_revalidation_webhook($path, $payload);
261
+ }
262
+ ```
263
+
264
+ ## Verification
265
+
266
+ ```bash
267
+ # Check webhook constant is defined
268
+ wp config get HEADLESS_WEBHOOK_URL
269
+
270
+ # Test webhook fires on post save
271
+ wp post update 1 --post_title="Webhook Test $(date +%s)"
272
+ # Check frontend logs for incoming webhook
273
+
274
+ # Verify revalidation worked
275
+ curl -s -o /dev/null -w "%{http_code}" https://app.example.com/blog/hello-world
276
+ # Should return 200 with updated content
277
+ ```