claude-plugin-wordpress-manager 2.2.1 → 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 +21 -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/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,125 @@
|
|
|
1
|
+
# Template Architecture
|
|
2
|
+
|
|
3
|
+
Use this file when designing page templates for headless WordPress programmatic SEO — custom post types as data source, URL structure, ISR/SSG configuration, and bulk content creation.
|
|
4
|
+
|
|
5
|
+
## Custom Post Types as Data Source
|
|
6
|
+
|
|
7
|
+
WordPress CPTs are the ideal data backbone for programmatic pages:
|
|
8
|
+
|
|
9
|
+
```php
|
|
10
|
+
// Register a CPT for programmatic content (e.g., locations)
|
|
11
|
+
register_post_type('location', [
|
|
12
|
+
'public' => true,
|
|
13
|
+
'show_in_rest' => true, // Required for headless access
|
|
14
|
+
'supports' => ['title', 'editor', 'custom-fields', 'thumbnail'],
|
|
15
|
+
'rewrite' => ['slug' => 'locations'],
|
|
16
|
+
'has_archive' => true,
|
|
17
|
+
]);
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Required CPT fields for programmatic SEO:**
|
|
21
|
+
- Title → H1 and `<title>` tag
|
|
22
|
+
- Custom fields → template variables (city, service, price, etc.)
|
|
23
|
+
- Taxonomy → category/tag for clustering
|
|
24
|
+
- Featured image → OG image
|
|
25
|
+
|
|
26
|
+
## URL Structure Design
|
|
27
|
+
|
|
28
|
+
| Pattern | Example | Use Case |
|
|
29
|
+
|---------|---------|----------|
|
|
30
|
+
| `/{service}/{city}` | `/plumbing/miami` | Service-area pages |
|
|
31
|
+
| `/{product}/{variant}` | `/shoes/red-size-10` | Product variants |
|
|
32
|
+
| `/{category}/{item}` | `/restaurants/joes-pizza` | Directory listings |
|
|
33
|
+
| `/{brand}/{model}/{year}` | `/toyota/camry/2024` | Multi-attribute pages |
|
|
34
|
+
| `/{topic}/{subtopic}` | `/learn/react-hooks` | Educational content |
|
|
35
|
+
|
|
36
|
+
**URL rules:**
|
|
37
|
+
- Lowercase, hyphenated slugs
|
|
38
|
+
- Max 3 levels deep (SEO best practice)
|
|
39
|
+
- Include primary keyword in first path segment
|
|
40
|
+
- Avoid query parameters for indexable pages
|
|
41
|
+
|
|
42
|
+
## ISR Configuration (Headless Frontend)
|
|
43
|
+
|
|
44
|
+
### Next.js (App Router)
|
|
45
|
+
|
|
46
|
+
```javascript
|
|
47
|
+
// app/[service]/[city]/page.js
|
|
48
|
+
export async function generateStaticParams() {
|
|
49
|
+
const res = await fetch(`${WP_API}/wp/v2/location?per_page=100`);
|
|
50
|
+
const locations = await res.json();
|
|
51
|
+
return locations.map((loc) => ({
|
|
52
|
+
service: loc.acf.service_slug,
|
|
53
|
+
city: loc.slug,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const revalidate = 3600; // ISR: revalidate every hour
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Nuxt
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
// nuxt.config.ts
|
|
64
|
+
export default defineNuxtConfig({
|
|
65
|
+
routeRules: {
|
|
66
|
+
'/services/**': { isr: 3600 },
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Astro
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
// astro.config.mjs — hybrid mode for ISR-like behavior
|
|
75
|
+
export default defineConfig({
|
|
76
|
+
output: 'hybrid',
|
|
77
|
+
adapter: node({ mode: 'standalone' }),
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Revalidate intervals by content type:**
|
|
82
|
+
|
|
83
|
+
| Content Type | Interval | Reason |
|
|
84
|
+
|-------------|----------|--------|
|
|
85
|
+
| Location pages | 3600s (1h) | Rarely change |
|
|
86
|
+
| Product pages | 900s (15m) | Price/stock updates |
|
|
87
|
+
| Blog/guides | 86400s (24h) | Stable content |
|
|
88
|
+
| Comparison pages | 3600s (1h) | Data-driven, periodic |
|
|
89
|
+
|
|
90
|
+
## Template Variables
|
|
91
|
+
|
|
92
|
+
A programmatic page template maps CPT fields to HTML:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
Title: "{service_name} in {city_name} — {brand}"
|
|
96
|
+
Meta desc: "Professional {service_name} in {city_name}. {unique_selling_point}."
|
|
97
|
+
H1: "{service_name} in {city_name}"
|
|
98
|
+
Body: Template paragraph using {city_population}, {service_details}, {local_info}
|
|
99
|
+
Schema: LocalBusiness JSON-LD with {name}, {address}, {phone}, {geo}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Anti-pattern:** Avoid thin content — each page must have enough unique value to justify indexing. Minimum 300 words of meaningful, varied content per page.
|
|
103
|
+
|
|
104
|
+
## Bulk Content Creation Workflow
|
|
105
|
+
|
|
106
|
+
1. **Prepare data:** CSV or JSON with one row per page (city, service, attributes)
|
|
107
|
+
2. **Create CPT entries:** Loop via WordPress REST API:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# Using wp-rest-bridge create_content tool
|
|
111
|
+
for each row in data:
|
|
112
|
+
create_content(type="location", title=row.title, content=row.body, meta=row.fields)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
3. **Assign taxonomies:** Tag each entry with relevant terms for clustering
|
|
116
|
+
4. **Generate frontend paths:** Run `generateStaticParams` or equivalent build step
|
|
117
|
+
5. **Verify:** Spot-check 5–10 pages for content quality and schema validity
|
|
118
|
+
|
|
119
|
+
## Decision Checklist
|
|
120
|
+
|
|
121
|
+
1. Is the data structured and repeatable? → Yes = proceed with CPT
|
|
122
|
+
2. Will each page have unique, valuable content? → Yes = proceed; No = consolidate
|
|
123
|
+
3. Is the URL pattern SEO-friendly and max 3 levels? → Verify before building
|
|
124
|
+
4. Is ISR/SSG configured with appropriate revalidation? → Match to content freshness
|
|
125
|
+
5. Are sitemaps generated for all programmatic pages? → See `technical-seo.md`
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* programmatic_seo_inspect.mjs — Detect programmatic SEO readiness.
|
|
3
|
+
*
|
|
4
|
+
* Scans for headless frontend, SEO plugins, content volume, custom post types,
|
|
5
|
+
* and WPGraphQL availability to assess readiness for programmatic page generation.
|
|
6
|
+
* Outputs a JSON report to stdout.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node programmatic_seo_inspect.mjs [--cwd=/path/to/check]
|
|
10
|
+
*
|
|
11
|
+
* Exit codes:
|
|
12
|
+
* 0 — programmatic SEO indicators detected
|
|
13
|
+
* 1 — no programmatic SEO 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 headless frontend
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
function detectHeadlessFrontend(cwd) {
|
|
83
|
+
const result = { detected: false, framework: null, location: null };
|
|
84
|
+
|
|
85
|
+
const frontendDirs = ["frontend", "client", "app", "web", "next", "nuxt", "."];
|
|
86
|
+
for (const dir of frontendDirs) {
|
|
87
|
+
const pkgPath = path.join(cwd, dir, "package.json");
|
|
88
|
+
const pkg = readJsonSafe(pkgPath);
|
|
89
|
+
if (!pkg) continue;
|
|
90
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
91
|
+
if (allDeps["next"]) { result.detected = true; result.framework = "Next.js"; result.location = dir; return result; }
|
|
92
|
+
if (allDeps["nuxt"] || allDeps["nuxt3"]) { result.detected = true; result.framework = "Nuxt"; result.location = dir; return result; }
|
|
93
|
+
if (allDeps["astro"]) { result.detected = true; result.framework = "Astro"; result.location = dir; return result; }
|
|
94
|
+
if (allDeps["gatsby"]) { result.detected = true; result.framework = "Gatsby"; result.location = dir; return result; }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check for config files directly
|
|
98
|
+
const configFiles = [
|
|
99
|
+
{ file: "next.config.js", fw: "Next.js" },
|
|
100
|
+
{ file: "next.config.mjs", fw: "Next.js" },
|
|
101
|
+
{ file: "next.config.ts", fw: "Next.js" },
|
|
102
|
+
{ file: "nuxt.config.ts", fw: "Nuxt" },
|
|
103
|
+
{ file: "nuxt.config.js", fw: "Nuxt" },
|
|
104
|
+
{ file: "astro.config.mjs", fw: "Astro" },
|
|
105
|
+
{ file: "astro.config.ts", fw: "Astro" },
|
|
106
|
+
];
|
|
107
|
+
for (const { file, fw } of configFiles) {
|
|
108
|
+
if (statSafe(path.join(cwd, file))) {
|
|
109
|
+
result.detected = true;
|
|
110
|
+
result.framework = fw;
|
|
111
|
+
result.location = ".";
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Detect SEO plugins
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
function detectSeoPlugin(cwd) {
|
|
124
|
+
const result = { detected: false, plugin: null };
|
|
125
|
+
|
|
126
|
+
const pluginsDir = path.join(cwd, "wp-content", "plugins");
|
|
127
|
+
const seoPlugins = [
|
|
128
|
+
{ dir: "wordpress-seo", name: "Yoast SEO" },
|
|
129
|
+
{ dir: "seo-by-rank-math", name: "Rank Math" },
|
|
130
|
+
{ dir: "all-in-one-seo-pack", name: "All in One SEO" },
|
|
131
|
+
{ dir: "the-seo-framework-extension-manager", name: "The SEO Framework" },
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
for (const plugin of seoPlugins) {
|
|
135
|
+
if (statSafe(path.join(pluginsDir, plugin.dir))?.isDirectory()) {
|
|
136
|
+
result.detected = true;
|
|
137
|
+
result.plugin = plugin.name;
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Detect content counts by type
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
function detectContentCounts(cwd) {
|
|
150
|
+
const result = { posts: 0, pages: 0, products: 0, custom_post_types: [] };
|
|
151
|
+
|
|
152
|
+
// Try WP-CLI for accurate counts
|
|
153
|
+
const postCount = execSafe("wp post list --post_type=post --post_status=publish --format=count 2>/dev/null", cwd);
|
|
154
|
+
if (postCount) result.posts = parseInt(postCount) || 0;
|
|
155
|
+
|
|
156
|
+
const pageCount = execSafe("wp post list --post_type=page --post_status=publish --format=count 2>/dev/null", cwd);
|
|
157
|
+
if (pageCount) result.pages = parseInt(pageCount) || 0;
|
|
158
|
+
|
|
159
|
+
const productCount = execSafe("wp post list --post_type=product --post_status=publish --format=count 2>/dev/null", cwd);
|
|
160
|
+
if (productCount) result.products = parseInt(productCount) || 0;
|
|
161
|
+
|
|
162
|
+
// Detect custom post types via WP-CLI
|
|
163
|
+
const cptList = execSafe("wp post-type list --_builtin=0 --format=json 2>/dev/null", cwd);
|
|
164
|
+
if (cptList) {
|
|
165
|
+
try {
|
|
166
|
+
const cpts = JSON.parse(cptList);
|
|
167
|
+
result.custom_post_types = cpts.map((c) => c.name || c);
|
|
168
|
+
} catch { /* ignore parse errors */ }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Detect WPGraphQL
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
function detectWPGraphQL(cwd) {
|
|
179
|
+
const pluginsDir = path.join(cwd, "wp-content", "plugins");
|
|
180
|
+
if (statSafe(path.join(pluginsDir, "wp-graphql"))?.isDirectory()) return true;
|
|
181
|
+
|
|
182
|
+
const composer = readJsonSafe(path.join(cwd, "composer.json"));
|
|
183
|
+
if (composer) {
|
|
184
|
+
const allDeps = { ...composer.require, ...composer["require-dev"] };
|
|
185
|
+
if (allDeps["wp-graphql/wp-graphql"]) return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Main
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
function main() {
|
|
196
|
+
const cwd = parseCwd();
|
|
197
|
+
|
|
198
|
+
if (!statSafe(cwd)?.isDirectory()) {
|
|
199
|
+
console.error(`Error: directory not found: ${cwd}`);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const headless = detectHeadlessFrontend(cwd);
|
|
204
|
+
const seo = detectSeoPlugin(cwd);
|
|
205
|
+
const content = detectContentCounts(cwd);
|
|
206
|
+
const hasWpgraphql = detectWPGraphQL(cwd);
|
|
207
|
+
const hasCustomPostTypes = content.custom_post_types.length > 0;
|
|
208
|
+
|
|
209
|
+
const detected = headless.detected || seo.detected || hasCustomPostTypes;
|
|
210
|
+
|
|
211
|
+
const report = {
|
|
212
|
+
tool: "programmatic_seo_inspect",
|
|
213
|
+
version: TOOL_VERSION,
|
|
214
|
+
cwd,
|
|
215
|
+
detected,
|
|
216
|
+
has_headless_frontend: headless.detected,
|
|
217
|
+
headless_framework: headless.framework,
|
|
218
|
+
headless_location: headless.location,
|
|
219
|
+
seo_plugin: seo.plugin,
|
|
220
|
+
content_counts: {
|
|
221
|
+
posts: content.posts,
|
|
222
|
+
pages: content.pages,
|
|
223
|
+
products: content.products,
|
|
224
|
+
},
|
|
225
|
+
has_custom_post_types: hasCustomPostTypes,
|
|
226
|
+
custom_post_types: content.custom_post_types,
|
|
227
|
+
has_wpgraphql: hasWpgraphql,
|
|
228
|
+
programmatic_seo_readiness: "unknown",
|
|
229
|
+
recommendations: [],
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Assess readiness
|
|
233
|
+
if (headless.detected && seo.detected && hasCustomPostTypes) {
|
|
234
|
+
report.programmatic_seo_readiness = "high";
|
|
235
|
+
} else if (headless.detected || (seo.detected && hasCustomPostTypes)) {
|
|
236
|
+
report.programmatic_seo_readiness = "medium";
|
|
237
|
+
} else if (seo.detected) {
|
|
238
|
+
report.programmatic_seo_readiness = "low";
|
|
239
|
+
} else {
|
|
240
|
+
report.programmatic_seo_readiness = "not_ready";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Recommendations
|
|
244
|
+
if (!headless.detected) {
|
|
245
|
+
report.recommendations.push("No headless frontend detected. Install Next.js, Nuxt, or Astro for ISR/SSG page rendering.");
|
|
246
|
+
}
|
|
247
|
+
if (!seo.detected) {
|
|
248
|
+
report.recommendations.push("No SEO plugin detected. Install Yoast SEO or Rank Math for sitemap generation and meta management.");
|
|
249
|
+
}
|
|
250
|
+
if (!hasCustomPostTypes) {
|
|
251
|
+
report.recommendations.push("No custom post types found. Create CPTs to serve as structured data sources for programmatic pages.");
|
|
252
|
+
}
|
|
253
|
+
if (!hasWpgraphql && headless.detected) {
|
|
254
|
+
report.recommendations.push("WPGraphQL not installed. Consider it for efficient batch data fetching from headless frontend.");
|
|
255
|
+
}
|
|
256
|
+
if (content.posts === 0 && content.pages === 0 && content.products === 0) {
|
|
257
|
+
report.recommendations.push("No published content found. Create initial content or verify WP-CLI access for accurate counts.");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
console.log(JSON.stringify(report, null, 2));
|
|
261
|
+
process.exit(detected ? 0 : 1);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
main();
|
|
@@ -109,3 +109,4 @@ For complex multi-step WooCommerce operations, use the `wp-ecommerce-manager` ag
|
|
|
109
109
|
- `wp-audit` — Audit WooCommerce store security and performance
|
|
110
110
|
- `wp-backup` — Backup WooCommerce database and uploads
|
|
111
111
|
- `wp-webhooks` — WooCommerce webhook management (order/product/customer event notifications)
|
|
112
|
+
- `wp-content-attribution` — Content-commerce attribution, UTM tracking, revenue per content piece
|