claude-plugin-wordpress-manager 1.4.0 → 1.7.0
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 +7 -3
- package/CHANGELOG.md +111 -0
- package/README.md +10 -3
- 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/GUIDE.md +68 -15
- 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-local-dev-tools-assessment.md +332 -0
- package/docs/plans/2026-02-27-local-env-design.md +179 -0
- package/docs/plans/2026-02-27-site-type-guides-design.md +44 -0
- package/package.json +7 -3
- package/skills/wordpress-router/SKILL.md +25 -5
- package/skills/wordpress-router/references/decision-tree.md +59 -3
- 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-deploy/SKILL.md +12 -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-local-env/SKILL.md +233 -0
- package/skills/wp-local-env/references/localwp-adapter.md +156 -0
- package/skills/wp-local-env/references/mcp-adapter-setup.md +153 -0
- package/skills/wp-local-env/references/studio-adapter.md +127 -0
- package/skills/wp-local-env/references/wpenv-adapter.md +121 -0
- package/skills/wp-local-env/scripts/detect_local_env.mjs +404 -0
- package/skills/wp-playground/SKILL.md +13 -1
- 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
- package/skills/wp-wpcli-and-ops/SKILL.md +6 -0
|
@@ -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
|
+
```
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# WPGraphQL
|
|
2
|
+
|
|
3
|
+
Use this file when setting up and using WPGraphQL for headless WordPress.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
wp plugin install wp-graphql --activate
|
|
9
|
+
|
|
10
|
+
# Verify
|
|
11
|
+
wp graphql --help
|
|
12
|
+
curl -s http://localhost:8888/graphql -H "Content-Type: application/json" \
|
|
13
|
+
-d '{"query": "{ generalSettings { title } }"}' | jq
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
GraphQL IDE: `http://localhost:8888/wp-admin/admin.php?page=graphiql-ide`
|
|
17
|
+
|
|
18
|
+
## Common queries
|
|
19
|
+
|
|
20
|
+
### Posts
|
|
21
|
+
|
|
22
|
+
```graphql
|
|
23
|
+
# List posts with pagination
|
|
24
|
+
query GetPosts($first: Int = 10, $after: String) {
|
|
25
|
+
posts(first: $first, after: $after) {
|
|
26
|
+
pageInfo {
|
|
27
|
+
hasNextPage
|
|
28
|
+
endCursor
|
|
29
|
+
}
|
|
30
|
+
nodes {
|
|
31
|
+
id
|
|
32
|
+
databaseId
|
|
33
|
+
title
|
|
34
|
+
slug
|
|
35
|
+
date
|
|
36
|
+
excerpt
|
|
37
|
+
content
|
|
38
|
+
uri
|
|
39
|
+
featuredImage {
|
|
40
|
+
node {
|
|
41
|
+
sourceUrl(size: LARGE)
|
|
42
|
+
altText
|
|
43
|
+
mediaDetails {
|
|
44
|
+
width
|
|
45
|
+
height
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
author {
|
|
50
|
+
node {
|
|
51
|
+
name
|
|
52
|
+
avatar {
|
|
53
|
+
url
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
categories {
|
|
58
|
+
nodes {
|
|
59
|
+
name
|
|
60
|
+
slug
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
tags {
|
|
64
|
+
nodes {
|
|
65
|
+
name
|
|
66
|
+
slug
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Single post by slug
|
|
75
|
+
|
|
76
|
+
```graphql
|
|
77
|
+
query GetPost($slug: ID!) {
|
|
78
|
+
post(id: $slug, idType: SLUG) {
|
|
79
|
+
title
|
|
80
|
+
content
|
|
81
|
+
date
|
|
82
|
+
modified
|
|
83
|
+
seo {
|
|
84
|
+
title
|
|
85
|
+
metaDesc
|
|
86
|
+
opengraphImage {
|
|
87
|
+
sourceUrl
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
author {
|
|
91
|
+
node {
|
|
92
|
+
name
|
|
93
|
+
description
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Pages
|
|
101
|
+
|
|
102
|
+
```graphql
|
|
103
|
+
query GetPage($uri: String!) {
|
|
104
|
+
pageBy(uri: $uri) {
|
|
105
|
+
title
|
|
106
|
+
content
|
|
107
|
+
template {
|
|
108
|
+
templateName
|
|
109
|
+
}
|
|
110
|
+
children {
|
|
111
|
+
nodes {
|
|
112
|
+
... on Page {
|
|
113
|
+
title
|
|
114
|
+
uri
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Menus
|
|
123
|
+
|
|
124
|
+
```graphql
|
|
125
|
+
query GetMenu {
|
|
126
|
+
menus(where: { location: PRIMARY }) {
|
|
127
|
+
nodes {
|
|
128
|
+
menuItems(first: 50) {
|
|
129
|
+
nodes {
|
|
130
|
+
id
|
|
131
|
+
label
|
|
132
|
+
url
|
|
133
|
+
parentId
|
|
134
|
+
cssClasses
|
|
135
|
+
target
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Custom Post Types
|
|
144
|
+
|
|
145
|
+
```php
|
|
146
|
+
// Register CPT with GraphQL support
|
|
147
|
+
register_post_type('product', [
|
|
148
|
+
'label' => 'Products',
|
|
149
|
+
'public' => true,
|
|
150
|
+
'show_in_graphql' => true,
|
|
151
|
+
'graphql_single_name' => 'product',
|
|
152
|
+
'graphql_plural_name' => 'products',
|
|
153
|
+
]);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
```graphql
|
|
157
|
+
query GetProducts {
|
|
158
|
+
products(first: 12) {
|
|
159
|
+
nodes {
|
|
160
|
+
title
|
|
161
|
+
slug
|
|
162
|
+
productFields { # ACF field group
|
|
163
|
+
price
|
|
164
|
+
sku
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Custom Taxonomies
|
|
172
|
+
|
|
173
|
+
```php
|
|
174
|
+
register_taxonomy('product_category', 'product', [
|
|
175
|
+
'label' => 'Product Categories',
|
|
176
|
+
'show_in_graphql' => true,
|
|
177
|
+
'graphql_single_name' => 'productCategory',
|
|
178
|
+
'graphql_plural_name' => 'productCategories',
|
|
179
|
+
]);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## ACF integration
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
wp plugin install wpgraphql-acf --activate
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
ACF fields are automatically exposed when the field group's "Show in GraphQL" setting is enabled.
|
|
189
|
+
|
|
190
|
+
```graphql
|
|
191
|
+
query GetPostWithACF {
|
|
192
|
+
post(id: "hello-world", idType: SLUG) {
|
|
193
|
+
title
|
|
194
|
+
customFields { # ACF field group name (camelCase)
|
|
195
|
+
subtitle
|
|
196
|
+
heroImage {
|
|
197
|
+
sourceUrl
|
|
198
|
+
altText
|
|
199
|
+
}
|
|
200
|
+
features { # Repeater field
|
|
201
|
+
title
|
|
202
|
+
description
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Custom resolvers
|
|
210
|
+
|
|
211
|
+
```php
|
|
212
|
+
// Add custom field to existing type
|
|
213
|
+
add_action('graphql_register_types', function() {
|
|
214
|
+
register_graphql_field('Post', 'readingTime', [
|
|
215
|
+
'type' => 'Int',
|
|
216
|
+
'description' => 'Estimated reading time in minutes',
|
|
217
|
+
'resolve' => function($post) {
|
|
218
|
+
$content = get_post_field('post_content', $post->databaseId);
|
|
219
|
+
$word_count = str_word_count(strip_tags($content));
|
|
220
|
+
return max(1, ceil($word_count / 200));
|
|
221
|
+
},
|
|
222
|
+
]);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Add custom root query
|
|
226
|
+
add_action('graphql_register_types', function() {
|
|
227
|
+
register_graphql_field('RootQuery', 'siteOptions', [
|
|
228
|
+
'type' => 'SiteOptions',
|
|
229
|
+
'description' => 'Global site options',
|
|
230
|
+
'resolve' => function() {
|
|
231
|
+
return [
|
|
232
|
+
'phone' => get_option('site_phone'),
|
|
233
|
+
'address' => get_option('site_address'),
|
|
234
|
+
];
|
|
235
|
+
},
|
|
236
|
+
]);
|
|
237
|
+
|
|
238
|
+
register_graphql_object_type('SiteOptions', [
|
|
239
|
+
'fields' => [
|
|
240
|
+
'phone' => ['type' => 'String'],
|
|
241
|
+
'address' => ['type' => 'String'],
|
|
242
|
+
],
|
|
243
|
+
]);
|
|
244
|
+
});
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Mutations
|
|
248
|
+
|
|
249
|
+
```graphql
|
|
250
|
+
# Create a comment (authenticated)
|
|
251
|
+
mutation CreateComment($input: CreateCommentInput!) {
|
|
252
|
+
createComment(input: $input) {
|
|
253
|
+
success
|
|
254
|
+
comment {
|
|
255
|
+
id
|
|
256
|
+
content
|
|
257
|
+
date
|
|
258
|
+
author {
|
|
259
|
+
node {
|
|
260
|
+
name
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Variables:
|
|
269
|
+
```json
|
|
270
|
+
{
|
|
271
|
+
"input": {
|
|
272
|
+
"commentOn": 1,
|
|
273
|
+
"content": "Great post!",
|
|
274
|
+
"author": "John"
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Performance
|
|
280
|
+
|
|
281
|
+
### Query complexity limits
|
|
282
|
+
|
|
283
|
+
WPGraphQL enforces query depth and complexity limits by default.
|
|
284
|
+
|
|
285
|
+
```php
|
|
286
|
+
// Adjust limits in wp-config.php or plugin
|
|
287
|
+
add_filter('graphql_max_query_amount', function() {
|
|
288
|
+
return 100; // max nodes per query (default: 100)
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Persisted queries
|
|
293
|
+
|
|
294
|
+
```php
|
|
295
|
+
// Register a persisted query
|
|
296
|
+
add_action('graphql_register_types', function() {
|
|
297
|
+
register_graphql_query_alias('homepage', '
|
|
298
|
+
query Homepage {
|
|
299
|
+
posts(first: 6) { nodes { title slug excerpt } }
|
|
300
|
+
menus(where: { location: PRIMARY }) { nodes { menuItems { nodes { label url } } } }
|
|
301
|
+
}
|
|
302
|
+
');
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Object caching
|
|
307
|
+
|
|
308
|
+
WPGraphQL integrates with WordPress object cache. Use Redis or Memcached for production:
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
wp plugin install wp-graphql-smart-cache --activate
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Verification
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
# Test GraphQL endpoint
|
|
318
|
+
curl -s http://localhost:8888/graphql \
|
|
319
|
+
-H "Content-Type: application/json" \
|
|
320
|
+
-d '{"query": "{ posts(first: 1) { nodes { title } } }"}' | jq
|
|
321
|
+
|
|
322
|
+
# Check schema introspection
|
|
323
|
+
curl -s http://localhost:8888/graphql \
|
|
324
|
+
-H "Content-Type: application/json" \
|
|
325
|
+
-d '{"query": "{ __schema { types { name } } }"}' | jq '.data.__schema.types | length'
|
|
326
|
+
|
|
327
|
+
# Verify CPT is registered in schema
|
|
328
|
+
curl -s http://localhost:8888/graphql \
|
|
329
|
+
-H "Content-Type: application/json" \
|
|
330
|
+
-d '{"query": "{ __type(name: \"Product\") { name fields { name } } }"}' | jq
|
|
331
|
+
```
|