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,375 @@
1
+ /**
2
+ * test_inspect.mjs — Detect testing frameworks and configuration in a WordPress project.
3
+ *
4
+ * Scans for Playwright, Jest, PHPUnit, wp-env, and CI config.
5
+ * Outputs a JSON report to stdout with detected frameworks,
6
+ * test directories, configuration files, and CI integration.
7
+ *
8
+ * Usage:
9
+ * node test_inspect.mjs [--cwd=/path/to/check]
10
+ *
11
+ * Exit codes:
12
+ * 0 — at least one test framework detected
13
+ * 1 — no test frameworks detected
14
+ */
15
+
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ import process from "node:process";
19
+ import { execSync } from "node:child_process";
20
+
21
+ const TOOL_VERSION = "1.0.0";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Helpers
25
+ // ---------------------------------------------------------------------------
26
+
27
+ function statSafe(p) {
28
+ try {
29
+ return fs.statSync(p);
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ function readFileSafe(p) {
36
+ try {
37
+ return fs.readFileSync(p, "utf8");
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ function readJsonSafe(p) {
44
+ const raw = readFileSafe(p);
45
+ if (!raw) return null;
46
+ try {
47
+ return JSON.parse(raw);
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ function execSafe(cmd, cwd, timeoutMs = 5000) {
54
+ try {
55
+ return execSync(cmd, { encoding: "utf8", timeout: timeoutMs, cwd, stdio: ["pipe", "pipe", "pipe"] }).trim();
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ function globDirs(base, patterns) {
62
+ const found = [];
63
+ for (const pattern of patterns) {
64
+ const full = path.join(base, pattern);
65
+ if (statSafe(full)?.isDirectory()) {
66
+ found.push(pattern);
67
+ }
68
+ }
69
+ return found;
70
+ }
71
+
72
+ function globFiles(base, patterns) {
73
+ const found = [];
74
+ for (const pattern of patterns) {
75
+ const full = path.join(base, pattern);
76
+ if (statSafe(full)?.isFile()) {
77
+ found.push(pattern);
78
+ }
79
+ }
80
+ return found;
81
+ }
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Parse --cwd argument
85
+ // ---------------------------------------------------------------------------
86
+
87
+ function parseCwd() {
88
+ const cwdArg = process.argv.find((a) => a.startsWith("--cwd="));
89
+ return cwdArg ? cwdArg.slice(6) : process.cwd();
90
+ }
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // Detect Playwright
94
+ // ---------------------------------------------------------------------------
95
+
96
+ function detectPlaywright(cwd) {
97
+ const result = { detected: false, configFile: null, testDirs: [], wpE2eUtils: false };
98
+
99
+ const configFiles = [
100
+ "playwright.config.js",
101
+ "playwright.config.ts",
102
+ "playwright.config.mjs",
103
+ ];
104
+ const found = globFiles(cwd, configFiles);
105
+ if (found.length > 0) {
106
+ result.detected = true;
107
+ result.configFile = found[0];
108
+ }
109
+
110
+ const testDirs = globDirs(cwd, [
111
+ "tests/e2e",
112
+ "tests/playwright",
113
+ "e2e",
114
+ "test/e2e",
115
+ "specs",
116
+ ]);
117
+ result.testDirs = testDirs;
118
+
119
+ // Check for @wordpress/e2e-test-utils-playwright
120
+ const pkg = readJsonSafe(path.join(cwd, "package.json"));
121
+ if (pkg) {
122
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
123
+ if (allDeps["@wordpress/e2e-test-utils-playwright"]) {
124
+ result.wpE2eUtils = true;
125
+ result.detected = true;
126
+ }
127
+ if (allDeps["@playwright/test"] || allDeps["playwright"]) {
128
+ result.detected = true;
129
+ }
130
+ }
131
+
132
+ return result;
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Detect Jest
137
+ // ---------------------------------------------------------------------------
138
+
139
+ function detectJest(cwd) {
140
+ const result = { detected: false, configFile: null, testDirs: [], wpScripts: false };
141
+
142
+ const configFiles = [
143
+ "jest.config.js",
144
+ "jest.config.ts",
145
+ "jest.config.mjs",
146
+ "jest.config.json",
147
+ ];
148
+ const found = globFiles(cwd, configFiles);
149
+ if (found.length > 0) {
150
+ result.detected = true;
151
+ result.configFile = found[0];
152
+ }
153
+
154
+ // Check package.json for jest config
155
+ const pkg = readJsonSafe(path.join(cwd, "package.json"));
156
+ if (pkg) {
157
+ if (pkg.jest) {
158
+ result.detected = true;
159
+ result.configFile = result.configFile || "package.json (jest key)";
160
+ }
161
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
162
+ if (allDeps["@wordpress/scripts"]) {
163
+ result.wpScripts = true;
164
+ result.detected = true;
165
+ }
166
+ if (allDeps["jest"]) {
167
+ result.detected = true;
168
+ }
169
+ }
170
+
171
+ const testDirs = globDirs(cwd, [
172
+ "tests/js",
173
+ "tests/unit",
174
+ "tests/jest",
175
+ "src/__tests__",
176
+ "__tests__",
177
+ "test/js",
178
+ ]);
179
+ result.testDirs = testDirs;
180
+
181
+ return result;
182
+ }
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // Detect PHPUnit
186
+ // ---------------------------------------------------------------------------
187
+
188
+ function detectPHPUnit(cwd) {
189
+ const result = { detected: false, configFile: null, testDirs: [], bootstrap: null };
190
+
191
+ const configFiles = [
192
+ "phpunit.xml",
193
+ "phpunit.xml.dist",
194
+ "phpunit.dist.xml",
195
+ ];
196
+ const found = globFiles(cwd, configFiles);
197
+ if (found.length > 0) {
198
+ result.detected = true;
199
+ result.configFile = found[0];
200
+
201
+ // Parse bootstrap path from XML
202
+ const content = readFileSafe(path.join(cwd, found[0]));
203
+ if (content) {
204
+ const match = content.match(/bootstrap="([^"]+)"/);
205
+ if (match) result.bootstrap = match[1];
206
+ }
207
+ }
208
+
209
+ const testDirs = globDirs(cwd, [
210
+ "tests/phpunit",
211
+ "tests/php",
212
+ "tests/unit",
213
+ "tests",
214
+ ]);
215
+ result.testDirs = testDirs;
216
+
217
+ // Check composer.json
218
+ const composer = readJsonSafe(path.join(cwd, "composer.json"));
219
+ if (composer) {
220
+ const allDeps = { ...composer.require, ...composer["require-dev"] };
221
+ if (allDeps["phpunit/phpunit"] || allDeps["yoast/phpunit-polyfills"]) {
222
+ result.detected = true;
223
+ }
224
+ }
225
+
226
+ return result;
227
+ }
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // Detect wp-env
231
+ // ---------------------------------------------------------------------------
232
+
233
+ function detectWpEnv(cwd) {
234
+ const result = { detected: false, configFile: null, running: false };
235
+
236
+ if (statSafe(path.join(cwd, ".wp-env.json"))?.isFile()) {
237
+ result.detected = true;
238
+ result.configFile = ".wp-env.json";
239
+ }
240
+
241
+ if (statSafe(path.join(cwd, ".wp-env.override.json"))?.isFile()) {
242
+ result.detected = true;
243
+ }
244
+
245
+ const pkg = readJsonSafe(path.join(cwd, "package.json"));
246
+ if (pkg) {
247
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
248
+ if (allDeps["@wordpress/env"]) {
249
+ result.detected = true;
250
+ }
251
+ }
252
+
253
+ // Check if wp-env is running (Docker containers)
254
+ const dockerCheck = execSafe("docker ps --filter name=wp-env --format '{{.Names}}'", cwd);
255
+ if (dockerCheck && dockerCheck.length > 0) {
256
+ result.running = true;
257
+ }
258
+
259
+ return result;
260
+ }
261
+
262
+ // ---------------------------------------------------------------------------
263
+ // Detect CI configuration
264
+ // ---------------------------------------------------------------------------
265
+
266
+ function detectCI(cwd) {
267
+ const result = { detected: false, provider: null, hasTestStep: false };
268
+
269
+ // GitHub Actions
270
+ const ghDirs = globDirs(cwd, [".github/workflows"]);
271
+ if (ghDirs.length > 0) {
272
+ const workflowDir = path.join(cwd, ".github", "workflows");
273
+ try {
274
+ const files = fs.readdirSync(workflowDir);
275
+ for (const file of files) {
276
+ if (file.endsWith(".yml") || file.endsWith(".yaml")) {
277
+ result.detected = true;
278
+ result.provider = "github-actions";
279
+ const content = readFileSafe(path.join(workflowDir, file));
280
+ if (content && /phpunit|jest|playwright|wp-env|npm test|npm run test/i.test(content)) {
281
+ result.hasTestStep = true;
282
+ }
283
+ }
284
+ }
285
+ } catch { /* ignore */ }
286
+ }
287
+
288
+ // GitLab CI
289
+ if (statSafe(path.join(cwd, ".gitlab-ci.yml"))?.isFile()) {
290
+ result.detected = true;
291
+ result.provider = result.provider || "gitlab-ci";
292
+ const content = readFileSafe(path.join(cwd, ".gitlab-ci.yml"));
293
+ if (content && /phpunit|jest|playwright|wp-env/i.test(content)) {
294
+ result.hasTestStep = true;
295
+ }
296
+ }
297
+
298
+ return result;
299
+ }
300
+
301
+ // ---------------------------------------------------------------------------
302
+ // Detect npm test scripts
303
+ // ---------------------------------------------------------------------------
304
+
305
+ function detectScripts(cwd) {
306
+ const pkg = readJsonSafe(path.join(cwd, "package.json"));
307
+ if (!pkg?.scripts) return {};
308
+
309
+ const testScripts = {};
310
+ for (const [key, value] of Object.entries(pkg.scripts)) {
311
+ if (/test|e2e|playwright|jest|phpunit/i.test(key)) {
312
+ testScripts[key] = value;
313
+ }
314
+ }
315
+ return testScripts;
316
+ }
317
+
318
+ // ---------------------------------------------------------------------------
319
+ // Main
320
+ // ---------------------------------------------------------------------------
321
+
322
+ function main() {
323
+ const cwd = parseCwd();
324
+
325
+ if (!statSafe(cwd)?.isDirectory()) {
326
+ console.error(`Error: directory not found: ${cwd}`);
327
+ process.exit(1);
328
+ }
329
+
330
+ const playwright = detectPlaywright(cwd);
331
+ const jest = detectJest(cwd);
332
+ const phpunit = detectPHPUnit(cwd);
333
+ const wpEnv = detectWpEnv(cwd);
334
+ const ci = detectCI(cwd);
335
+ const scripts = detectScripts(cwd);
336
+
337
+ const anyDetected = playwright.detected || jest.detected || phpunit.detected;
338
+
339
+ const report = {
340
+ tool: "test_inspect",
341
+ version: TOOL_VERSION,
342
+ cwd,
343
+ detected: anyDetected,
344
+ frameworks: {
345
+ playwright,
346
+ jest,
347
+ phpunit,
348
+ },
349
+ environment: {
350
+ wpEnv,
351
+ },
352
+ ci,
353
+ scripts,
354
+ recommendations: [],
355
+ };
356
+
357
+ // Generate recommendations
358
+ if (!anyDetected) {
359
+ report.recommendations.push("No test frameworks detected. Consider adding Playwright for E2E and PHPUnit for unit tests.");
360
+ }
361
+ if (!wpEnv.detected && (playwright.detected || phpunit.detected)) {
362
+ report.recommendations.push("Consider adding .wp-env.json for a consistent WordPress test environment.");
363
+ }
364
+ if (anyDetected && !ci.hasTestStep) {
365
+ report.recommendations.push("Test frameworks detected but CI does not run tests. Add a test step to your CI pipeline.");
366
+ }
367
+ if (playwright.detected && !playwright.wpE2eUtils) {
368
+ report.recommendations.push("Consider using @wordpress/e2e-test-utils-playwright for WordPress-specific test helpers.");
369
+ }
370
+
371
+ console.log(JSON.stringify(report, null, 2));
372
+ process.exit(anyDetected ? 0 : 1);
373
+ }
374
+
375
+ main();
@@ -0,0 +1,168 @@
1
+ ---
2
+ name: wp-headless
3
+ description: "Use when building headless/decoupled WordPress architectures: choosing between REST API and WPGraphQL, headless authentication (JWT, application passwords, NextAuth), CORS configuration, frontend framework integration (Next.js, Nuxt, Astro), content webhooks, and ISR/SSG strategies."
4
+ compatibility: "Targets WordPress 6.9+ (PHP 7.2.24+). Filesystem-based agent with bash + node."
5
+ version: 1.0.0
6
+ source: "vinmor/wordpress-manager"
7
+ ---
8
+
9
+ # WP Headless
10
+
11
+ ## When to use
12
+
13
+ Use this skill when building or maintaining a decoupled/headless WordPress architecture:
14
+
15
+ - Building a decoupled site with WordPress as the CMS and a separate frontend
16
+ - Choosing between REST API and WPGraphQL for data fetching
17
+ - Configuring WordPress as a headless CMS backend
18
+ - Integrating with Next.js, Nuxt, or Astro frontends
19
+ - Setting up headless authentication (JWT, application passwords, NextAuth/Auth.js)
20
+ - Configuring CORS for cross-origin API access
21
+ - Implementing content webhooks for on-demand revalidation (ISR)
22
+ - Planning SSG, SSR, or ISR rendering strategies
23
+
24
+ ## Inputs required
25
+
26
+ - **WordPress site**: URL, admin access, hosting type
27
+ - **Frontend framework**: Next.js, Nuxt, Astro, or other
28
+ - **Authentication requirements**: public content only vs authenticated features
29
+ - **Hosting for frontend**: Vercel, Netlify, self-hosted, or other
30
+ - **Deployment strategy**: SSG (static), SSR (server), ISR (incremental), or hybrid
31
+
32
+ ## Procedure
33
+
34
+ ### 0) Detect headless setup
35
+
36
+ Run the detection script to assess the current architecture:
37
+
38
+ ```bash
39
+ node skills/wp-headless/scripts/headless_inspect.mjs --cwd=/path/to/wordpress
40
+ ```
41
+
42
+ The script outputs JSON with:
43
+ - `apiLayer` — REST API and/or WPGraphQL availability, custom endpoints count
44
+ - `frontend` — detected frontend framework (Next.js, Nuxt, Astro)
45
+ - `auth` — authentication methods available
46
+ - `cors` — CORS configuration status and allowed origins
47
+ - `webhooks` — outgoing webhook configuration
48
+ - `isHeadless` — boolean assessment of whether the setup is headless
49
+
50
+ ### 1) Choose API layer
51
+
52
+ Decide between REST API (built-in) and WPGraphQL (plugin) based on project needs.
53
+
54
+ | Factor | REST API | WPGraphQL |
55
+ |--------|----------|-----------|
56
+ | Installation | Built-in, zero setup | Requires plugin |
57
+ | Data fetching | Fixed response shape | Fetch exactly what you need |
58
+ | Related data | Multiple requests | Single query with connections |
59
+ | Learning curve | Low (familiar HTTP) | Medium (GraphQL syntax) |
60
+ | Caching | Simple (HTTP cache) | Complex (query-level) |
61
+ | Best for | Simple sites, mobile apps | Complex content, performance-critical |
62
+
63
+ Use REST with `_fields` parameter for simple needs. Use WPGraphQL for complex content models.
64
+
65
+ Read: `references/api-layer-choice.md`
66
+
67
+ For REST endpoint development, also reference the `wp-rest-api` skill.
68
+
69
+ ### 2) WPGraphQL setup
70
+
71
+ If using WPGraphQL:
72
+ 1. Install: `wp plugin install wp-graphql --activate`
73
+ 2. Explore schema at `/graphql` endpoint with GraphiQL
74
+ 3. Register custom types and fields
75
+ 4. Use cursor-based pagination (`first`/`after`)
76
+ 5. Consider WPGraphQL Smart Cache for performance
77
+
78
+ Read: `references/wpgraphql.md`
79
+
80
+ ### 3) Headless authentication
81
+
82
+ Choose the authentication method based on use case:
83
+
84
+ - **Application Passwords** (built-in): best for server-to-server and build-time fetching
85
+ - **JWT** (plugin): best for client-side authentication flows
86
+ - **NextAuth/Auth.js**: best for Next.js projects with WordPress as OAuth provider
87
+ - **Preview mode**: special auth for draft content preview
88
+
89
+ For security best practices in authentication, reference the `wp-security` skill.
90
+
91
+ Read: `references/headless-auth.md`
92
+
93
+ ### 4) CORS configuration
94
+
95
+ Configure Cross-Origin Resource Sharing to allow the frontend to access WordPress APIs:
96
+
97
+ ```php
98
+ add_filter('allowed_http_origins', function($origins) {
99
+ $origins[] = 'https://frontend.example.com';
100
+ return $origins;
101
+ });
102
+ ```
103
+
104
+ Key rules:
105
+ - Never use `Access-Control-Allow-Origin: *` with credentials
106
+ - Always specify exact origins in production
107
+ - Handle preflight `OPTIONS` requests
108
+ - WPGraphQL has built-in CORS settings
109
+
110
+ Read: `references/cors-config.md`
111
+
112
+ ### 5) Frontend integration
113
+
114
+ Connect the frontend framework to WordPress data:
115
+
116
+ - **Next.js**: `fetch()` in App Router with `revalidate`, `getStaticProps` in Pages Router, ISR for incremental updates
117
+ - **Nuxt**: `useFetch()` / `useAsyncData()`, ISR with `routeRules`
118
+ - **Astro**: content collections from API, static-first with on-demand rendering
119
+
120
+ Common patterns: centralized API client, TypeScript types from schema, image optimization with WordPress media URLs.
121
+
122
+ Read: `references/frontend-integration.md`
123
+
124
+ ### 6) Content webhooks and revalidation
125
+
126
+ Trigger frontend rebuilds or cache invalidation when WordPress content changes:
127
+
128
+ ```php
129
+ add_action('transition_post_status', function($new, $old, $post) {
130
+ if ($new === 'publish') {
131
+ wp_remote_post('https://frontend.example.com/api/revalidate', [
132
+ 'body' => json_encode(['path' => '/' . $post->post_name]),
133
+ 'headers' => ['Content-Type' => 'application/json', 'Authorization' => 'Bearer SECRET'],
134
+ ]);
135
+ }
136
+ }, 10, 3);
137
+ ```
138
+
139
+ Strategies: path-based ISR, tag-based revalidation, full rebuild triggers. WPGraphQL Smart Cache provides automatic invalidation.
140
+
141
+ Read: `references/webhooks.md`
142
+
143
+ ## Verification
144
+
145
+ - API returns data: `curl https://wp.example.com/wp-json/wp/v2/posts` returns JSON
146
+ - Frontend renders WordPress content correctly
147
+ - Authentication works: protected endpoints require credentials, public ones don't
148
+ - CORS headers correct: check `Access-Control-Allow-Origin` in response headers
149
+ - Preview mode: draft content visible in frontend preview
150
+ - Webhooks trigger: publish a post and confirm frontend revalidates
151
+ - Builds succeed: `next build` / `nuxt generate` / `astro build` completes
152
+
153
+ ## Failure modes / debugging
154
+
155
+ - **CORS errors**: check browser DevTools Network tab for preflight failures; verify origin whitelist matches exactly (protocol + domain + port)
156
+ - **Authentication failures**: verify application password format (`user:xxxx xxxx xxxx`), check JWT token expiry, confirm `Authorization` header is forwarded
157
+ - **Stale content**: ISR `revalidate` interval too high; webhook not triggering; check `transition_post_status` hook fires on publish
158
+ - **GraphQL schema missing fields**: custom post types need `show_in_graphql => true`; ACF fields need WPGraphQL for ACF extension
159
+ - **Preview not working**: draft mode API route misconfigured; preview secret mismatch; WordPress preview URL not pointing to frontend
160
+ - **Build failures**: API unreachable during build; increase timeout; add fallback for missing data
161
+
162
+ ## Escalation
163
+
164
+ - WPGraphQL documentation: https://www.wpgraphql.com/docs
165
+ - Next.js WordPress examples: https://github.com/vercel/next.js/tree/canary/examples/cms-wordpress
166
+ - Astro WordPress integration: https://docs.astro.build/en/guides/cms/wordpress/
167
+ - For REST endpoint development, use the `wp-rest-api` skill
168
+ - For authentication security, use the `wp-security` skill
@@ -0,0 +1,160 @@
1
+ # API Layer Choice
2
+
3
+ Use this file when deciding between REST API and WPGraphQL for a headless WordPress project.
4
+
5
+ ## Comparison matrix
6
+
7
+ | Criterion | REST API | WPGraphQL |
8
+ |-----------|----------|-----------|
9
+ | Built into core | Yes (since 4.7) | Plugin required |
10
+ | Data fetching | Fixed endpoints, multiple requests | Single query, exact fields |
11
+ | Over-fetching | Common (returns all fields) | Eliminated (request only what you need) |
12
+ | Under-fetching | Common (need multiple requests) | Eliminated (nested queries) |
13
+ | Caching | HTTP caching (CDN-friendly) | Requires custom caching layer |
14
+ | Learning curve | Lower (familiar REST patterns) | Higher (GraphQL query language) |
15
+ | Community/ecosystem | Largest | Growing, strong Gatsby/Next.js integration |
16
+ | Real-time | Polling or custom | Subscriptions (with extensions) |
17
+ | Authentication | Cookie, Application Passwords, JWT | Same as REST + GraphQL-specific |
18
+ | File uploads | Native multipart | Requires separate REST endpoint |
19
+ | Performance | Predictable | Faster for complex pages, slower for simple |
20
+ | Debugging | Standard HTTP tools | Requires GraphQL client (GraphiQL) |
21
+
22
+ ## When to choose REST API
23
+
24
+ - **Simple content sites** — blog, portfolio, brochure
25
+ - **CDN-heavy architecture** — REST responses cache naturally at edge
26
+ - **Team unfamiliar with GraphQL** — lower learning curve
27
+ - **Third-party integrations** — most services expect REST
28
+ - **WooCommerce headless** — WooCommerce REST API is mature and well-documented
29
+ - **Mobile apps** — REST is universal across platforms
30
+ - **Server-side rendering with few queries** — ISR/SSG pages that make 1-3 API calls
31
+
32
+ ## When to choose WPGraphQL
33
+
34
+ - **Complex page compositions** — homepage with posts, categories, menus, options, custom fields
35
+ - **Component-driven frontend** — React/Vue components that each declare their data needs
36
+ - **Gatsby projects** — gatsby-source-wordpress uses WPGraphQL natively
37
+ - **Deeply nested data** — posts → author → posts → categories in one query
38
+ - **Multiple content types per page** — dashboard-style layouts
39
+ - **Rapid frontend development** — frontend devs query exactly what they need
40
+
41
+ ## Hybrid approach
42
+
43
+ Use both:
44
+
45
+ ```
46
+ REST API → Simple CRUD, file uploads, WooCommerce, webhooks
47
+ WPGraphQL → Complex page data fetching, component queries
48
+ ```
49
+
50
+ This is common in production. Example:
51
+ - Blog listing page: WPGraphQL (needs posts + categories + featured images + author in one query)
52
+ - Contact form submission: REST API (simple POST)
53
+ - WooCommerce cart/checkout: WooCommerce REST API
54
+ - Media upload: REST API (multipart form data)
55
+
56
+ ## REST API quick setup for headless
57
+
58
+ ```php
59
+ // Register custom endpoint
60
+ add_action('rest_api_init', function() {
61
+ register_rest_route('myapp/v1', '/homepage', [
62
+ 'methods' => 'GET',
63
+ 'callback' => 'get_homepage_data',
64
+ 'permission_callback' => '__return_true',
65
+ ]);
66
+ });
67
+
68
+ function get_homepage_data() {
69
+ return [
70
+ 'hero' => get_field('hero', 'option'),
71
+ 'posts' => get_posts(['numberposts' => 6, 'post_type' => 'post']),
72
+ 'menu' => wp_get_nav_menu_items('primary'),
73
+ ];
74
+ }
75
+ ```
76
+
77
+ ## WPGraphQL quick setup for headless
78
+
79
+ ```bash
80
+ wp plugin install wp-graphql --activate
81
+ ```
82
+
83
+ ```graphql
84
+ # Single query for a complex homepage
85
+ query Homepage {
86
+ posts(first: 6) {
87
+ nodes {
88
+ title
89
+ excerpt
90
+ uri
91
+ featuredImage {
92
+ node {
93
+ sourceUrl(size: MEDIUM_LARGE)
94
+ altText
95
+ }
96
+ }
97
+ categories {
98
+ nodes {
99
+ name
100
+ slug
101
+ }
102
+ }
103
+ }
104
+ }
105
+ menus(where: { location: PRIMARY }) {
106
+ nodes {
107
+ menuItems {
108
+ nodes {
109
+ label
110
+ url
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+ ```
117
+
118
+ ## Performance considerations
119
+
120
+ ### REST API
121
+
122
+ ```
123
+ Homepage data:
124
+ GET /wp-json/wp/v2/posts?per_page=6 → 1 request
125
+ GET /wp-json/wp/v2/categories → 1 request
126
+ GET /wp-json/wp/v2/media/{id} (per post) → 6 requests
127
+ GET /wp-json/wp/v2/menus/primary → 1 request
128
+ Total: 9 requests, ~150KB response (with over-fetching)
129
+ ```
130
+
131
+ ### WPGraphQL
132
+
133
+ ```
134
+ Homepage data:
135
+ POST /graphql (single query) → 1 request
136
+ Total: 1 request, ~25KB response (exact fields only)
137
+ ```
138
+
139
+ ### Mitigation for REST over-fetching
140
+
141
+ ```php
142
+ // Use _fields parameter to reduce payload
143
+ // GET /wp-json/wp/v2/posts?_fields=id,title,excerpt,featured_media
144
+
145
+ // Or create custom endpoints that aggregate data
146
+ register_rest_route('myapp/v1', '/homepage', [
147
+ 'callback' => function() {
148
+ // Return exactly what the frontend needs
149
+ }
150
+ ]);
151
+ ```
152
+
153
+ ## Decision checklist
154
+
155
+ 1. What is the team's GraphQL experience? (low → REST)
156
+ 2. How many API calls per page? (>3 → consider WPGraphQL)
157
+ 3. Is edge caching critical? (yes → REST preferred)
158
+ 4. Using Gatsby? (yes → WPGraphQL)
159
+ 5. Using WooCommerce? (yes → REST for commerce, WPGraphQL for content)
160
+ 6. How nested is the data model? (deeply nested → WPGraphQL)