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.
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +26 -0
- package/agents/wp-content-strategist.md +25 -0
- package/agents/wp-ecommerce-manager.md +23 -0
- package/agents/wp-site-manager.md +26 -0
- package/docs/GUIDE.md +116 -28
- package/package.json +8 -3
- package/skills/wordpress-router/references/decision-tree.md +8 -2
- package/skills/wp-content/SKILL.md +1 -0
- package/skills/wp-content-attribution/SKILL.md +97 -0
- package/skills/wp-content-attribution/references/attribution-models.md +189 -0
- package/skills/wp-content-attribution/references/conversion-funnels.md +137 -0
- package/skills/wp-content-attribution/references/reporting-dashboards.md +199 -0
- package/skills/wp-content-attribution/references/roi-calculation.md +202 -0
- package/skills/wp-content-attribution/references/utm-tracking-setup.md +161 -0
- package/skills/wp-content-attribution/scripts/attribution_inspect.mjs +277 -0
- package/skills/wp-headless/SKILL.md +1 -0
- package/skills/wp-i18n/SKILL.md +1 -0
- package/skills/wp-multilang-network/SKILL.md +107 -0
- package/skills/wp-multilang-network/references/content-sync.md +182 -0
- package/skills/wp-multilang-network/references/hreflang-config.md +198 -0
- package/skills/wp-multilang-network/references/language-routing.md +234 -0
- package/skills/wp-multilang-network/references/network-architecture.md +119 -0
- package/skills/wp-multilang-network/references/seo-international.md +213 -0
- package/skills/wp-multilang-network/scripts/multilang_inspect.mjs +308 -0
- package/skills/wp-multisite/SKILL.md +1 -0
- package/skills/wp-programmatic-seo/SKILL.md +97 -0
- package/skills/wp-programmatic-seo/references/data-sources.md +200 -0
- package/skills/wp-programmatic-seo/references/location-seo.md +134 -0
- package/skills/wp-programmatic-seo/references/product-seo.md +147 -0
- package/skills/wp-programmatic-seo/references/technical-seo.md +197 -0
- package/skills/wp-programmatic-seo/references/template-architecture.md +125 -0
- package/skills/wp-programmatic-seo/scripts/programmatic_seo_inspect.mjs +264 -0
- 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
|