claude-plugin-wordpress-manager 2.2.0 → 2.3.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 (34) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/CHANGELOG.md +26 -0
  3. package/agents/wp-content-strategist.md +25 -0
  4. package/agents/wp-ecommerce-manager.md +23 -0
  5. package/agents/wp-site-manager.md +26 -0
  6. package/docs/GUIDE.md +116 -28
  7. package/package.json +8 -3
  8. package/skills/wordpress-router/references/decision-tree.md +8 -2
  9. package/skills/wp-content/SKILL.md +1 -0
  10. package/skills/wp-content-attribution/SKILL.md +97 -0
  11. package/skills/wp-content-attribution/references/attribution-models.md +189 -0
  12. package/skills/wp-content-attribution/references/conversion-funnels.md +137 -0
  13. package/skills/wp-content-attribution/references/reporting-dashboards.md +199 -0
  14. package/skills/wp-content-attribution/references/roi-calculation.md +202 -0
  15. package/skills/wp-content-attribution/references/utm-tracking-setup.md +161 -0
  16. package/skills/wp-content-attribution/scripts/attribution_inspect.mjs +277 -0
  17. package/skills/wp-headless/SKILL.md +1 -0
  18. package/skills/wp-i18n/SKILL.md +1 -0
  19. package/skills/wp-multilang-network/SKILL.md +107 -0
  20. package/skills/wp-multilang-network/references/content-sync.md +182 -0
  21. package/skills/wp-multilang-network/references/hreflang-config.md +198 -0
  22. package/skills/wp-multilang-network/references/language-routing.md +234 -0
  23. package/skills/wp-multilang-network/references/network-architecture.md +119 -0
  24. package/skills/wp-multilang-network/references/seo-international.md +213 -0
  25. package/skills/wp-multilang-network/scripts/multilang_inspect.mjs +308 -0
  26. package/skills/wp-multisite/SKILL.md +1 -0
  27. package/skills/wp-programmatic-seo/SKILL.md +97 -0
  28. package/skills/wp-programmatic-seo/references/data-sources.md +200 -0
  29. package/skills/wp-programmatic-seo/references/location-seo.md +134 -0
  30. package/skills/wp-programmatic-seo/references/product-seo.md +147 -0
  31. package/skills/wp-programmatic-seo/references/technical-seo.md +197 -0
  32. package/skills/wp-programmatic-seo/references/template-architecture.md +125 -0
  33. package/skills/wp-programmatic-seo/scripts/programmatic_seo_inspect.mjs +264 -0
  34. package/skills/wp-woocommerce/SKILL.md +1 -0
@@ -0,0 +1,308 @@
1
+ /**
2
+ * multilang_inspect.mjs — Detect multi-language network readiness.
3
+ *
4
+ * Scans for WordPress Multisite status, multilingual plugins, sub-site
5
+ * language patterns, hreflang tags, and WPLANG configuration.
6
+ * Outputs a JSON report to stdout.
7
+ *
8
+ * Usage:
9
+ * node multilang_inspect.mjs [--cwd=/path/to/check]
10
+ *
11
+ * Exit codes:
12
+ * 0 — multi-language network indicators detected
13
+ * 1 — no multi-language network indicators detected
14
+ */
15
+
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ import process from "node:process";
19
+ import { execSync } from "node:child_process";
20
+
21
+ const TOOL_VERSION = "1.0.0";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Helpers
25
+ // ---------------------------------------------------------------------------
26
+
27
+ function statSafe(p) {
28
+ try {
29
+ return fs.statSync(p);
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ function readFileSafe(p) {
36
+ try {
37
+ return fs.readFileSync(p, "utf8");
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ function readJsonSafe(p) {
44
+ const raw = readFileSafe(p);
45
+ if (!raw) return null;
46
+ try {
47
+ return JSON.parse(raw);
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ function execSafe(cmd, cwd, timeoutMs = 5000) {
54
+ try {
55
+ return execSync(cmd, { encoding: "utf8", timeout: timeoutMs, cwd, stdio: ["pipe", "pipe", "pipe"] }).trim();
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ function readdirSafe(dir) {
62
+ try {
63
+ return fs.readdirSync(dir);
64
+ } catch {
65
+ return [];
66
+ }
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Parse --cwd argument
71
+ // ---------------------------------------------------------------------------
72
+
73
+ function parseCwd() {
74
+ const cwdArg = process.argv.find((a) => a.startsWith("--cwd="));
75
+ return cwdArg ? cwdArg.slice(6) : process.cwd();
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Detect WordPress Multisite
80
+ // ---------------------------------------------------------------------------
81
+
82
+ function detectMultisite(cwd) {
83
+ const result = { is_multisite: false, site_count: 0 };
84
+
85
+ // Check wp-config.php for MULTISITE constant
86
+ const wpConfig = readFileSafe(path.join(cwd, "wp-config.php"));
87
+ if (wpConfig) {
88
+ if (/define\s*\(\s*['"]MULTISITE['"]\s*,\s*true\s*\)/i.test(wpConfig)) {
89
+ result.is_multisite = true;
90
+ }
91
+ if (/define\s*\(\s*['"]WP_ALLOW_MULTISITE['"]\s*,\s*true\s*\)/i.test(wpConfig)) {
92
+ result.is_multisite = true;
93
+ }
94
+ }
95
+
96
+ // Try WP-CLI for site count
97
+ const siteCount = execSafe("wp site list --format=count 2>/dev/null", cwd);
98
+ if (siteCount) {
99
+ const count = parseInt(siteCount);
100
+ if (count > 1) {
101
+ result.is_multisite = true;
102
+ result.site_count = count;
103
+ }
104
+ }
105
+
106
+ return result;
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Detect multilingual plugins
111
+ // ---------------------------------------------------------------------------
112
+
113
+ function detectMultilingualPlugin(cwd) {
114
+ const result = { detected: false, plugin: null };
115
+
116
+ const pluginsDir = path.join(cwd, "wp-content", "plugins");
117
+ const multilingualPlugins = [
118
+ { dir: "sitepress-multilingual-cms", name: "WPML" },
119
+ { dir: "polylang", name: "Polylang" },
120
+ { dir: "polylang-pro", name: "Polylang Pro" },
121
+ { dir: "multilingualpress", name: "MultilingualPress" },
122
+ { dir: "translatepress-multilingual", name: "TranslatePress" },
123
+ { dir: "weglot", name: "Weglot" },
124
+ ];
125
+
126
+ for (const plugin of multilingualPlugins) {
127
+ if (statSafe(path.join(pluginsDir, plugin.dir))?.isDirectory()) {
128
+ result.detected = true;
129
+ result.plugin = plugin.name;
130
+ return result;
131
+ }
132
+ }
133
+
134
+ return result;
135
+ }
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // Detect language patterns in sub-sites
139
+ // ---------------------------------------------------------------------------
140
+
141
+ function detectLanguagePatterns(cwd) {
142
+ const result = { detected_languages: [], sites: [] };
143
+
144
+ // ISO 639-1 language codes to look for
145
+ const langCodes = new Set([
146
+ "en", "it", "de", "fr", "es", "pt", "nl", "pl", "sv", "da", "no", "fi",
147
+ "ru", "ja", "zh", "ko", "ar", "he", "hi", "tr", "el", "cs", "ro", "hu",
148
+ "uk", "bg", "hr", "sk", "sl", "et", "lv", "lt", "th", "vi", "id", "ms",
149
+ ]);
150
+
151
+ // Try WP-CLI to list sites with paths
152
+ const siteList = execSafe("wp site list --fields=blog_id,url,path --format=json 2>/dev/null", cwd);
153
+ if (siteList) {
154
+ try {
155
+ const sites = JSON.parse(siteList);
156
+ for (const site of sites) {
157
+ const pathSlug = (site.path || "").replace(/\//g, "").toLowerCase();
158
+ const isLang = langCodes.has(pathSlug);
159
+ result.sites.push({
160
+ blog_id: site.blog_id,
161
+ url: site.url,
162
+ path: site.path,
163
+ detected_language: isLang ? pathSlug : null,
164
+ });
165
+ if (isLang && !result.detected_languages.includes(pathSlug)) {
166
+ result.detected_languages.push(pathSlug);
167
+ }
168
+ }
169
+ } catch { /* ignore parse errors */ }
170
+ }
171
+
172
+ return result;
173
+ }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Detect hreflang tags
177
+ // ---------------------------------------------------------------------------
178
+
179
+ function detectHreflang(cwd) {
180
+ const result = { detected: false, sources: [] };
181
+
182
+ // Check mu-plugins for hreflang generation
183
+ const muPluginsDir = path.join(cwd, "wp-content", "mu-plugins");
184
+ const muFiles = readdirSafe(muPluginsDir);
185
+ for (const file of muFiles) {
186
+ if (!file.endsWith(".php")) continue;
187
+ const content = readFileSafe(path.join(muPluginsDir, file));
188
+ if (content && /hreflang/i.test(content)) {
189
+ result.detected = true;
190
+ result.sources.push(`mu-plugin: ${file}`);
191
+ }
192
+ }
193
+
194
+ // Check theme files for hreflang
195
+ const themesDir = path.join(cwd, "wp-content", "themes");
196
+ const themes = readdirSafe(themesDir);
197
+ for (const theme of themes) {
198
+ const headerPhp = readFileSafe(path.join(themesDir, theme, "header.php"));
199
+ if (headerPhp && /hreflang/i.test(headerPhp)) {
200
+ result.detected = true;
201
+ result.sources.push(`theme: ${theme}/header.php`);
202
+ }
203
+ const functionsPhp = readFileSafe(path.join(themesDir, theme, "functions.php"));
204
+ if (functionsPhp && /hreflang/i.test(functionsPhp)) {
205
+ result.detected = true;
206
+ result.sources.push(`theme: ${theme}/functions.php`);
207
+ }
208
+ }
209
+
210
+ // Multilingual plugins typically handle hreflang automatically
211
+ const pluginsDir = path.join(cwd, "wp-content", "plugins");
212
+ const hreflangPlugins = [
213
+ { dir: "sitepress-multilingual-cms", name: "WPML (auto hreflang)" },
214
+ { dir: "polylang", name: "Polylang (auto hreflang)" },
215
+ { dir: "multilingualpress", name: "MultilingualPress (auto hreflang)" },
216
+ ];
217
+ for (const plugin of hreflangPlugins) {
218
+ if (statSafe(path.join(pluginsDir, plugin.dir))?.isDirectory()) {
219
+ result.detected = true;
220
+ result.sources.push(`plugin: ${plugin.name}`);
221
+ }
222
+ }
223
+
224
+ return result;
225
+ }
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // Detect WPLANG configuration
229
+ // ---------------------------------------------------------------------------
230
+
231
+ function detectWplang(cwd) {
232
+ const wpConfig = readFileSafe(path.join(cwd, "wp-config.php"));
233
+ if (!wpConfig) return null;
234
+
235
+ const match = wpConfig.match(/define\s*\(\s*['"]WPLANG['"]\s*,\s*['"]([\w_]+)['"]\s*\)/i);
236
+ return match ? match[1] : null;
237
+ }
238
+
239
+ // ---------------------------------------------------------------------------
240
+ // Main
241
+ // ---------------------------------------------------------------------------
242
+
243
+ function main() {
244
+ const cwd = parseCwd();
245
+
246
+ if (!statSafe(cwd)?.isDirectory()) {
247
+ console.error(`Error: directory not found: ${cwd}`);
248
+ process.exit(1);
249
+ }
250
+
251
+ const multisite = detectMultisite(cwd);
252
+ const multilingual = detectMultilingualPlugin(cwd);
253
+ const languages = detectLanguagePatterns(cwd);
254
+ const hreflang = detectHreflang(cwd);
255
+ const wplang = detectWplang(cwd);
256
+
257
+ const detected = multisite.is_multisite || multilingual.detected || languages.detected_languages.length > 0;
258
+
259
+ const report = {
260
+ tool: "multilang_inspect",
261
+ version: TOOL_VERSION,
262
+ cwd,
263
+ detected,
264
+ is_multisite: multisite.is_multisite,
265
+ site_count: multisite.site_count,
266
+ multilingual_plugin: multilingual.plugin,
267
+ detected_languages: languages.detected_languages,
268
+ sites: languages.sites,
269
+ has_hreflang: hreflang.detected,
270
+ hreflang_sources: hreflang.sources,
271
+ wplang: wplang,
272
+ multilang_readiness: "unknown",
273
+ recommendations: [],
274
+ };
275
+
276
+ // Assess readiness
277
+ if (multisite.is_multisite && multilingual.detected && hreflang.detected && languages.detected_languages.length >= 2) {
278
+ report.multilang_readiness = "high";
279
+ } else if (multisite.is_multisite && (multilingual.detected || languages.detected_languages.length > 0)) {
280
+ report.multilang_readiness = "medium";
281
+ } else if (multisite.is_multisite) {
282
+ report.multilang_readiness = "low";
283
+ } else {
284
+ report.multilang_readiness = "not_ready";
285
+ }
286
+
287
+ // Recommendations
288
+ if (!multisite.is_multisite) {
289
+ report.recommendations.push("WordPress Multisite is not enabled. Enable Multisite to create per-language sub-sites. See wp-multisite skill.");
290
+ }
291
+ if (multisite.is_multisite && !multilingual.detected) {
292
+ report.recommendations.push("No multilingual plugin detected. Install WPML, Polylang, or MultilingualPress for translation management.");
293
+ }
294
+ if (multisite.is_multisite && languages.detected_languages.length === 0) {
295
+ report.recommendations.push("No language-coded sub-sites detected. Create sub-sites with ISO 639-1 slugs (e.g., /it/, /de/, /fr/).");
296
+ }
297
+ if (multisite.is_multisite && !hreflang.detected) {
298
+ report.recommendations.push("No hreflang tags detected. Install the hreflang mu-plugin or configure your multilingual plugin to generate hreflang.");
299
+ }
300
+ if (multisite.is_multisite && multisite.site_count <= 1) {
301
+ report.recommendations.push("Only one site in the network. Create additional sub-sites for each target language.");
302
+ }
303
+
304
+ console.log(JSON.stringify(report, null, 2));
305
+ process.exit(detected ? 0 : 1);
306
+ }
307
+
308
+ main();
@@ -90,3 +90,4 @@ For complex multi-step multisite operations, use the `wp-site-manager` agent (wh
90
90
  - `wp-wpcli-and-ops` — WP-CLI command reference and multisite flags
91
91
  - `wp-security` — Super Admin capabilities and multisite security
92
92
  - `wp-deploy` — Deploy to multisite network
93
+ - `wp-multilang-network` — Multi-language network orchestration (hreflang, content sync, international SEO)
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: wp-programmatic-seo
3
+ description: |
4
+ This skill should be used when the user asks to "generate template pages",
5
+ "create city pages", "programmatic SEO", "scalable landing pages", "dynamic pages
6
+ from data", "product variant pages", "location-based SEO", "ISR pages at scale",
7
+ "bulk page generation", or mentions creating large numbers of pages from templates
8
+ or data sources using WordPress as content backend.
9
+ version: 1.0.0
10
+ ---
11
+
12
+ ## Overview
13
+
14
+ Programmatic SEO is the systematic generation of large-scale, search-optimized pages from structured data. WordPress serves as the canonical content source (via custom post types, taxonomies, and REST API), while a headless frontend (Next.js, Nuxt, Astro) renders the pages using ISR/SSG for performance and crawlability.
15
+
16
+ This skill orchestrates existing MCP tools — content CRUD, multisite management, headless architecture — into scalable page generation workflows. No new tools are required.
17
+
18
+ ## When to Use
19
+
20
+ - User wants to generate 100s–1000s of pages from templates and structured data
21
+ - City/location pages (e.g., "plumber in {city}" for 200 cities)
22
+ - Product variant pages (size/color/model combinations)
23
+ - Comparison pages (product A vs product B)
24
+ - Directory listings from external data (CSV, API)
25
+ - Any `/{entity}/{location}/{variant}` URL pattern at scale
26
+
27
+ ## Programmatic SEO vs Manual Content
28
+
29
+ | Aspect | Programmatic SEO | Manual Content |
30
+ |--------|-----------------|----------------|
31
+ | Scale | 100s–1000s of pages | 1–50 pages |
32
+ | Consistency | Template-enforced uniformity | Variable quality |
33
+ | Maintenance | Update template → all pages update | Edit each page individually |
34
+ | SEO value | Long-tail keyword coverage | High per-page authority |
35
+ | Setup cost | Higher initial (template + data) | Lower initial, higher ongoing |
36
+ | Content depth | Data-driven, structured | Human-crafted, nuanced |
37
+
38
+ ## Prerequisites / Detection
39
+
40
+ ```bash
41
+ node skills/wp-programmatic-seo/scripts/programmatic_seo_inspect.mjs --cwd=/path/to/wordpress
42
+ ```
43
+
44
+ The script checks headless frontend presence, SEO plugins, content volume, custom post types, and WPGraphQL availability.
45
+
46
+ ## Programmatic SEO Operations Decision Tree
47
+
48
+ 1. **What type of programmatic content?**
49
+
50
+ - "template pages" / "page templates" / "dynamic templates"
51
+ → **Template Architecture** — Read: `references/template-architecture.md`
52
+
53
+ - "city pages" / "location pages" / "LocalBusiness" / "service area pages"
54
+ → **Location-Based SEO** — Read: `references/location-seo.md`
55
+
56
+ - "product variants" / "filtered pages" / "comparison pages" / "category landing"
57
+ → **Product Programmatic SEO** — Read: `references/product-seo.md`
58
+
59
+ - "data-driven" / "API pages" / "custom endpoint" / "CSV import" / "external data"
60
+ → **Data-Driven Content** — Read: `references/data-sources.md`
61
+
62
+ - "sitemap" / "indexing" / "crawl budget" / "canonical" / "Core Web Vitals"
63
+ → **Technical SEO** — Read: `references/technical-seo.md`
64
+
65
+ 2. **Common workflow (all types):**
66
+ 1. Assess data source — what structured data exists? (products, locations, categories)
67
+ 2. Design URL pattern — `/{service}/{city}` or `/{product}/{variant}`
68
+ 3. Create CPT or taxonomy in WordPress if needed (via `create_content` MCP tool)
69
+ 4. Build page template with dynamic fields (title, meta, H1, body)
70
+ 5. Generate content in bulk using REST API (loop with `create_content`)
71
+ 6. Configure headless frontend ISR/SSG (reference: template-architecture)
72
+ 7. Generate and submit XML sitemap (reference: technical-seo)
73
+
74
+ ## Recommended Agent
75
+
76
+ `wp-content-strategist` — handles content strategy, template design, and bulk generation workflows.
77
+
78
+ ## Additional Resources
79
+
80
+ ### Reference Files
81
+
82
+ | File | Description |
83
+ |------|-------------|
84
+ | **`references/template-architecture.md`** | Page template patterns, URL design, ISR/SSG config, bulk creation |
85
+ | **`references/location-seo.md`** | City/location pages, LocalBusiness schema, geo-targeting |
86
+ | **`references/product-seo.md`** | Product variants, comparison pages, Product schema |
87
+ | **`references/data-sources.md`** | REST API, WPGraphQL, external data, content quality gates |
88
+ | **`references/technical-seo.md`** | Sitemaps, crawl budget, canonicals, internal linking, CWV |
89
+
90
+ ### Related Skills
91
+
92
+ - `wp-headless` — headless architecture, ISR/SSG, webhooks
93
+ - `wp-multisite` — multisite network for segmented content at scale
94
+ - `wp-woocommerce` — product data as SEO content source
95
+ - `wp-rest-api` — REST API endpoints for content CRUD
96
+ - `wp-content` — content management fundamentals
97
+ - `wp-content-repurposing` — transform existing content into new formats
@@ -0,0 +1,200 @@
1
+ # Data Sources for Programmatic SEO
2
+
3
+ Use this file when connecting structured data (WordPress REST API, WPGraphQL, external APIs, CSV imports) to programmatic page generation, including quality gates and freshness strategies.
4
+
5
+ ## WordPress REST API as Data Source
6
+
7
+ The REST API provides paginated, filterable access to all WordPress content:
8
+
9
+ ### Pagination for Large Datasets
10
+
11
+ ```bash
12
+ # Fetch all posts with pagination (100 per page max)
13
+ GET /wp-json/wp/v2/posts?per_page=100&page=1
14
+ # Check X-WP-TotalPages header for total pages
15
+
16
+ # Fetch with filters
17
+ GET /wp-json/wp/v2/location?per_page=100&status=publish&orderby=title
18
+
19
+ # Embed related data (featured image, author, terms)
20
+ GET /wp-json/wp/v2/posts?_embed&per_page=50
21
+ ```
22
+
23
+ ### Filtering and Search
24
+
25
+ | Parameter | Description | Example |
26
+ |-----------|-------------|---------|
27
+ | `categories` | Filter by category ID | `?categories=5,12` |
28
+ | `tags` | Filter by tag ID | `?tags=8` |
29
+ | `search` | Full-text search | `?search=miami` |
30
+ | `before`/`after` | Date range | `?after=2024-01-01T00:00:00` |
31
+ | `meta_key`/`meta_value` | Custom field filter (requires plugin) | `?meta_key=city&meta_value=Miami` |
32
+ | `orderby` | Sort: date, title, modified, rand | `?orderby=title&order=asc` |
33
+
34
+ ### Custom REST Fields
35
+
36
+ Expose CPT meta for programmatic consumption:
37
+
38
+ ```php
39
+ register_rest_field('location', 'seo_data', [
40
+ 'get_callback' => function ($post) {
41
+ return [
42
+ 'city' => get_post_meta($post['id'], 'city', true),
43
+ 'state' => get_post_meta($post['id'], 'state', true),
44
+ 'population' => (int) get_post_meta($post['id'], 'population', true),
45
+ 'coordinates' => [
46
+ 'lat' => (float) get_post_meta($post['id'], 'lat', true),
47
+ 'lng' => (float) get_post_meta($post['id'], 'lng', true),
48
+ ],
49
+ ];
50
+ },
51
+ ]);
52
+ ```
53
+
54
+ ## WPGraphQL Queries for Programmatic Content
55
+
56
+ WPGraphQL is more efficient for complex, nested data fetching:
57
+
58
+ ### Batch Queries with Fragments
59
+
60
+ ```graphql
61
+ fragment LocationFields on Location {
62
+ id
63
+ title
64
+ slug
65
+ locationMeta {
66
+ city
67
+ state
68
+ population
69
+ latitude
70
+ longitude
71
+ }
72
+ seo {
73
+ title
74
+ metaDesc
75
+ canonical
76
+ }
77
+ }
78
+
79
+ query AllLocations($first: Int!, $after: String) {
80
+ locations(first: $first, after: $after) {
81
+ pageInfo {
82
+ hasNextPage
83
+ endCursor
84
+ }
85
+ nodes {
86
+ ...LocationFields
87
+ }
88
+ }
89
+ }
90
+ ```
91
+
92
+ ### Advantages over REST API for Programmatic SEO
93
+
94
+ | Aspect | REST API | WPGraphQL |
95
+ |--------|----------|-----------|
96
+ | Payload size | Full objects, many unused fields | Only requested fields |
97
+ | Nested data | Multiple requests or `_embed` | Single query with relations |
98
+ | Pagination | Offset-based (slow at high pages) | Cursor-based (consistent speed) |
99
+ | Batch queries | N requests for N types | 1 request with aliases |
100
+
101
+ ## Custom REST Endpoints
102
+
103
+ Register specialized endpoints for aggregated programmatic data:
104
+
105
+ ```php
106
+ add_action('rest_api_init', function () {
107
+ register_rest_route('programmatic-seo/v1', '/page-data', [
108
+ 'methods' => 'GET',
109
+ 'callback' => function ($request) {
110
+ $type = $request->get_param('type') ?? 'location';
111
+ $posts = get_posts([
112
+ 'post_type' => $type,
113
+ 'posts_per_page' => -1,
114
+ 'post_status' => 'publish',
115
+ ]);
116
+ return array_map(function ($post) {
117
+ return [
118
+ 'slug' => $post->post_name,
119
+ 'title' => $post->post_title,
120
+ 'content' => apply_filters('the_content', $post->post_content),
121
+ 'meta' => get_post_meta($post->ID),
122
+ ];
123
+ }, $posts);
124
+ },
125
+ 'permission_callback' => '__return_true',
126
+ ]);
127
+ });
128
+ ```
129
+
130
+ ## External Data Integration
131
+
132
+ ### CSV Import → CPT
133
+
134
+ ```bash
135
+ # Workflow:
136
+ # 1. Parse CSV with Node.js or WP-CLI
137
+ # 2. Create WordPress posts via REST API
138
+
139
+ # WP-CLI approach:
140
+ wp post create --post_type=location --post_title="Plumbing in Miami" \
141
+ --post_status=publish --meta_input='{"city":"Miami","state":"FL","zip":"33101"}'
142
+
143
+ # MCP tool approach (in loop):
144
+ create_content(type="location", title="Plumbing in Miami", status="publish",
145
+ meta={"city": "Miami", "state": "FL", "zip": "33101"})
146
+ ```
147
+
148
+ ### API Sync → WordPress
149
+
150
+ For data from external APIs (property listings, job boards, etc.):
151
+
152
+ 1. **Cron-based sync:** WP-Cron or system cron triggers API fetch + `wp_insert_post()`
153
+ 2. **Webhook-based sync:** External service sends webhooks → WordPress receiver updates CPT
154
+ 3. **Manual import:** Admin uploads CSV → background job creates/updates posts
155
+
156
+ ## Data Freshness Strategies
157
+
158
+ | Strategy | Trigger | Use Case |
159
+ |----------|---------|----------|
160
+ | Cron update | Scheduled (hourly/daily) | Price updates, stock changes |
161
+ | Webhook revalidation | External event | Order status, inventory sync |
162
+ | On-demand ISR | Manual admin action | Content corrections, new pages |
163
+ | Build-time SSG | Git push / deploy | Static directories, rarely changing data |
164
+
165
+ ### Webhook-Triggered Revalidation
166
+
167
+ ```javascript
168
+ // Next.js API route for on-demand ISR
169
+ // POST /api/revalidate?secret=TOKEN&path=/services/miami
170
+ export default async function handler(req, res) {
171
+ if (req.query.secret !== process.env.REVALIDATION_SECRET) {
172
+ return res.status(401).json({ message: 'Invalid token' });
173
+ }
174
+ await res.revalidate(req.query.path);
175
+ return res.json({ revalidated: true });
176
+ }
177
+ ```
178
+
179
+ ## Content Quality Gates
180
+
181
+ Before publishing programmatic pages, enforce minimum quality standards:
182
+
183
+ | Gate | Threshold | Action if Fails |
184
+ |------|-----------|-----------------|
185
+ | Word count | >= 300 words | Block publish, flag for review |
186
+ | Required fields | All template vars populated | Block publish, log missing fields |
187
+ | SEO score | Title + meta desc + H1 present | Warning, auto-generate from template |
188
+ | Duplicate check | No existing page with same slug | Skip creation, log duplicate |
189
+ | Image present | Featured image or OG image set | Warning, use category default image |
190
+ | Schema valid | JSON-LD parses without errors | Block publish, fix template |
191
+
192
+ **Implementation:** Add validation in the bulk creation loop before calling `create_content`.
193
+
194
+ ## Decision Checklist
195
+
196
+ 1. Is REST API or WPGraphQL better for this dataset? → Complex nesting = WPGraphQL; simple = REST
197
+ 2. Are all needed fields exposed via API? → Register `rest_field` or GraphQL fields if not
198
+ 3. How will data stay fresh? → Choose cron/webhook/ISR based on update frequency
199
+ 4. Are quality gates enforced before publish? → Never publish without validation
200
+ 5. Can the data source handle bulk queries? → Test pagination at expected scale