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,245 @@
|
|
|
1
|
+
# CORS Configuration
|
|
2
|
+
|
|
3
|
+
Use this file when configuring Cross-Origin Resource Sharing for headless WordPress.
|
|
4
|
+
|
|
5
|
+
## Why CORS matters for headless
|
|
6
|
+
|
|
7
|
+
In a headless setup, the frontend (e.g., `app.example.com`) and WordPress backend (`wp.example.com`) are on different origins. Browsers block cross-origin requests unless the server explicitly allows them via CORS headers.
|
|
8
|
+
|
|
9
|
+
## WordPress CORS via PHP
|
|
10
|
+
|
|
11
|
+
### Allow specific origin (recommended)
|
|
12
|
+
|
|
13
|
+
```php
|
|
14
|
+
add_action('init', function() {
|
|
15
|
+
$allowed_origins = [
|
|
16
|
+
'https://app.example.com',
|
|
17
|
+
'https://staging.example.com',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
|
21
|
+
|
|
22
|
+
if (in_array($origin, $allowed_origins, true)) {
|
|
23
|
+
header("Access-Control-Allow-Origin: $origin");
|
|
24
|
+
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
|
25
|
+
header('Access-Control-Allow-Headers: Authorization, Content-Type, X-WP-Nonce');
|
|
26
|
+
header('Access-Control-Allow-Credentials: true');
|
|
27
|
+
header('Access-Control-Max-Age: 86400');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Handle preflight
|
|
31
|
+
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|
32
|
+
status_header(204);
|
|
33
|
+
exit;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### REST API specific filter
|
|
39
|
+
|
|
40
|
+
```php
|
|
41
|
+
add_action('rest_api_init', function() {
|
|
42
|
+
remove_filter('rest_pre_serve_request', 'rest_send_cors_headers');
|
|
43
|
+
|
|
44
|
+
add_filter('rest_pre_serve_request', function($value) {
|
|
45
|
+
$allowed_origins = [
|
|
46
|
+
'https://app.example.com',
|
|
47
|
+
'https://staging.example.com',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
$origin = get_http_origin();
|
|
51
|
+
|
|
52
|
+
if ($origin && in_array($origin, $allowed_origins, true)) {
|
|
53
|
+
header("Access-Control-Allow-Origin: $origin");
|
|
54
|
+
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
|
55
|
+
header('Access-Control-Allow-Headers: Authorization, Content-Type, X-WP-Nonce');
|
|
56
|
+
header('Access-Control-Allow-Credentials: true');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return $value;
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### WPGraphQL CORS
|
|
65
|
+
|
|
66
|
+
```php
|
|
67
|
+
add_action('graphql_response_headers_to_send', function($headers) {
|
|
68
|
+
$allowed_origins = [
|
|
69
|
+
'https://app.example.com',
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
|
73
|
+
|
|
74
|
+
if (in_array($origin, $allowed_origins, true)) {
|
|
75
|
+
$headers['Access-Control-Allow-Origin'] = $origin;
|
|
76
|
+
$headers['Access-Control-Allow-Headers'] = 'Authorization, Content-Type';
|
|
77
|
+
$headers['Access-Control-Allow-Credentials'] = 'true';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return $headers;
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Server-level CORS
|
|
85
|
+
|
|
86
|
+
### Nginx
|
|
87
|
+
|
|
88
|
+
```nginx
|
|
89
|
+
# /etc/nginx/conf.d/cors.conf or within server block
|
|
90
|
+
map $http_origin $cors_origin {
|
|
91
|
+
default "";
|
|
92
|
+
"https://app.example.com" "https://app.example.com";
|
|
93
|
+
"https://staging.example.com" "https://staging.example.com";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
server {
|
|
97
|
+
# ... existing config
|
|
98
|
+
|
|
99
|
+
location /wp-json/ {
|
|
100
|
+
if ($cors_origin != "") {
|
|
101
|
+
add_header Access-Control-Allow-Origin $cors_origin always;
|
|
102
|
+
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
|
|
103
|
+
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-WP-Nonce" always;
|
|
104
|
+
add_header Access-Control-Allow-Credentials "true" always;
|
|
105
|
+
add_header Access-Control-Max-Age 86400 always;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if ($request_method = OPTIONS) {
|
|
109
|
+
return 204;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# ... proxy or fastcgi config
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
location /graphql {
|
|
116
|
+
# Same CORS headers as above
|
|
117
|
+
if ($cors_origin != "") {
|
|
118
|
+
add_header Access-Control-Allow-Origin $cors_origin always;
|
|
119
|
+
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
|
|
120
|
+
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
|
|
121
|
+
add_header Access-Control-Allow-Credentials "true" always;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if ($request_method = OPTIONS) {
|
|
125
|
+
return 204;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# ... proxy or fastcgi config
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Apache (.htaccess)
|
|
134
|
+
|
|
135
|
+
```apache
|
|
136
|
+
<IfModule mod_headers.c>
|
|
137
|
+
SetEnvIf Origin "^https://(app|staging)\.example\.com$" CORS_ORIGIN=$0
|
|
138
|
+
Header always set Access-Control-Allow-Origin %{CORS_ORIGIN}e env=CORS_ORIGIN
|
|
139
|
+
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
|
140
|
+
Header always set Access-Control-Allow-Headers "Authorization, Content-Type, X-WP-Nonce"
|
|
141
|
+
Header always set Access-Control-Allow-Credentials "true"
|
|
142
|
+
Header always set Access-Control-Max-Age "86400"
|
|
143
|
+
</IfModule>
|
|
144
|
+
|
|
145
|
+
# Handle preflight
|
|
146
|
+
RewriteEngine On
|
|
147
|
+
RewriteCond %{REQUEST_METHOD} OPTIONS
|
|
148
|
+
RewriteRule ^(.*)$ $1 [R=204,L]
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## CORS headers explained
|
|
152
|
+
|
|
153
|
+
| Header | Purpose |
|
|
154
|
+
|--------|---------|
|
|
155
|
+
| `Access-Control-Allow-Origin` | Which origins can access the resource |
|
|
156
|
+
| `Access-Control-Allow-Methods` | Which HTTP methods are allowed |
|
|
157
|
+
| `Access-Control-Allow-Headers` | Which request headers are allowed |
|
|
158
|
+
| `Access-Control-Allow-Credentials` | Whether cookies/auth can be sent |
|
|
159
|
+
| `Access-Control-Max-Age` | How long preflight results can be cached (seconds) |
|
|
160
|
+
| `Access-Control-Expose-Headers` | Which response headers the client can read |
|
|
161
|
+
|
|
162
|
+
## Common issues
|
|
163
|
+
|
|
164
|
+
### "No 'Access-Control-Allow-Origin' header"
|
|
165
|
+
|
|
166
|
+
The server doesn't include the CORS header. Fix: add the headers as shown above.
|
|
167
|
+
|
|
168
|
+
### "The value of 'Access-Control-Allow-Origin' is not equal to the supplied origin"
|
|
169
|
+
|
|
170
|
+
Origin mismatch. Check:
|
|
171
|
+
- Trailing slashes (`https://app.example.com` vs `https://app.example.com/`)
|
|
172
|
+
- Protocol (`http` vs `https`)
|
|
173
|
+
- Port numbers (`localhost:3000` vs `localhost`)
|
|
174
|
+
|
|
175
|
+
### "Credentials flag is true but Access-Control-Allow-Origin is '*'"
|
|
176
|
+
|
|
177
|
+
Cannot use `*` with `credentials: true`. Must specify the exact origin:
|
|
178
|
+
```
|
|
179
|
+
Access-Control-Allow-Origin: https://app.example.com ✓
|
|
180
|
+
Access-Control-Allow-Origin: * ✗ (with credentials)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Preflight (OPTIONS) returns 401/403
|
|
184
|
+
|
|
185
|
+
WordPress or a security plugin is blocking OPTIONS requests. Fix:
|
|
186
|
+
```php
|
|
187
|
+
add_action('init', function() {
|
|
188
|
+
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|
189
|
+
// Send CORS headers and exit before WordPress auth runs
|
|
190
|
+
header('Access-Control-Allow-Origin: https://app.example.com');
|
|
191
|
+
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
|
192
|
+
header('Access-Control-Allow-Headers: Authorization, Content-Type');
|
|
193
|
+
header('Access-Control-Allow-Credentials: true');
|
|
194
|
+
status_header(204);
|
|
195
|
+
exit;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Development CORS
|
|
201
|
+
|
|
202
|
+
```php
|
|
203
|
+
// ONLY for local development — never in production
|
|
204
|
+
if (defined('WP_DEBUG') && WP_DEBUG) {
|
|
205
|
+
add_action('init', function() {
|
|
206
|
+
header('Access-Control-Allow-Origin: http://localhost:3000');
|
|
207
|
+
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
|
208
|
+
header('Access-Control-Allow-Headers: Authorization, Content-Type');
|
|
209
|
+
header('Access-Control-Allow-Credentials: true');
|
|
210
|
+
|
|
211
|
+
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|
212
|
+
status_header(204);
|
|
213
|
+
exit;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Security rules
|
|
220
|
+
|
|
221
|
+
1. **Never use `*` in production** — always whitelist specific origins
|
|
222
|
+
2. **Never reflect the Origin header blindly** — validate against a whitelist
|
|
223
|
+
3. **Use `Credentials: true` only when needed** — for authenticated requests
|
|
224
|
+
4. **Set `Max-Age` for caching** — reduces preflight requests (86400 = 24 hours)
|
|
225
|
+
5. **Expose only needed headers** — minimize `Access-Control-Expose-Headers`
|
|
226
|
+
|
|
227
|
+
## Verification
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
# Test CORS headers
|
|
231
|
+
curl -I -X OPTIONS https://wp.example.com/wp-json/wp/v2/posts \
|
|
232
|
+
-H "Origin: https://app.example.com" \
|
|
233
|
+
-H "Access-Control-Request-Method: GET" \
|
|
234
|
+
-H "Access-Control-Request-Headers: Authorization"
|
|
235
|
+
|
|
236
|
+
# Check response headers include:
|
|
237
|
+
# Access-Control-Allow-Origin: https://app.example.com
|
|
238
|
+
# Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
|
|
239
|
+
# Access-Control-Allow-Headers: Authorization, Content-Type
|
|
240
|
+
|
|
241
|
+
# Test from frontend
|
|
242
|
+
fetch('https://wp.example.com/wp-json/wp/v2/posts', {
|
|
243
|
+
credentials: 'include',
|
|
244
|
+
}).then(r => console.log('CORS OK:', r.ok));
|
|
245
|
+
```
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# Frontend Integration
|
|
2
|
+
|
|
3
|
+
Use this file when connecting a JavaScript frontend framework to a headless WordPress backend.
|
|
4
|
+
|
|
5
|
+
## Next.js integration
|
|
6
|
+
|
|
7
|
+
### Data fetching (App Router)
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
// lib/wordpress.js
|
|
11
|
+
const WP_URL = process.env.NEXT_PUBLIC_WP_URL || 'https://wp.example.com';
|
|
12
|
+
|
|
13
|
+
export async function getPosts(page = 1, perPage = 10) {
|
|
14
|
+
const res = await fetch(
|
|
15
|
+
`${WP_URL}/wp-json/wp/v2/posts?page=${page}&per_page=${perPage}&_embed`,
|
|
16
|
+
{ next: { revalidate: 60 } } // ISR: revalidate every 60 seconds
|
|
17
|
+
);
|
|
18
|
+
if (!res.ok) throw new Error('Failed to fetch posts');
|
|
19
|
+
return {
|
|
20
|
+
posts: await res.json(),
|
|
21
|
+
totalPages: Number(res.headers.get('X-WP-TotalPages')),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function getPost(slug) {
|
|
26
|
+
const res = await fetch(
|
|
27
|
+
`${WP_URL}/wp-json/wp/v2/posts?slug=${slug}&_embed`,
|
|
28
|
+
{ next: { revalidate: 60 } }
|
|
29
|
+
);
|
|
30
|
+
const posts = await res.json();
|
|
31
|
+
return posts[0] || null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function getPage(slug) {
|
|
35
|
+
const res = await fetch(
|
|
36
|
+
`${WP_URL}/wp-json/wp/v2/pages?slug=${slug}&_embed`,
|
|
37
|
+
{ next: { revalidate: 300 } }
|
|
38
|
+
);
|
|
39
|
+
const pages = await res.json();
|
|
40
|
+
return pages[0] || null;
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Page components
|
|
45
|
+
|
|
46
|
+
```jsx
|
|
47
|
+
// app/blog/page.js
|
|
48
|
+
import { getPosts } from '@/lib/wordpress';
|
|
49
|
+
|
|
50
|
+
export default async function BlogPage() {
|
|
51
|
+
const { posts } = await getPosts();
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<main>
|
|
55
|
+
<h1>Blog</h1>
|
|
56
|
+
{posts.map((post) => (
|
|
57
|
+
<article key={post.id}>
|
|
58
|
+
<h2>
|
|
59
|
+
<a href={`/blog/${post.slug}`}>{post.title.rendered}</a>
|
|
60
|
+
</h2>
|
|
61
|
+
<div dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} />
|
|
62
|
+
</article>
|
|
63
|
+
))}
|
|
64
|
+
</main>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// app/blog/[slug]/page.js
|
|
69
|
+
import { getPost, getPosts } from '@/lib/wordpress';
|
|
70
|
+
import { notFound } from 'next/navigation';
|
|
71
|
+
|
|
72
|
+
export async function generateStaticParams() {
|
|
73
|
+
const { posts } = await getPosts(1, 100);
|
|
74
|
+
return posts.map((post) => ({ slug: post.slug }));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default async function PostPage({ params }) {
|
|
78
|
+
const post = await getPost(params.slug);
|
|
79
|
+
if (!post) notFound();
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<article>
|
|
83
|
+
<h1>{post.title.rendered}</h1>
|
|
84
|
+
<div dangerouslySetInnerHTML={{ __html: post.content.rendered }} />
|
|
85
|
+
</article>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### ISR (Incremental Static Regeneration)
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
// Revalidate on demand via webhook (see webhooks.md)
|
|
94
|
+
// app/api/revalidate/route.js
|
|
95
|
+
import { revalidatePath } from 'next/cache';
|
|
96
|
+
|
|
97
|
+
export async function POST(request) {
|
|
98
|
+
const secret = request.headers.get('x-revalidate-secret');
|
|
99
|
+
if (secret !== process.env.REVALIDATE_SECRET) {
|
|
100
|
+
return Response.json({ error: 'Invalid secret' }, { status: 401 });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const { path } = await request.json();
|
|
104
|
+
revalidatePath(path);
|
|
105
|
+
return Response.json({ revalidated: true });
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Nuxt 3 integration
|
|
110
|
+
|
|
111
|
+
### Composable
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
// composables/useWordPress.js
|
|
115
|
+
export function useWordPress() {
|
|
116
|
+
const config = useRuntimeConfig();
|
|
117
|
+
const wpUrl = config.public.wpUrl;
|
|
118
|
+
|
|
119
|
+
async function getPosts(page = 1) {
|
|
120
|
+
return useFetch(`${wpUrl}/wp-json/wp/v2/posts`, {
|
|
121
|
+
params: { page, per_page: 10, _embed: true },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function getPost(slug) {
|
|
126
|
+
const { data } = await useFetch(`${wpUrl}/wp-json/wp/v2/posts`, {
|
|
127
|
+
params: { slug, _embed: true },
|
|
128
|
+
});
|
|
129
|
+
return data.value?.[0] || null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { getPosts, getPost };
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### nuxt.config.ts
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
export default defineNuxtConfig({
|
|
140
|
+
runtimeConfig: {
|
|
141
|
+
public: {
|
|
142
|
+
wpUrl: process.env.WP_URL || 'https://wp.example.com',
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
routeRules: {
|
|
146
|
+
'/blog/**': { isr: 60 }, // Revalidate every 60s
|
|
147
|
+
'/': { isr: 300 },
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Astro integration
|
|
153
|
+
|
|
154
|
+
### Data fetching
|
|
155
|
+
|
|
156
|
+
```astro
|
|
157
|
+
---
|
|
158
|
+
// src/pages/blog/[slug].astro
|
|
159
|
+
import Layout from '@/layouts/Layout.astro';
|
|
160
|
+
|
|
161
|
+
export async function getStaticPaths() {
|
|
162
|
+
const res = await fetch(`${import.meta.env.WP_URL}/wp-json/wp/v2/posts?per_page=100`);
|
|
163
|
+
const posts = await res.json();
|
|
164
|
+
|
|
165
|
+
return posts.map((post) => ({
|
|
166
|
+
params: { slug: post.slug },
|
|
167
|
+
props: { post },
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const { post } = Astro.props;
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
<Layout title={post.title.rendered}>
|
|
175
|
+
<article>
|
|
176
|
+
<h1 set:html={post.title.rendered} />
|
|
177
|
+
<div set:html={post.content.rendered} />
|
|
178
|
+
</article>
|
|
179
|
+
</Layout>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## WPGraphQL with frontend frameworks
|
|
183
|
+
|
|
184
|
+
### Apollo Client (React/Next.js)
|
|
185
|
+
|
|
186
|
+
```js
|
|
187
|
+
// lib/apollo.js
|
|
188
|
+
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
|
|
189
|
+
|
|
190
|
+
const client = new ApolloClient({
|
|
191
|
+
link: new HttpLink({
|
|
192
|
+
uri: `${process.env.NEXT_PUBLIC_WP_URL}/graphql`,
|
|
193
|
+
}),
|
|
194
|
+
cache: new InMemoryCache(),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
export default client;
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### urql (lightweight alternative)
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
import { Client, cacheExchange, fetchExchange } from 'urql';
|
|
204
|
+
|
|
205
|
+
const client = new Client({
|
|
206
|
+
url: `${process.env.NEXT_PUBLIC_WP_URL}/graphql`,
|
|
207
|
+
exchanges: [cacheExchange, fetchExchange],
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Rendering WordPress content
|
|
212
|
+
|
|
213
|
+
### Sanitizing HTML content
|
|
214
|
+
|
|
215
|
+
```jsx
|
|
216
|
+
// React: dangerouslySetInnerHTML (ensure source is trusted)
|
|
217
|
+
<div dangerouslySetInnerHTML={{ __html: post.content.rendered }} />
|
|
218
|
+
|
|
219
|
+
// With DOMPurify for extra safety
|
|
220
|
+
import DOMPurify from 'isomorphic-dompurify';
|
|
221
|
+
|
|
222
|
+
function WPContent({ html }) {
|
|
223
|
+
const clean = DOMPurify.sanitize(html, {
|
|
224
|
+
ADD_TAGS: ['iframe'],
|
|
225
|
+
ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling'],
|
|
226
|
+
});
|
|
227
|
+
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Handling WordPress images
|
|
232
|
+
|
|
233
|
+
```jsx
|
|
234
|
+
// Replace WordPress image URLs with optimized versions
|
|
235
|
+
function optimizeImages(html, wpUrl) {
|
|
236
|
+
// Replace with Next.js Image optimization
|
|
237
|
+
return html.replace(
|
|
238
|
+
/src="([^"]*wp-content\/uploads\/[^"]*)"/g,
|
|
239
|
+
(match, url) => `src="/_next/image?url=${encodeURIComponent(url)}&w=1200&q=80"`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### WordPress blocks in React
|
|
245
|
+
|
|
246
|
+
```jsx
|
|
247
|
+
// Parse block content into React components
|
|
248
|
+
function renderBlock(block) {
|
|
249
|
+
switch (block.blockName) {
|
|
250
|
+
case 'core/paragraph':
|
|
251
|
+
return <p dangerouslySetInnerHTML={{ __html: block.innerHTML }} />;
|
|
252
|
+
case 'core/heading':
|
|
253
|
+
return <h2 dangerouslySetInnerHTML={{ __html: block.innerHTML }} />;
|
|
254
|
+
case 'core/image':
|
|
255
|
+
return <figure dangerouslySetInnerHTML={{ __html: block.innerHTML }} />;
|
|
256
|
+
default:
|
|
257
|
+
return <div dangerouslySetInnerHTML={{ __html: block.innerHTML }} />;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## SEO in headless WordPress
|
|
263
|
+
|
|
264
|
+
### Yoast SEO integration
|
|
265
|
+
|
|
266
|
+
```js
|
|
267
|
+
// Fetch Yoast data from REST API
|
|
268
|
+
// Requires Yoast SEO plugin (adds yoast_head to REST responses)
|
|
269
|
+
export async function getPostSEO(slug) {
|
|
270
|
+
const res = await fetch(
|
|
271
|
+
`${WP_URL}/wp-json/wp/v2/posts?slug=${slug}&_fields=yoast_head_json`
|
|
272
|
+
);
|
|
273
|
+
const posts = await res.json();
|
|
274
|
+
return posts[0]?.yoast_head_json || {};
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
```jsx
|
|
279
|
+
// app/blog/[slug]/page.js
|
|
280
|
+
export async function generateMetadata({ params }) {
|
|
281
|
+
const seo = await getPostSEO(params.slug);
|
|
282
|
+
return {
|
|
283
|
+
title: seo.title,
|
|
284
|
+
description: seo.description,
|
|
285
|
+
openGraph: {
|
|
286
|
+
title: seo.og_title,
|
|
287
|
+
description: seo.og_description,
|
|
288
|
+
images: seo.og_image ? [{ url: seo.og_image[0].url }] : [],
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Preview mode
|
|
295
|
+
|
|
296
|
+
```js
|
|
297
|
+
// app/api/preview/route.js (Next.js)
|
|
298
|
+
import { draftMode } from 'next/headers';
|
|
299
|
+
import { redirect } from 'next/navigation';
|
|
300
|
+
|
|
301
|
+
export async function GET(request) {
|
|
302
|
+
const { searchParams } = new URL(request.url);
|
|
303
|
+
const secret = searchParams.get('secret');
|
|
304
|
+
const slug = searchParams.get('slug');
|
|
305
|
+
|
|
306
|
+
if (secret !== process.env.WP_PREVIEW_SECRET) {
|
|
307
|
+
return Response.json({ error: 'Invalid secret' }, { status: 401 });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
draftMode().enable();
|
|
311
|
+
redirect(`/blog/${slug}`);
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Verification
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
# Test REST API from frontend origin
|
|
319
|
+
curl -H "Origin: https://app.example.com" \
|
|
320
|
+
https://wp.example.com/wp-json/wp/v2/posts?per_page=1
|
|
321
|
+
|
|
322
|
+
# Test _embed returns featured images
|
|
323
|
+
curl -s "https://wp.example.com/wp-json/wp/v2/posts?_embed&per_page=1" | \
|
|
324
|
+
jq '.[0]._embedded["wp:featuredmedia"][0].source_url'
|
|
325
|
+
|
|
326
|
+
# Test ISR revalidation
|
|
327
|
+
curl -X POST https://app.example.com/api/revalidate \
|
|
328
|
+
-H "Content-Type: application/json" \
|
|
329
|
+
-H "x-revalidate-secret: YOUR_SECRET" \
|
|
330
|
+
-d '{"path": "/blog/hello-world"}'
|
|
331
|
+
```
|