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.
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +97 -0
- package/README.md +27 -13
- package/agents/wp-accessibility-auditor.md +206 -0
- package/agents/wp-content-strategist.md +18 -0
- package/agents/wp-deployment-engineer.md +34 -2
- package/agents/wp-performance-optimizer.md +12 -0
- package/agents/wp-security-auditor.md +20 -0
- package/agents/wp-security-hardener.md +266 -0
- package/agents/wp-site-manager.md +14 -0
- package/agents/wp-test-engineer.md +207 -0
- package/docs/guides/INDEX.md +46 -0
- package/docs/guides/wp-blog.md +590 -0
- package/docs/guides/wp-design-system.md +976 -0
- package/docs/guides/wp-ecommerce.md +786 -0
- package/docs/guides/wp-landing-page.md +762 -0
- package/docs/guides/wp-portfolio.md +713 -0
- package/docs/plans/2026-02-27-design-system-guide-design.md +30 -0
- package/docs/plans/2026-02-27-site-type-guides-design.md +44 -0
- package/package.json +2 -2
- package/skills/wordpress-router/references/decision-tree.md +12 -2
- package/skills/wp-accessibility/SKILL.md +170 -0
- package/skills/wp-accessibility/references/a11y-audit-tools.md +248 -0
- package/skills/wp-accessibility/references/a11y-testing.md +222 -0
- package/skills/wp-accessibility/references/block-a11y.md +247 -0
- package/skills/wp-accessibility/references/interactive-a11y.md +272 -0
- package/skills/wp-accessibility/references/media-a11y.md +254 -0
- package/skills/wp-accessibility/references/theme-a11y.md +309 -0
- package/skills/wp-audit/SKILL.md +4 -0
- package/skills/wp-block-development/SKILL.md +5 -0
- package/skills/wp-block-themes/SKILL.md +4 -0
- package/skills/wp-e2e-testing/SKILL.md +186 -0
- package/skills/wp-e2e-testing/references/ci-integration.md +174 -0
- package/skills/wp-e2e-testing/references/jest-wordpress.md +114 -0
- package/skills/wp-e2e-testing/references/phpunit-wordpress.md +141 -0
- package/skills/wp-e2e-testing/references/playwright-wordpress.md +108 -0
- package/skills/wp-e2e-testing/references/test-data-generation.md +127 -0
- package/skills/wp-e2e-testing/references/visual-regression.md +107 -0
- package/skills/wp-e2e-testing/references/wp-env-setup.md +97 -0
- package/skills/wp-e2e-testing/scripts/test_inspect.mjs +375 -0
- package/skills/wp-headless/SKILL.md +168 -0
- package/skills/wp-headless/references/api-layer-choice.md +160 -0
- package/skills/wp-headless/references/cors-config.md +245 -0
- package/skills/wp-headless/references/frontend-integration.md +331 -0
- package/skills/wp-headless/references/headless-auth.md +286 -0
- package/skills/wp-headless/references/webhooks.md +277 -0
- package/skills/wp-headless/references/wpgraphql.md +331 -0
- package/skills/wp-headless/scripts/headless_inspect.mjs +321 -0
- package/skills/wp-i18n/SKILL.md +170 -0
- package/skills/wp-i18n/references/js-i18n.md +201 -0
- package/skills/wp-i18n/references/multilingual-setup.md +219 -0
- package/skills/wp-i18n/references/php-i18n.md +196 -0
- package/skills/wp-i18n/references/rtl-support.md +206 -0
- package/skills/wp-i18n/references/translation-workflow.md +178 -0
- package/skills/wp-i18n/references/wpcli-i18n.md +177 -0
- package/skills/wp-i18n/scripts/i18n_inspect.mjs +330 -0
- package/skills/wp-interactivity-api/SKILL.md +4 -0
- package/skills/wp-plugin-development/SKILL.md +6 -0
- package/skills/wp-rest-api/SKILL.md +4 -0
- package/skills/wp-security/SKILL.md +179 -0
- package/skills/wp-security/references/api-restriction.md +147 -0
- package/skills/wp-security/references/authentication-hardening.md +105 -0
- package/skills/wp-security/references/filesystem-hardening.md +105 -0
- package/skills/wp-security/references/http-headers.md +105 -0
- package/skills/wp-security/references/incident-response.md +144 -0
- package/skills/wp-security/references/user-capabilities.md +115 -0
- package/skills/wp-security/references/wp-config-security.md +129 -0
- 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
|
+
```
|