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,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
|
+
```
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* headless_inspect.mjs — Detect headless WordPress configuration.
|
|
3
|
+
*
|
|
4
|
+
* Scans for WPGraphQL, CORS config, frontend framework integration,
|
|
5
|
+
* and decoupled architecture indicators.
|
|
6
|
+
* Outputs a JSON report to stdout.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node headless_inspect.mjs [--cwd=/path/to/check]
|
|
10
|
+
*
|
|
11
|
+
* Exit codes:
|
|
12
|
+
* 0 — headless indicators detected
|
|
13
|
+
* 1 — no headless indicators 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 readdirSafe(dir) {
|
|
62
|
+
try {
|
|
63
|
+
return fs.readdirSync(dir);
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Parse --cwd argument
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
function parseCwd() {
|
|
74
|
+
const cwdArg = process.argv.find((a) => a.startsWith("--cwd="));
|
|
75
|
+
return cwdArg ? cwdArg.slice(6) : process.cwd();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Detect WPGraphQL
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
function detectWPGraphQL(cwd) {
|
|
83
|
+
const result = { detected: false, plugins: [] };
|
|
84
|
+
|
|
85
|
+
const pluginsDir = path.join(cwd, "wp-content", "plugins");
|
|
86
|
+
const graphqlPlugins = [
|
|
87
|
+
{ dir: "wp-graphql", name: "WPGraphQL" },
|
|
88
|
+
{ dir: "wpgraphql-acf", name: "WPGraphQL for ACF" },
|
|
89
|
+
{ dir: "wp-graphql-jwt-authentication", name: "WPGraphQL JWT Auth" },
|
|
90
|
+
{ dir: "wp-graphql-smart-cache", name: "WPGraphQL Smart Cache" },
|
|
91
|
+
{ dir: "wp-graphql-woocommerce", name: "WPGraphQL WooCommerce" },
|
|
92
|
+
{ dir: "wp-gatsby", name: "WP Gatsby" },
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
for (const plugin of graphqlPlugins) {
|
|
96
|
+
if (statSafe(path.join(pluginsDir, plugin.dir))?.isDirectory()) {
|
|
97
|
+
result.detected = true;
|
|
98
|
+
result.plugins.push(plugin.name);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check composer.json for WPGraphQL
|
|
103
|
+
const composer = readJsonSafe(path.join(cwd, "composer.json"));
|
|
104
|
+
if (composer) {
|
|
105
|
+
const allDeps = { ...composer.require, ...composer["require-dev"] };
|
|
106
|
+
if (allDeps["wp-graphql/wp-graphql"]) {
|
|
107
|
+
result.detected = true;
|
|
108
|
+
if (!result.plugins.includes("WPGraphQL")) result.plugins.push("WPGraphQL (composer)");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Detect CORS configuration
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
function detectCORS(cwd) {
|
|
120
|
+
const result = { detected: false, sources: [] };
|
|
121
|
+
|
|
122
|
+
// Check .htaccess
|
|
123
|
+
const htaccess = readFileSafe(path.join(cwd, ".htaccess"));
|
|
124
|
+
if (htaccess && /Access-Control-Allow-Origin/i.test(htaccess)) {
|
|
125
|
+
result.detected = true;
|
|
126
|
+
result.sources.push(".htaccess");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check wp-config.php for CORS constants
|
|
130
|
+
const wpConfig = readFileSafe(path.join(cwd, "wp-config.php"));
|
|
131
|
+
if (wpConfig && /CORS|Access-Control/i.test(wpConfig)) {
|
|
132
|
+
result.detected = true;
|
|
133
|
+
result.sources.push("wp-config.php");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check PHP files for CORS headers
|
|
137
|
+
const corsPhp = execSafe(
|
|
138
|
+
`grep -rl --include="*.php" "Access-Control-Allow-Origin" . 2>/dev/null | head -5`,
|
|
139
|
+
cwd
|
|
140
|
+
);
|
|
141
|
+
if (corsPhp && corsPhp.length > 0) {
|
|
142
|
+
result.detected = true;
|
|
143
|
+
const files = corsPhp.split("\n").map((f) => path.relative(cwd, f));
|
|
144
|
+
result.sources.push(...files.filter((f) => !result.sources.includes(f)));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check nginx config (common locations)
|
|
148
|
+
for (const confPath of ["/etc/nginx/sites-enabled/default", "/etc/nginx/conf.d/default.conf"]) {
|
|
149
|
+
const content = readFileSafe(confPath);
|
|
150
|
+
if (content && /Access-Control-Allow-Origin/i.test(content)) {
|
|
151
|
+
result.detected = true;
|
|
152
|
+
result.sources.push(confPath);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Detect frontend framework
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
function detectFrontend(cwd) {
|
|
164
|
+
const result = { detected: false, framework: null, location: null };
|
|
165
|
+
|
|
166
|
+
// Check for frontend directories
|
|
167
|
+
const frontendDirs = ["frontend", "client", "app", "web", "next", "nuxt"];
|
|
168
|
+
for (const dir of frontendDirs) {
|
|
169
|
+
const pkgPath = path.join(cwd, dir, "package.json");
|
|
170
|
+
const pkg = readJsonSafe(pkgPath);
|
|
171
|
+
if (pkg) {
|
|
172
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
173
|
+
if (allDeps["next"]) {
|
|
174
|
+
result.detected = true;
|
|
175
|
+
result.framework = "Next.js";
|
|
176
|
+
result.location = dir;
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
if (allDeps["nuxt"] || allDeps["nuxt3"]) {
|
|
180
|
+
result.detected = true;
|
|
181
|
+
result.framework = "Nuxt";
|
|
182
|
+
result.location = dir;
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
if (allDeps["astro"]) {
|
|
186
|
+
result.detected = true;
|
|
187
|
+
result.framework = "Astro";
|
|
188
|
+
result.location = dir;
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
if (allDeps["gatsby"]) {
|
|
192
|
+
result.detected = true;
|
|
193
|
+
result.framework = "Gatsby";
|
|
194
|
+
result.location = dir;
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check root package.json
|
|
201
|
+
const rootPkg = readJsonSafe(path.join(cwd, "package.json"));
|
|
202
|
+
if (rootPkg) {
|
|
203
|
+
const allDeps = { ...rootPkg.dependencies, ...rootPkg.devDependencies };
|
|
204
|
+
if (allDeps["next"]) { result.detected = true; result.framework = "Next.js"; result.location = "."; }
|
|
205
|
+
else if (allDeps["nuxt"] || allDeps["nuxt3"]) { result.detected = true; result.framework = "Nuxt"; result.location = "."; }
|
|
206
|
+
else if (allDeps["astro"]) { result.detected = true; result.framework = "Astro"; result.location = "."; }
|
|
207
|
+
else if (allDeps["gatsby"]) { result.detected = true; result.framework = "Gatsby"; result.location = "."; }
|
|
208
|
+
else if (allDeps["gatsby-source-wordpress"]) { result.detected = true; result.framework = "Gatsby"; result.location = "."; }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Detect headless indicators in WordPress
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
function detectHeadlessIndicators(cwd) {
|
|
219
|
+
const result = { indicators: [] };
|
|
220
|
+
|
|
221
|
+
// Check for headless theme (minimal or API-only themes)
|
|
222
|
+
const themeDir = path.join(cwd, "wp-content", "themes");
|
|
223
|
+
if (statSafe(themeDir)?.isDirectory()) {
|
|
224
|
+
const themes = readdirSafe(themeDir);
|
|
225
|
+
for (const theme of themes) {
|
|
226
|
+
const functionsPhp = readFileSafe(path.join(themeDir, theme, "functions.php"));
|
|
227
|
+
if (functionsPhp && /headless|decoupled|api.only/i.test(functionsPhp)) {
|
|
228
|
+
result.indicators.push(`Headless theme detected: ${theme}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Check for webhook configuration
|
|
234
|
+
const wpConfig = readFileSafe(path.join(cwd, "wp-config.php"));
|
|
235
|
+
if (wpConfig) {
|
|
236
|
+
if (/HEADLESS_WEBHOOK/i.test(wpConfig)) {
|
|
237
|
+
result.indicators.push("Webhook configuration found in wp-config.php");
|
|
238
|
+
}
|
|
239
|
+
if (/HEADLESS_FRONTEND|FRONTEND_URL/i.test(wpConfig)) {
|
|
240
|
+
result.indicators.push("Frontend URL constant found in wp-config.php");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Check for REST API customizations
|
|
245
|
+
const restCustom = execSafe(
|
|
246
|
+
`grep -rl --include="*.php" "register_rest_route" . 2>/dev/null | wc -l`,
|
|
247
|
+
cwd
|
|
248
|
+
);
|
|
249
|
+
if (restCustom && parseInt(restCustom) > 3) {
|
|
250
|
+
result.indicators.push(`${restCustom} files with custom REST routes detected`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Check for headless plugins
|
|
254
|
+
const pluginsDir = path.join(cwd, "wp-content", "plugins");
|
|
255
|
+
const headlessPlugins = [
|
|
256
|
+
{ dir: "faust-wordpress", name: "Faust.js (WP Engine)" },
|
|
257
|
+
{ dir: "atlas-content-modeler", name: "Atlas Content Modeler" },
|
|
258
|
+
{ dir: "wp-gatsby", name: "WP Gatsby" },
|
|
259
|
+
{ dir: "headless-mode", name: "Headless Mode" },
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
for (const plugin of headlessPlugins) {
|
|
263
|
+
if (statSafe(path.join(pluginsDir, plugin.dir))?.isDirectory()) {
|
|
264
|
+
result.indicators.push(`Plugin: ${plugin.name}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
// Main
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
function main() {
|
|
276
|
+
const cwd = parseCwd();
|
|
277
|
+
|
|
278
|
+
if (!statSafe(cwd)?.isDirectory()) {
|
|
279
|
+
console.error(`Error: directory not found: ${cwd}`);
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const graphql = detectWPGraphQL(cwd);
|
|
284
|
+
const cors = detectCORS(cwd);
|
|
285
|
+
const frontend = detectFrontend(cwd);
|
|
286
|
+
const indicators = detectHeadlessIndicators(cwd);
|
|
287
|
+
|
|
288
|
+
const detected = graphql.detected || cors.detected || frontend.detected || indicators.indicators.length > 0;
|
|
289
|
+
|
|
290
|
+
const report = {
|
|
291
|
+
tool: "headless_inspect",
|
|
292
|
+
version: TOOL_VERSION,
|
|
293
|
+
cwd,
|
|
294
|
+
detected,
|
|
295
|
+
graphql,
|
|
296
|
+
cors,
|
|
297
|
+
frontend,
|
|
298
|
+
indicators: indicators.indicators,
|
|
299
|
+
apiLayer: graphql.detected ? "WPGraphQL" : "REST API",
|
|
300
|
+
recommendations: [],
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// Recommendations
|
|
304
|
+
if (frontend.detected && !graphql.detected && !cors.detected) {
|
|
305
|
+
report.recommendations.push("Frontend framework detected but no CORS or GraphQL setup found. Configure CORS headers for cross-origin API access.");
|
|
306
|
+
}
|
|
307
|
+
if (graphql.detected && !cors.detected) {
|
|
308
|
+
report.recommendations.push("WPGraphQL detected but no CORS configuration found. Add CORS headers for frontend access.");
|
|
309
|
+
}
|
|
310
|
+
if (frontend.framework === "Gatsby" && !graphql.detected) {
|
|
311
|
+
report.recommendations.push("Gatsby detected. Install WPGraphQL for optimal integration with gatsby-source-wordpress.");
|
|
312
|
+
}
|
|
313
|
+
if (detected && indicators.indicators.length === 0) {
|
|
314
|
+
report.recommendations.push("Consider adding a webhook system to notify the frontend of content changes.");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
console.log(JSON.stringify(report, null, 2));
|
|
318
|
+
process.exit(detected ? 0 : 1);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
main();
|