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,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)
|