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.
Files changed (81) hide show
  1. package/.claude-plugin/plugin.json +7 -3
  2. package/CHANGELOG.md +111 -0
  3. package/README.md +10 -3
  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/GUIDE.md +68 -15
  13. package/docs/guides/INDEX.md +46 -0
  14. package/docs/guides/wp-blog.md +590 -0
  15. package/docs/guides/wp-design-system.md +976 -0
  16. package/docs/guides/wp-ecommerce.md +786 -0
  17. package/docs/guides/wp-landing-page.md +762 -0
  18. package/docs/guides/wp-portfolio.md +713 -0
  19. package/docs/plans/2026-02-27-design-system-guide-design.md +30 -0
  20. package/docs/plans/2026-02-27-local-dev-tools-assessment.md +332 -0
  21. package/docs/plans/2026-02-27-local-env-design.md +179 -0
  22. package/docs/plans/2026-02-27-site-type-guides-design.md +44 -0
  23. package/package.json +7 -3
  24. package/skills/wordpress-router/SKILL.md +25 -5
  25. package/skills/wordpress-router/references/decision-tree.md +59 -3
  26. package/skills/wp-accessibility/SKILL.md +170 -0
  27. package/skills/wp-accessibility/references/a11y-audit-tools.md +248 -0
  28. package/skills/wp-accessibility/references/a11y-testing.md +222 -0
  29. package/skills/wp-accessibility/references/block-a11y.md +247 -0
  30. package/skills/wp-accessibility/references/interactive-a11y.md +272 -0
  31. package/skills/wp-accessibility/references/media-a11y.md +254 -0
  32. package/skills/wp-accessibility/references/theme-a11y.md +309 -0
  33. package/skills/wp-audit/SKILL.md +4 -0
  34. package/skills/wp-block-development/SKILL.md +5 -0
  35. package/skills/wp-block-themes/SKILL.md +4 -0
  36. package/skills/wp-deploy/SKILL.md +12 -0
  37. package/skills/wp-e2e-testing/SKILL.md +186 -0
  38. package/skills/wp-e2e-testing/references/ci-integration.md +174 -0
  39. package/skills/wp-e2e-testing/references/jest-wordpress.md +114 -0
  40. package/skills/wp-e2e-testing/references/phpunit-wordpress.md +141 -0
  41. package/skills/wp-e2e-testing/references/playwright-wordpress.md +108 -0
  42. package/skills/wp-e2e-testing/references/test-data-generation.md +127 -0
  43. package/skills/wp-e2e-testing/references/visual-regression.md +107 -0
  44. package/skills/wp-e2e-testing/references/wp-env-setup.md +97 -0
  45. package/skills/wp-e2e-testing/scripts/test_inspect.mjs +375 -0
  46. package/skills/wp-headless/SKILL.md +168 -0
  47. package/skills/wp-headless/references/api-layer-choice.md +160 -0
  48. package/skills/wp-headless/references/cors-config.md +245 -0
  49. package/skills/wp-headless/references/frontend-integration.md +331 -0
  50. package/skills/wp-headless/references/headless-auth.md +286 -0
  51. package/skills/wp-headless/references/webhooks.md +277 -0
  52. package/skills/wp-headless/references/wpgraphql.md +331 -0
  53. package/skills/wp-headless/scripts/headless_inspect.mjs +321 -0
  54. package/skills/wp-i18n/SKILL.md +170 -0
  55. package/skills/wp-i18n/references/js-i18n.md +201 -0
  56. package/skills/wp-i18n/references/multilingual-setup.md +219 -0
  57. package/skills/wp-i18n/references/php-i18n.md +196 -0
  58. package/skills/wp-i18n/references/rtl-support.md +206 -0
  59. package/skills/wp-i18n/references/translation-workflow.md +178 -0
  60. package/skills/wp-i18n/references/wpcli-i18n.md +177 -0
  61. package/skills/wp-i18n/scripts/i18n_inspect.mjs +330 -0
  62. package/skills/wp-interactivity-api/SKILL.md +4 -0
  63. package/skills/wp-local-env/SKILL.md +233 -0
  64. package/skills/wp-local-env/references/localwp-adapter.md +156 -0
  65. package/skills/wp-local-env/references/mcp-adapter-setup.md +153 -0
  66. package/skills/wp-local-env/references/studio-adapter.md +127 -0
  67. package/skills/wp-local-env/references/wpenv-adapter.md +121 -0
  68. package/skills/wp-local-env/scripts/detect_local_env.mjs +404 -0
  69. package/skills/wp-playground/SKILL.md +13 -1
  70. package/skills/wp-plugin-development/SKILL.md +6 -0
  71. package/skills/wp-rest-api/SKILL.md +4 -0
  72. package/skills/wp-security/SKILL.md +179 -0
  73. package/skills/wp-security/references/api-restriction.md +147 -0
  74. package/skills/wp-security/references/authentication-hardening.md +105 -0
  75. package/skills/wp-security/references/filesystem-hardening.md +105 -0
  76. package/skills/wp-security/references/http-headers.md +105 -0
  77. package/skills/wp-security/references/incident-response.md +144 -0
  78. package/skills/wp-security/references/user-capabilities.md +115 -0
  79. package/skills/wp-security/references/wp-config-security.md +129 -0
  80. package/skills/wp-security/scripts/security_inspect.mjs +393 -0
  81. 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
+ ```