claude-plugin-wordpress-manager 1.4.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +7 -3
- package/CHANGELOG.md +111 -0
- package/README.md +10 -3
- 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/GUIDE.md +68 -15
- 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-local-dev-tools-assessment.md +332 -0
- package/docs/plans/2026-02-27-local-env-design.md +179 -0
- package/docs/plans/2026-02-27-site-type-guides-design.md +44 -0
- package/package.json +7 -3
- package/skills/wordpress-router/SKILL.md +25 -5
- package/skills/wordpress-router/references/decision-tree.md +59 -3
- 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-deploy/SKILL.md +12 -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-local-env/SKILL.md +233 -0
- package/skills/wp-local-env/references/localwp-adapter.md +156 -0
- package/skills/wp-local-env/references/mcp-adapter-setup.md +153 -0
- package/skills/wp-local-env/references/studio-adapter.md +127 -0
- package/skills/wp-local-env/references/wpenv-adapter.md +121 -0
- package/skills/wp-local-env/scripts/detect_local_env.mjs +404 -0
- package/skills/wp-playground/SKILL.md +13 -1
- 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
- package/skills/wp-wpcli-and-ops/SKILL.md +6 -0
|
@@ -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
|
+
```
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# Headless Authentication
|
|
2
|
+
|
|
3
|
+
Use this file when implementing authentication for decoupled WordPress frontends.
|
|
4
|
+
|
|
5
|
+
## Authentication methods comparison
|
|
6
|
+
|
|
7
|
+
| Method | Use case | Security | Complexity |
|
|
8
|
+
|--------|----------|----------|-----------|
|
|
9
|
+
| Application Passwords | Server-to-server, CI/CD | Good | Low |
|
|
10
|
+
| JWT (plugin) | SPA authentication | Good | Medium |
|
|
11
|
+
| Cookie-based (same domain) | Subdomain frontends | Good | Low |
|
|
12
|
+
| OAuth 2.0 | Third-party apps | Best | High |
|
|
13
|
+
| Nonce + Cookie | Same-origin AJAX | Good | Low (but not for headless) |
|
|
14
|
+
|
|
15
|
+
## Application Passwords (WordPress 5.6+)
|
|
16
|
+
|
|
17
|
+
Best for: server-side requests, build-time data fetching, CI/CD pipelines.
|
|
18
|
+
|
|
19
|
+
### Setup
|
|
20
|
+
|
|
21
|
+
Users → Profile → Application Passwords → Add New
|
|
22
|
+
|
|
23
|
+
### Usage
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Basic Auth with application password
|
|
27
|
+
curl -u "username:xxxx xxxx xxxx xxxx xxxx xxxx" \
|
|
28
|
+
https://site.com/wp-json/wp/v2/posts
|
|
29
|
+
|
|
30
|
+
# Base64 encoded
|
|
31
|
+
curl -H "Authorization: Basic $(echo -n 'username:xxxx xxxx xxxx xxxx' | base64)" \
|
|
32
|
+
https://site.com/wp-json/wp/v2/posts
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### In Next.js (server-side)
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
// lib/wordpress.js
|
|
39
|
+
const WP_URL = process.env.WORDPRESS_URL;
|
|
40
|
+
const WP_AUTH = Buffer.from(
|
|
41
|
+
`${process.env.WP_USER}:${process.env.WP_APP_PASSWORD}`
|
|
42
|
+
).toString('base64');
|
|
43
|
+
|
|
44
|
+
export async function fetchWP(endpoint, options = {}) {
|
|
45
|
+
const res = await fetch(`${WP_URL}/wp-json/wp/v2/${endpoint}`, {
|
|
46
|
+
...options,
|
|
47
|
+
headers: {
|
|
48
|
+
'Authorization': `Basic ${WP_AUTH}`,
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
...options.headers,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
if (!res.ok) throw new Error(`WP API error: ${res.status}`);
|
|
54
|
+
return res.json();
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Restrict application passwords
|
|
59
|
+
|
|
60
|
+
```php
|
|
61
|
+
// Only allow application passwords for specific roles
|
|
62
|
+
add_filter('wp_is_application_passwords_available_for_user', function($available, $user) {
|
|
63
|
+
return in_array('administrator', $user->roles);
|
|
64
|
+
}, 10, 2);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## JWT Authentication
|
|
68
|
+
|
|
69
|
+
Best for: SPA (React/Vue) with user login, client-side authenticated requests.
|
|
70
|
+
|
|
71
|
+
### Plugin setup
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
wp plugin install jwt-authentication-for-wp-rest-api --activate
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Add to `wp-config.php`:
|
|
78
|
+
```php
|
|
79
|
+
define('JWT_AUTH_SECRET_KEY', 'your-secret-key-at-least-32-chars-long');
|
|
80
|
+
define('JWT_AUTH_CORS_ENABLE', true);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Add to `.htaccess`:
|
|
84
|
+
```apache
|
|
85
|
+
RewriteEngine On
|
|
86
|
+
RewriteCond %{HTTP:Authorization} ^(.*)
|
|
87
|
+
RewriteRule .* - [E=HTTP_AUTHORIZATION:%1]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Token flow
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
// 1. Get token
|
|
94
|
+
const loginRes = await fetch('https://site.com/wp-json/jwt-auth/v1/token', {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: { 'Content-Type': 'application/json' },
|
|
97
|
+
body: JSON.stringify({
|
|
98
|
+
username: 'user@example.com',
|
|
99
|
+
password: 'password',
|
|
100
|
+
}),
|
|
101
|
+
});
|
|
102
|
+
const { token, user_display_name } = await loginRes.json();
|
|
103
|
+
|
|
104
|
+
// 2. Use token for authenticated requests
|
|
105
|
+
const postsRes = await fetch('https://site.com/wp-json/wp/v2/posts', {
|
|
106
|
+
headers: {
|
|
107
|
+
'Authorization': `Bearer ${token}`,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// 3. Validate token (optional)
|
|
112
|
+
const validateRes = await fetch('https://site.com/wp-json/jwt-auth/v1/token/validate', {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers: {
|
|
115
|
+
'Authorization': `Bearer ${token}`,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Token storage (frontend)
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
// Store in httpOnly cookie (recommended for SSR frameworks)
|
|
124
|
+
// Set via API route, not client-side JavaScript
|
|
125
|
+
|
|
126
|
+
// For SPAs: store in memory (most secure for client-side)
|
|
127
|
+
let authToken = null;
|
|
128
|
+
|
|
129
|
+
function setToken(token) {
|
|
130
|
+
authToken = token; // Lost on page refresh — that's OK for security
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getToken() {
|
|
134
|
+
return authToken;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// AVOID: localStorage (XSS vulnerable)
|
|
138
|
+
// localStorage.setItem('token', token); // DON'T DO THIS
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Refresh token pattern
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
class AuthManager {
|
|
145
|
+
constructor() {
|
|
146
|
+
this.token = null;
|
|
147
|
+
this.refreshToken = null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async login(username, password) {
|
|
151
|
+
const res = await fetch('/wp-json/jwt-auth/v1/token', {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: { 'Content-Type': 'application/json' },
|
|
154
|
+
body: JSON.stringify({ username, password }),
|
|
155
|
+
});
|
|
156
|
+
const data = await res.json();
|
|
157
|
+
this.token = data.token;
|
|
158
|
+
return data;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async authenticatedFetch(url, options = {}) {
|
|
162
|
+
let res = await fetch(url, {
|
|
163
|
+
...options,
|
|
164
|
+
headers: {
|
|
165
|
+
...options.headers,
|
|
166
|
+
'Authorization': `Bearer ${this.token}`,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Token expired — re-authenticate
|
|
171
|
+
if (res.status === 403) {
|
|
172
|
+
// Redirect to login or use refresh token
|
|
173
|
+
throw new Error('Session expired. Please log in again.');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return res;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## WPGraphQL authentication
|
|
182
|
+
|
|
183
|
+
```graphql
|
|
184
|
+
# Login mutation (with wp-graphql-jwt-authentication plugin)
|
|
185
|
+
mutation Login($username: String!, $password: String!) {
|
|
186
|
+
login(input: { username: $username, password: $password }) {
|
|
187
|
+
authToken
|
|
188
|
+
refreshToken
|
|
189
|
+
user {
|
|
190
|
+
id
|
|
191
|
+
name
|
|
192
|
+
email
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# Refresh token
|
|
198
|
+
mutation RefreshToken($token: String!) {
|
|
199
|
+
refreshJwtAuthToken(input: { jwtRefreshToken: $token }) {
|
|
200
|
+
authToken
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Next.js authentication pattern
|
|
206
|
+
|
|
207
|
+
### API route for login
|
|
208
|
+
|
|
209
|
+
```js
|
|
210
|
+
// pages/api/login.js (Next.js)
|
|
211
|
+
export default async function handler(req, res) {
|
|
212
|
+
if (req.method !== 'POST') return res.status(405).end();
|
|
213
|
+
|
|
214
|
+
const { username, password } = req.body;
|
|
215
|
+
|
|
216
|
+
const wpRes = await fetch(`${process.env.WP_URL}/wp-json/jwt-auth/v1/token`, {
|
|
217
|
+
method: 'POST',
|
|
218
|
+
headers: { 'Content-Type': 'application/json' },
|
|
219
|
+
body: JSON.stringify({ username, password }),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (!wpRes.ok) {
|
|
223
|
+
return res.status(401).json({ error: 'Invalid credentials' });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const { token } = await wpRes.json();
|
|
227
|
+
|
|
228
|
+
// Set httpOnly cookie (not accessible via JavaScript)
|
|
229
|
+
res.setHeader('Set-Cookie', [
|
|
230
|
+
`wp_token=${token}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=86400`,
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
return res.status(200).json({ success: true });
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Middleware for protected routes
|
|
238
|
+
|
|
239
|
+
```js
|
|
240
|
+
// middleware.js (Next.js)
|
|
241
|
+
import { NextResponse } from 'next/server';
|
|
242
|
+
|
|
243
|
+
export function middleware(request) {
|
|
244
|
+
const token = request.cookies.get('wp_token')?.value;
|
|
245
|
+
|
|
246
|
+
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
|
|
247
|
+
return NextResponse.redirect(new URL('/login', request.url));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return NextResponse.next();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export const config = {
|
|
254
|
+
matcher: ['/dashboard/:path*', '/profile/:path*'],
|
|
255
|
+
};
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Security checklist
|
|
259
|
+
|
|
260
|
+
- [ ] Never store JWT in localStorage (XSS risk)
|
|
261
|
+
- [ ] Use httpOnly cookies for token storage when possible
|
|
262
|
+
- [ ] Set short token expiry (15 min) with refresh tokens
|
|
263
|
+
- [ ] Use HTTPS for all API requests
|
|
264
|
+
- [ ] Validate tokens server-side on every request
|
|
265
|
+
- [ ] Implement CORS properly (see `cors-config.md`)
|
|
266
|
+
- [ ] Rate-limit login endpoints
|
|
267
|
+
- [ ] Use strong JWT secret key (32+ characters)
|
|
268
|
+
- [ ] Disable XML-RPC if not needed
|
|
269
|
+
|
|
270
|
+
## Verification
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
# Test application password
|
|
274
|
+
curl -u "admin:xxxx xxxx xxxx xxxx" https://site.com/wp-json/wp/v2/users/me
|
|
275
|
+
|
|
276
|
+
# Test JWT token
|
|
277
|
+
TOKEN=$(curl -s -X POST https://site.com/wp-json/jwt-auth/v1/token \
|
|
278
|
+
-H "Content-Type: application/json" \
|
|
279
|
+
-d '{"username":"admin","password":"password"}' | jq -r '.token')
|
|
280
|
+
|
|
281
|
+
curl -H "Authorization: Bearer $TOKEN" https://site.com/wp-json/wp/v2/users/me
|
|
282
|
+
|
|
283
|
+
# Verify unauthenticated access is blocked
|
|
284
|
+
curl -s -o /dev/null -w "%{http_code}" https://site.com/wp-json/wp/v2/users
|
|
285
|
+
# Should return 401 if REST API is restricted
|
|
286
|
+
```
|