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,168 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wp-headless
|
|
3
|
+
description: "Use when building headless/decoupled WordPress architectures: choosing between REST API and WPGraphQL, headless authentication (JWT, application passwords, NextAuth), CORS configuration, frontend framework integration (Next.js, Nuxt, Astro), content webhooks, and ISR/SSG strategies."
|
|
4
|
+
compatibility: "Targets WordPress 6.9+ (PHP 7.2.24+). Filesystem-based agent with bash + node."
|
|
5
|
+
version: 1.0.0
|
|
6
|
+
source: "vinmor/wordpress-manager"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# WP Headless
|
|
10
|
+
|
|
11
|
+
## When to use
|
|
12
|
+
|
|
13
|
+
Use this skill when building or maintaining a decoupled/headless WordPress architecture:
|
|
14
|
+
|
|
15
|
+
- Building a decoupled site with WordPress as the CMS and a separate frontend
|
|
16
|
+
- Choosing between REST API and WPGraphQL for data fetching
|
|
17
|
+
- Configuring WordPress as a headless CMS backend
|
|
18
|
+
- Integrating with Next.js, Nuxt, or Astro frontends
|
|
19
|
+
- Setting up headless authentication (JWT, application passwords, NextAuth/Auth.js)
|
|
20
|
+
- Configuring CORS for cross-origin API access
|
|
21
|
+
- Implementing content webhooks for on-demand revalidation (ISR)
|
|
22
|
+
- Planning SSG, SSR, or ISR rendering strategies
|
|
23
|
+
|
|
24
|
+
## Inputs required
|
|
25
|
+
|
|
26
|
+
- **WordPress site**: URL, admin access, hosting type
|
|
27
|
+
- **Frontend framework**: Next.js, Nuxt, Astro, or other
|
|
28
|
+
- **Authentication requirements**: public content only vs authenticated features
|
|
29
|
+
- **Hosting for frontend**: Vercel, Netlify, self-hosted, or other
|
|
30
|
+
- **Deployment strategy**: SSG (static), SSR (server), ISR (incremental), or hybrid
|
|
31
|
+
|
|
32
|
+
## Procedure
|
|
33
|
+
|
|
34
|
+
### 0) Detect headless setup
|
|
35
|
+
|
|
36
|
+
Run the detection script to assess the current architecture:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
node skills/wp-headless/scripts/headless_inspect.mjs --cwd=/path/to/wordpress
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The script outputs JSON with:
|
|
43
|
+
- `apiLayer` — REST API and/or WPGraphQL availability, custom endpoints count
|
|
44
|
+
- `frontend` — detected frontend framework (Next.js, Nuxt, Astro)
|
|
45
|
+
- `auth` — authentication methods available
|
|
46
|
+
- `cors` — CORS configuration status and allowed origins
|
|
47
|
+
- `webhooks` — outgoing webhook configuration
|
|
48
|
+
- `isHeadless` — boolean assessment of whether the setup is headless
|
|
49
|
+
|
|
50
|
+
### 1) Choose API layer
|
|
51
|
+
|
|
52
|
+
Decide between REST API (built-in) and WPGraphQL (plugin) based on project needs.
|
|
53
|
+
|
|
54
|
+
| Factor | REST API | WPGraphQL |
|
|
55
|
+
|--------|----------|-----------|
|
|
56
|
+
| Installation | Built-in, zero setup | Requires plugin |
|
|
57
|
+
| Data fetching | Fixed response shape | Fetch exactly what you need |
|
|
58
|
+
| Related data | Multiple requests | Single query with connections |
|
|
59
|
+
| Learning curve | Low (familiar HTTP) | Medium (GraphQL syntax) |
|
|
60
|
+
| Caching | Simple (HTTP cache) | Complex (query-level) |
|
|
61
|
+
| Best for | Simple sites, mobile apps | Complex content, performance-critical |
|
|
62
|
+
|
|
63
|
+
Use REST with `_fields` parameter for simple needs. Use WPGraphQL for complex content models.
|
|
64
|
+
|
|
65
|
+
Read: `references/api-layer-choice.md`
|
|
66
|
+
|
|
67
|
+
For REST endpoint development, also reference the `wp-rest-api` skill.
|
|
68
|
+
|
|
69
|
+
### 2) WPGraphQL setup
|
|
70
|
+
|
|
71
|
+
If using WPGraphQL:
|
|
72
|
+
1. Install: `wp plugin install wp-graphql --activate`
|
|
73
|
+
2. Explore schema at `/graphql` endpoint with GraphiQL
|
|
74
|
+
3. Register custom types and fields
|
|
75
|
+
4. Use cursor-based pagination (`first`/`after`)
|
|
76
|
+
5. Consider WPGraphQL Smart Cache for performance
|
|
77
|
+
|
|
78
|
+
Read: `references/wpgraphql.md`
|
|
79
|
+
|
|
80
|
+
### 3) Headless authentication
|
|
81
|
+
|
|
82
|
+
Choose the authentication method based on use case:
|
|
83
|
+
|
|
84
|
+
- **Application Passwords** (built-in): best for server-to-server and build-time fetching
|
|
85
|
+
- **JWT** (plugin): best for client-side authentication flows
|
|
86
|
+
- **NextAuth/Auth.js**: best for Next.js projects with WordPress as OAuth provider
|
|
87
|
+
- **Preview mode**: special auth for draft content preview
|
|
88
|
+
|
|
89
|
+
For security best practices in authentication, reference the `wp-security` skill.
|
|
90
|
+
|
|
91
|
+
Read: `references/headless-auth.md`
|
|
92
|
+
|
|
93
|
+
### 4) CORS configuration
|
|
94
|
+
|
|
95
|
+
Configure Cross-Origin Resource Sharing to allow the frontend to access WordPress APIs:
|
|
96
|
+
|
|
97
|
+
```php
|
|
98
|
+
add_filter('allowed_http_origins', function($origins) {
|
|
99
|
+
$origins[] = 'https://frontend.example.com';
|
|
100
|
+
return $origins;
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Key rules:
|
|
105
|
+
- Never use `Access-Control-Allow-Origin: *` with credentials
|
|
106
|
+
- Always specify exact origins in production
|
|
107
|
+
- Handle preflight `OPTIONS` requests
|
|
108
|
+
- WPGraphQL has built-in CORS settings
|
|
109
|
+
|
|
110
|
+
Read: `references/cors-config.md`
|
|
111
|
+
|
|
112
|
+
### 5) Frontend integration
|
|
113
|
+
|
|
114
|
+
Connect the frontend framework to WordPress data:
|
|
115
|
+
|
|
116
|
+
- **Next.js**: `fetch()` in App Router with `revalidate`, `getStaticProps` in Pages Router, ISR for incremental updates
|
|
117
|
+
- **Nuxt**: `useFetch()` / `useAsyncData()`, ISR with `routeRules`
|
|
118
|
+
- **Astro**: content collections from API, static-first with on-demand rendering
|
|
119
|
+
|
|
120
|
+
Common patterns: centralized API client, TypeScript types from schema, image optimization with WordPress media URLs.
|
|
121
|
+
|
|
122
|
+
Read: `references/frontend-integration.md`
|
|
123
|
+
|
|
124
|
+
### 6) Content webhooks and revalidation
|
|
125
|
+
|
|
126
|
+
Trigger frontend rebuilds or cache invalidation when WordPress content changes:
|
|
127
|
+
|
|
128
|
+
```php
|
|
129
|
+
add_action('transition_post_status', function($new, $old, $post) {
|
|
130
|
+
if ($new === 'publish') {
|
|
131
|
+
wp_remote_post('https://frontend.example.com/api/revalidate', [
|
|
132
|
+
'body' => json_encode(['path' => '/' . $post->post_name]),
|
|
133
|
+
'headers' => ['Content-Type' => 'application/json', 'Authorization' => 'Bearer SECRET'],
|
|
134
|
+
]);
|
|
135
|
+
}
|
|
136
|
+
}, 10, 3);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Strategies: path-based ISR, tag-based revalidation, full rebuild triggers. WPGraphQL Smart Cache provides automatic invalidation.
|
|
140
|
+
|
|
141
|
+
Read: `references/webhooks.md`
|
|
142
|
+
|
|
143
|
+
## Verification
|
|
144
|
+
|
|
145
|
+
- API returns data: `curl https://wp.example.com/wp-json/wp/v2/posts` returns JSON
|
|
146
|
+
- Frontend renders WordPress content correctly
|
|
147
|
+
- Authentication works: protected endpoints require credentials, public ones don't
|
|
148
|
+
- CORS headers correct: check `Access-Control-Allow-Origin` in response headers
|
|
149
|
+
- Preview mode: draft content visible in frontend preview
|
|
150
|
+
- Webhooks trigger: publish a post and confirm frontend revalidates
|
|
151
|
+
- Builds succeed: `next build` / `nuxt generate` / `astro build` completes
|
|
152
|
+
|
|
153
|
+
## Failure modes / debugging
|
|
154
|
+
|
|
155
|
+
- **CORS errors**: check browser DevTools Network tab for preflight failures; verify origin whitelist matches exactly (protocol + domain + port)
|
|
156
|
+
- **Authentication failures**: verify application password format (`user:xxxx xxxx xxxx`), check JWT token expiry, confirm `Authorization` header is forwarded
|
|
157
|
+
- **Stale content**: ISR `revalidate` interval too high; webhook not triggering; check `transition_post_status` hook fires on publish
|
|
158
|
+
- **GraphQL schema missing fields**: custom post types need `show_in_graphql => true`; ACF fields need WPGraphQL for ACF extension
|
|
159
|
+
- **Preview not working**: draft mode API route misconfigured; preview secret mismatch; WordPress preview URL not pointing to frontend
|
|
160
|
+
- **Build failures**: API unreachable during build; increase timeout; add fallback for missing data
|
|
161
|
+
|
|
162
|
+
## Escalation
|
|
163
|
+
|
|
164
|
+
- WPGraphQL documentation: https://www.wpgraphql.com/docs
|
|
165
|
+
- Next.js WordPress examples: https://github.com/vercel/next.js/tree/canary/examples/cms-wordpress
|
|
166
|
+
- Astro WordPress integration: https://docs.astro.build/en/guides/cms/wordpress/
|
|
167
|
+
- For REST endpoint development, use the `wp-rest-api` skill
|
|
168
|
+
- For authentication security, use the `wp-security` skill
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# API Layer Choice
|
|
2
|
+
|
|
3
|
+
Use this file when deciding between REST API and WPGraphQL for a headless WordPress project.
|
|
4
|
+
|
|
5
|
+
## Comparison matrix
|
|
6
|
+
|
|
7
|
+
| Criterion | REST API | WPGraphQL |
|
|
8
|
+
|-----------|----------|-----------|
|
|
9
|
+
| Built into core | Yes (since 4.7) | Plugin required |
|
|
10
|
+
| Data fetching | Fixed endpoints, multiple requests | Single query, exact fields |
|
|
11
|
+
| Over-fetching | Common (returns all fields) | Eliminated (request only what you need) |
|
|
12
|
+
| Under-fetching | Common (need multiple requests) | Eliminated (nested queries) |
|
|
13
|
+
| Caching | HTTP caching (CDN-friendly) | Requires custom caching layer |
|
|
14
|
+
| Learning curve | Lower (familiar REST patterns) | Higher (GraphQL query language) |
|
|
15
|
+
| Community/ecosystem | Largest | Growing, strong Gatsby/Next.js integration |
|
|
16
|
+
| Real-time | Polling or custom | Subscriptions (with extensions) |
|
|
17
|
+
| Authentication | Cookie, Application Passwords, JWT | Same as REST + GraphQL-specific |
|
|
18
|
+
| File uploads | Native multipart | Requires separate REST endpoint |
|
|
19
|
+
| Performance | Predictable | Faster for complex pages, slower for simple |
|
|
20
|
+
| Debugging | Standard HTTP tools | Requires GraphQL client (GraphiQL) |
|
|
21
|
+
|
|
22
|
+
## When to choose REST API
|
|
23
|
+
|
|
24
|
+
- **Simple content sites** — blog, portfolio, brochure
|
|
25
|
+
- **CDN-heavy architecture** — REST responses cache naturally at edge
|
|
26
|
+
- **Team unfamiliar with GraphQL** — lower learning curve
|
|
27
|
+
- **Third-party integrations** — most services expect REST
|
|
28
|
+
- **WooCommerce headless** — WooCommerce REST API is mature and well-documented
|
|
29
|
+
- **Mobile apps** — REST is universal across platforms
|
|
30
|
+
- **Server-side rendering with few queries** — ISR/SSG pages that make 1-3 API calls
|
|
31
|
+
|
|
32
|
+
## When to choose WPGraphQL
|
|
33
|
+
|
|
34
|
+
- **Complex page compositions** — homepage with posts, categories, menus, options, custom fields
|
|
35
|
+
- **Component-driven frontend** — React/Vue components that each declare their data needs
|
|
36
|
+
- **Gatsby projects** — gatsby-source-wordpress uses WPGraphQL natively
|
|
37
|
+
- **Deeply nested data** — posts → author → posts → categories in one query
|
|
38
|
+
- **Multiple content types per page** — dashboard-style layouts
|
|
39
|
+
- **Rapid frontend development** — frontend devs query exactly what they need
|
|
40
|
+
|
|
41
|
+
## Hybrid approach
|
|
42
|
+
|
|
43
|
+
Use both:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
REST API → Simple CRUD, file uploads, WooCommerce, webhooks
|
|
47
|
+
WPGraphQL → Complex page data fetching, component queries
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This is common in production. Example:
|
|
51
|
+
- Blog listing page: WPGraphQL (needs posts + categories + featured images + author in one query)
|
|
52
|
+
- Contact form submission: REST API (simple POST)
|
|
53
|
+
- WooCommerce cart/checkout: WooCommerce REST API
|
|
54
|
+
- Media upload: REST API (multipart form data)
|
|
55
|
+
|
|
56
|
+
## REST API quick setup for headless
|
|
57
|
+
|
|
58
|
+
```php
|
|
59
|
+
// Register custom endpoint
|
|
60
|
+
add_action('rest_api_init', function() {
|
|
61
|
+
register_rest_route('myapp/v1', '/homepage', [
|
|
62
|
+
'methods' => 'GET',
|
|
63
|
+
'callback' => 'get_homepage_data',
|
|
64
|
+
'permission_callback' => '__return_true',
|
|
65
|
+
]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function get_homepage_data() {
|
|
69
|
+
return [
|
|
70
|
+
'hero' => get_field('hero', 'option'),
|
|
71
|
+
'posts' => get_posts(['numberposts' => 6, 'post_type' => 'post']),
|
|
72
|
+
'menu' => wp_get_nav_menu_items('primary'),
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## WPGraphQL quick setup for headless
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
wp plugin install wp-graphql --activate
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```graphql
|
|
84
|
+
# Single query for a complex homepage
|
|
85
|
+
query Homepage {
|
|
86
|
+
posts(first: 6) {
|
|
87
|
+
nodes {
|
|
88
|
+
title
|
|
89
|
+
excerpt
|
|
90
|
+
uri
|
|
91
|
+
featuredImage {
|
|
92
|
+
node {
|
|
93
|
+
sourceUrl(size: MEDIUM_LARGE)
|
|
94
|
+
altText
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
categories {
|
|
98
|
+
nodes {
|
|
99
|
+
name
|
|
100
|
+
slug
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
menus(where: { location: PRIMARY }) {
|
|
106
|
+
nodes {
|
|
107
|
+
menuItems {
|
|
108
|
+
nodes {
|
|
109
|
+
label
|
|
110
|
+
url
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Performance considerations
|
|
119
|
+
|
|
120
|
+
### REST API
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
Homepage data:
|
|
124
|
+
GET /wp-json/wp/v2/posts?per_page=6 → 1 request
|
|
125
|
+
GET /wp-json/wp/v2/categories → 1 request
|
|
126
|
+
GET /wp-json/wp/v2/media/{id} (per post) → 6 requests
|
|
127
|
+
GET /wp-json/wp/v2/menus/primary → 1 request
|
|
128
|
+
Total: 9 requests, ~150KB response (with over-fetching)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### WPGraphQL
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
Homepage data:
|
|
135
|
+
POST /graphql (single query) → 1 request
|
|
136
|
+
Total: 1 request, ~25KB response (exact fields only)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Mitigation for REST over-fetching
|
|
140
|
+
|
|
141
|
+
```php
|
|
142
|
+
// Use _fields parameter to reduce payload
|
|
143
|
+
// GET /wp-json/wp/v2/posts?_fields=id,title,excerpt,featured_media
|
|
144
|
+
|
|
145
|
+
// Or create custom endpoints that aggregate data
|
|
146
|
+
register_rest_route('myapp/v1', '/homepage', [
|
|
147
|
+
'callback' => function() {
|
|
148
|
+
// Return exactly what the frontend needs
|
|
149
|
+
}
|
|
150
|
+
]);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Decision checklist
|
|
154
|
+
|
|
155
|
+
1. What is the team's GraphQL experience? (low → REST)
|
|
156
|
+
2. How many API calls per page? (>3 → consider WPGraphQL)
|
|
157
|
+
3. Is edge caching critical? (yes → REST preferred)
|
|
158
|
+
4. Using Gatsby? (yes → WPGraphQL)
|
|
159
|
+
5. Using WooCommerce? (yes → REST for commerce, WPGraphQL for content)
|
|
160
|
+
6. How nested is the data model? (deeply nested → WPGraphQL)
|
|
@@ -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
|
+
```
|