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.
Files changed (81) hide show
  1. package/.claude-plugin/plugin.json +7 -3
  2. package/CHANGELOG.md +111 -0
  3. package/README.md +10 -3
  4. package/agents/wp-accessibility-auditor.md +206 -0
  5. package/agents/wp-content-strategist.md +18 -0
  6. package/agents/wp-deployment-engineer.md +34 -2
  7. package/agents/wp-performance-optimizer.md +12 -0
  8. package/agents/wp-security-auditor.md +20 -0
  9. package/agents/wp-security-hardener.md +266 -0
  10. package/agents/wp-site-manager.md +14 -0
  11. package/agents/wp-test-engineer.md +207 -0
  12. package/docs/GUIDE.md +68 -15
  13. package/docs/guides/INDEX.md +46 -0
  14. package/docs/guides/wp-blog.md +590 -0
  15. package/docs/guides/wp-design-system.md +976 -0
  16. package/docs/guides/wp-ecommerce.md +786 -0
  17. package/docs/guides/wp-landing-page.md +762 -0
  18. package/docs/guides/wp-portfolio.md +713 -0
  19. package/docs/plans/2026-02-27-design-system-guide-design.md +30 -0
  20. package/docs/plans/2026-02-27-local-dev-tools-assessment.md +332 -0
  21. package/docs/plans/2026-02-27-local-env-design.md +179 -0
  22. package/docs/plans/2026-02-27-site-type-guides-design.md +44 -0
  23. package/package.json +7 -3
  24. package/skills/wordpress-router/SKILL.md +25 -5
  25. package/skills/wordpress-router/references/decision-tree.md +59 -3
  26. package/skills/wp-accessibility/SKILL.md +170 -0
  27. package/skills/wp-accessibility/references/a11y-audit-tools.md +248 -0
  28. package/skills/wp-accessibility/references/a11y-testing.md +222 -0
  29. package/skills/wp-accessibility/references/block-a11y.md +247 -0
  30. package/skills/wp-accessibility/references/interactive-a11y.md +272 -0
  31. package/skills/wp-accessibility/references/media-a11y.md +254 -0
  32. package/skills/wp-accessibility/references/theme-a11y.md +309 -0
  33. package/skills/wp-audit/SKILL.md +4 -0
  34. package/skills/wp-block-development/SKILL.md +5 -0
  35. package/skills/wp-block-themes/SKILL.md +4 -0
  36. package/skills/wp-deploy/SKILL.md +12 -0
  37. package/skills/wp-e2e-testing/SKILL.md +186 -0
  38. package/skills/wp-e2e-testing/references/ci-integration.md +174 -0
  39. package/skills/wp-e2e-testing/references/jest-wordpress.md +114 -0
  40. package/skills/wp-e2e-testing/references/phpunit-wordpress.md +141 -0
  41. package/skills/wp-e2e-testing/references/playwright-wordpress.md +108 -0
  42. package/skills/wp-e2e-testing/references/test-data-generation.md +127 -0
  43. package/skills/wp-e2e-testing/references/visual-regression.md +107 -0
  44. package/skills/wp-e2e-testing/references/wp-env-setup.md +97 -0
  45. package/skills/wp-e2e-testing/scripts/test_inspect.mjs +375 -0
  46. package/skills/wp-headless/SKILL.md +168 -0
  47. package/skills/wp-headless/references/api-layer-choice.md +160 -0
  48. package/skills/wp-headless/references/cors-config.md +245 -0
  49. package/skills/wp-headless/references/frontend-integration.md +331 -0
  50. package/skills/wp-headless/references/headless-auth.md +286 -0
  51. package/skills/wp-headless/references/webhooks.md +277 -0
  52. package/skills/wp-headless/references/wpgraphql.md +331 -0
  53. package/skills/wp-headless/scripts/headless_inspect.mjs +321 -0
  54. package/skills/wp-i18n/SKILL.md +170 -0
  55. package/skills/wp-i18n/references/js-i18n.md +201 -0
  56. package/skills/wp-i18n/references/multilingual-setup.md +219 -0
  57. package/skills/wp-i18n/references/php-i18n.md +196 -0
  58. package/skills/wp-i18n/references/rtl-support.md +206 -0
  59. package/skills/wp-i18n/references/translation-workflow.md +178 -0
  60. package/skills/wp-i18n/references/wpcli-i18n.md +177 -0
  61. package/skills/wp-i18n/scripts/i18n_inspect.mjs +330 -0
  62. package/skills/wp-interactivity-api/SKILL.md +4 -0
  63. package/skills/wp-local-env/SKILL.md +233 -0
  64. package/skills/wp-local-env/references/localwp-adapter.md +156 -0
  65. package/skills/wp-local-env/references/mcp-adapter-setup.md +153 -0
  66. package/skills/wp-local-env/references/studio-adapter.md +127 -0
  67. package/skills/wp-local-env/references/wpenv-adapter.md +121 -0
  68. package/skills/wp-local-env/scripts/detect_local_env.mjs +404 -0
  69. package/skills/wp-playground/SKILL.md +13 -1
  70. package/skills/wp-plugin-development/SKILL.md +6 -0
  71. package/skills/wp-rest-api/SKILL.md +4 -0
  72. package/skills/wp-security/SKILL.md +179 -0
  73. package/skills/wp-security/references/api-restriction.md +147 -0
  74. package/skills/wp-security/references/authentication-hardening.md +105 -0
  75. package/skills/wp-security/references/filesystem-hardening.md +105 -0
  76. package/skills/wp-security/references/http-headers.md +105 -0
  77. package/skills/wp-security/references/incident-response.md +144 -0
  78. package/skills/wp-security/references/user-capabilities.md +115 -0
  79. package/skills/wp-security/references/wp-config-security.md +129 -0
  80. package/skills/wp-security/scripts/security_inspect.mjs +393 -0
  81. 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
+ ```