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,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
+ ```