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,202 @@
|
|
|
1
|
+
# ROI Calculation
|
|
2
|
+
|
|
3
|
+
Use this file when calculating content return on investment — revenue per post, content ROI formula, customer acquisition cost, and lifetime value by content source.
|
|
4
|
+
|
|
5
|
+
## Revenue Per Post
|
|
6
|
+
|
|
7
|
+
The most straightforward content attribution metric.
|
|
8
|
+
|
|
9
|
+
**Formula:**
|
|
10
|
+
```
|
|
11
|
+
Revenue Per Post = Total attributed revenue / Number of posts
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
**Per-post attribution (last-touch):**
|
|
15
|
+
```
|
|
16
|
+
Revenue(Post X) = SUM(order_total) WHERE _last_utm_campaign = "post-x-slug"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Per-post attribution (first-touch):**
|
|
20
|
+
```
|
|
21
|
+
Revenue(Post X) = SUM(order_total) WHERE _first_utm_campaign = "post-x-slug"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Calculating with WooCommerce MCP Tools
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Step 1: Get completed orders for the period
|
|
28
|
+
wc_list_orders(status="completed", after="2025-01-01", before="2025-01-31", per_page=100)
|
|
29
|
+
|
|
30
|
+
# Step 2: For each order, read utm meta
|
|
31
|
+
# Group orders by _last_utm_campaign value
|
|
32
|
+
# Sum order_total per campaign (campaign = post slug)
|
|
33
|
+
|
|
34
|
+
# Step 3: Get content list for the same period
|
|
35
|
+
list_content(type="post", status="publish", after="2025-01-01", before="2025-01-31")
|
|
36
|
+
|
|
37
|
+
# Step 4: Match campaign slugs to post titles
|
|
38
|
+
# Result: { post_title: "Cactus Water Benefits", slug: "cactus-water-benefits", revenue: 2500, orders: 15 }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Revenue Per Post Benchmarks
|
|
42
|
+
|
|
43
|
+
| Content Type | Typical Revenue/Post | Notes |
|
|
44
|
+
|-------------|---------------------|-------|
|
|
45
|
+
| Product review | $500–$5,000 | High intent, bottom-of-funnel |
|
|
46
|
+
| How-to guide | $100–$1,000 | Indirect conversion, builds trust |
|
|
47
|
+
| Comparison post | $1,000–$10,000 | Very high intent, decision-stage |
|
|
48
|
+
| Category guide | $200–$2,000 | Mid-funnel, product discovery |
|
|
49
|
+
| News/update | $50–$500 | Low intent, brand awareness |
|
|
50
|
+
|
|
51
|
+
## Content ROI Formula
|
|
52
|
+
|
|
53
|
+
**Formula:**
|
|
54
|
+
```
|
|
55
|
+
Content ROI = (Revenue - Content Cost) / Content Cost × 100%
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Example:**
|
|
59
|
+
```
|
|
60
|
+
Blog post cost: $200 (writer) + $50 (images) + $30 (editing) = $280
|
|
61
|
+
Revenue attributed: $1,400 (last-touch, 3 months)
|
|
62
|
+
Content ROI: ($1,400 - $280) / $280 × 100% = 400% ROI
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Content Cost Components
|
|
66
|
+
|
|
67
|
+
| Component | Typical Cost | How to Track |
|
|
68
|
+
|-----------|-------------|--------------|
|
|
69
|
+
| Writing | $0.10–$0.50/word | Per-post invoice or hourly rate |
|
|
70
|
+
| Editing | $25–$100/post | Editor time tracking |
|
|
71
|
+
| Images/media | $10–$100/post | Stock photo licenses, design time |
|
|
72
|
+
| SEO optimization | $25–$75/post | SEO tool costs + specialist time |
|
|
73
|
+
| Publishing/formatting | $15–$30/post | CMS time |
|
|
74
|
+
| **Total typical cost** | **$100–$500/post** | Sum all components |
|
|
75
|
+
|
|
76
|
+
### ROI by Content Category
|
|
77
|
+
|
|
78
|
+
Track ROI by category to identify the most profitable content pillars:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
Category Posts Total Cost Total Revenue ROI
|
|
82
|
+
──────────────────────────────────────────────────────
|
|
83
|
+
Product reviews 10 $3,000 $25,000 733%
|
|
84
|
+
How-to guides 20 $4,000 $8,000 100%
|
|
85
|
+
Company news 15 $1,500 $750 -50%
|
|
86
|
+
Case studies 5 $2,500 $12,000 380%
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Action:** Double down on product reviews and case studies; reduce company news investment.
|
|
90
|
+
|
|
91
|
+
## Customer Acquisition Cost (CAC) by Content Type
|
|
92
|
+
|
|
93
|
+
**Formula:**
|
|
94
|
+
```
|
|
95
|
+
CAC = Total content investment / New customers acquired from content
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**By content type:**
|
|
99
|
+
```
|
|
100
|
+
CAC(blog) = Blog content costs / New customers with _first_utm_source = "blog"
|
|
101
|
+
CAC(email) = Email costs / New customers with _first_utm_source = "newsletter"
|
|
102
|
+
CAC(social) = Social costs / New customers with _first_utm_source = "facebook|instagram"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### CAC Benchmarks (E-commerce)
|
|
106
|
+
|
|
107
|
+
| Channel | Typical CAC | Notes |
|
|
108
|
+
|---------|------------|-------|
|
|
109
|
+
| Organic blog content | $20–$80 | Lower CAC, longer to build |
|
|
110
|
+
| Email newsletter | $10–$40 | Lowest CAC, requires list |
|
|
111
|
+
| Paid search (Google) | $30–$100 | Immediate but expensive |
|
|
112
|
+
| Social media (organic) | $25–$75 | Variable, platform-dependent |
|
|
113
|
+
| Social media (paid) | $15–$60 | Scalable with budget |
|
|
114
|
+
|
|
115
|
+
## Lifetime Value (LTV) by Acquisition Source
|
|
116
|
+
|
|
117
|
+
**Formula:**
|
|
118
|
+
```
|
|
119
|
+
LTV = Average Order Value × Purchase Frequency × Customer Lifespan
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**By source:**
|
|
123
|
+
```
|
|
124
|
+
LTV(blog) = AOV(blog customers) × Frequency(blog customers) × Lifespan(blog customers)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Calculating with WooCommerce Data
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# Get top sellers to understand AOV patterns
|
|
131
|
+
wc_get_top_sellers(period="year")
|
|
132
|
+
|
|
133
|
+
# Get sales report for overall metrics
|
|
134
|
+
wc_get_sales_report(period="year")
|
|
135
|
+
# Returns: total_sales, total_orders → AOV = total_sales / total_orders
|
|
136
|
+
|
|
137
|
+
# For source-specific LTV:
|
|
138
|
+
# 1. Filter orders by _first_utm_source
|
|
139
|
+
# 2. Group by customer_id
|
|
140
|
+
# 3. Calculate per-customer: total_spent, order_count, first_order_date, last_order_date
|
|
141
|
+
# 4. Average across customers from that source
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### LTV:CAC Ratio
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
Healthy ratio: LTV:CAC > 3:1
|
|
148
|
+
|
|
149
|
+
Example:
|
|
150
|
+
Blog customers: LTV = $240, CAC = $50 → Ratio = 4.8:1 ✓ Excellent
|
|
151
|
+
Paid ad customers: LTV = $180, CAC = $80 → Ratio = 2.25:1 ⚠ Below target
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Action:** If LTV:CAC < 3:1, either reduce acquisition cost or increase customer retention/upsell.
|
|
155
|
+
|
|
156
|
+
## Content Efficiency Metrics
|
|
157
|
+
|
|
158
|
+
### Revenue Per Word
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
Revenue Per Word = Total attributed revenue / Total words published
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Useful for comparing content formats: long-form guides vs short product reviews.
|
|
165
|
+
|
|
166
|
+
### Revenue Per Topic
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
Revenue Per Topic = SUM(revenue of posts in topic cluster) / Number of posts in cluster
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Identifies which topic clusters are most commercially valuable.
|
|
173
|
+
|
|
174
|
+
### Time to ROI
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
Time to ROI = Days from publish date to break-even (revenue ≥ content cost)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Short time to ROI = content with immediate commercial intent (product reviews).
|
|
181
|
+
Long time to ROI = evergreen SEO content (compounds over months).
|
|
182
|
+
|
|
183
|
+
## Using WC Reports for Date Correlation
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
# Monthly sales report
|
|
187
|
+
wc_get_sales_report(date_min="2025-03-01", date_max="2025-03-31")
|
|
188
|
+
|
|
189
|
+
# Top-selling products in the period
|
|
190
|
+
wc_get_top_sellers(date_min="2025-03-01", date_max="2025-03-31")
|
|
191
|
+
|
|
192
|
+
# Cross-reference with content published before the period:
|
|
193
|
+
list_content(type="post", status="publish", before="2025-03-31", orderby="date", order="desc", per_page=20)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Decision Checklist
|
|
197
|
+
|
|
198
|
+
1. Is content cost tracked per post? → Set up cost tracking (spreadsheet or custom field)
|
|
199
|
+
2. Is revenue attributed to individual posts via UTM? → Verify mu-plugin capturing data
|
|
200
|
+
3. Is CAC calculated by acquisition source? → Group orders by first-touch source
|
|
201
|
+
4. Is LTV:CAC ratio above 3:1? → If not, optimize acquisition or retention
|
|
202
|
+
5. Are content investments being shifted to highest-ROI categories? → Review quarterly
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# UTM Tracking Setup
|
|
2
|
+
|
|
3
|
+
Use this file when setting up UTM parameter capture for WooCommerce orders — parameter architecture, mu-plugin installation, naming conventions, and verification.
|
|
4
|
+
|
|
5
|
+
## UTM Parameter Architecture
|
|
6
|
+
|
|
7
|
+
| Parameter | Purpose | Example |
|
|
8
|
+
|-----------|---------|---------|
|
|
9
|
+
| `utm_source` | Where traffic originates | `blog`, `newsletter`, `google`, `facebook` |
|
|
10
|
+
| `utm_medium` | Marketing channel type | `organic`, `email`, `cpc`, `social`, `referral` |
|
|
11
|
+
| `utm_campaign` | Specific campaign name | `spring-sale-2025`, `product-launch-x` |
|
|
12
|
+
| `utm_content` | Differentiates ad/link variants | `cta-button`, `sidebar-banner`, `post-footer` |
|
|
13
|
+
| `utm_term` | Paid search keywords | `cactus-water-buy`, `zero-calorie-drink` |
|
|
14
|
+
|
|
15
|
+
## mu-plugin Pattern: Capture UTM on Checkout
|
|
16
|
+
|
|
17
|
+
Create a mu-plugin to automatically capture UTM parameters from the visitor session and store them as WooCommerce order meta:
|
|
18
|
+
|
|
19
|
+
```php
|
|
20
|
+
<?php
|
|
21
|
+
/**
|
|
22
|
+
* Plugin Name: UTM Order Attribution
|
|
23
|
+
* Description: Captures UTM parameters from visitor session and stores as order meta.
|
|
24
|
+
* Version: 1.0.0
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// Store UTM params in session cookie on first visit
|
|
28
|
+
add_action('init', function () {
|
|
29
|
+
if (!is_admin() && !wp_doing_cron()) {
|
|
30
|
+
$utm_params = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'];
|
|
31
|
+
foreach ($utm_params as $param) {
|
|
32
|
+
if (isset($_GET[$param]) && !empty($_GET[$param])) {
|
|
33
|
+
// Store in cookie for 30 days (first-touch attribution)
|
|
34
|
+
$cookie_name = '_wc_' . $param;
|
|
35
|
+
if (!isset($_COOKIE[$cookie_name])) {
|
|
36
|
+
setcookie($cookie_name, sanitize_text_field($_GET[$param]), time() + (30 * DAY_IN_SECONDS), '/');
|
|
37
|
+
}
|
|
38
|
+
// Always update last-touch cookie
|
|
39
|
+
setcookie('_wc_last_' . $param, sanitize_text_field($_GET[$param]), time() + (30 * DAY_IN_SECONDS), '/');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Save UTM data to order meta on checkout
|
|
46
|
+
add_action('woocommerce_checkout_order_processed', function ($order_id) {
|
|
47
|
+
$order = wc_get_order($order_id);
|
|
48
|
+
if (!$order) return;
|
|
49
|
+
|
|
50
|
+
$utm_params = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'];
|
|
51
|
+
foreach ($utm_params as $param) {
|
|
52
|
+
// Save first-touch
|
|
53
|
+
$first_cookie = '_wc_' . $param;
|
|
54
|
+
if (isset($_COOKIE[$first_cookie]) && !empty($_COOKIE[$first_cookie])) {
|
|
55
|
+
$order->update_meta_data('_first_' . $param, sanitize_text_field($_COOKIE[$first_cookie]));
|
|
56
|
+
}
|
|
57
|
+
// Save last-touch
|
|
58
|
+
$last_cookie = '_wc_last_' . $param;
|
|
59
|
+
if (isset($_COOKIE[$last_cookie]) && !empty($_COOKIE[$last_cookie])) {
|
|
60
|
+
$order->update_meta_data('_last_' . $param, sanitize_text_field($_COOKIE[$last_cookie]));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Save landing page URL (first page visited)
|
|
65
|
+
if (isset($_COOKIE['_wc_landing_page'])) {
|
|
66
|
+
$order->update_meta_data('_landing_page', sanitize_url($_COOKIE['_wc_landing_page']));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
$order->save();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Track landing page
|
|
73
|
+
add_action('init', function () {
|
|
74
|
+
if (!is_admin() && !wp_doing_cron() && !isset($_COOKIE['_wc_landing_page'])) {
|
|
75
|
+
$landing = esc_url_raw((isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
|
|
76
|
+
setcookie('_wc_landing_page', $landing, time() + (30 * DAY_IN_SECONDS), '/');
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Installation:**
|
|
82
|
+
1. Save as `wp-content/mu-plugins/utm-order-attribution.php`
|
|
83
|
+
2. mu-plugins load automatically — no activation needed
|
|
84
|
+
3. Verify by placing a test order with UTM params in the URL
|
|
85
|
+
|
|
86
|
+
## UTM Naming Conventions
|
|
87
|
+
|
|
88
|
+
Consistent naming is critical for accurate attribution. Adopt these rules:
|
|
89
|
+
|
|
90
|
+
| Rule | Good | Bad |
|
|
91
|
+
|------|------|-----|
|
|
92
|
+
| Lowercase always | `utm_source=blog` | `utm_source=Blog` |
|
|
93
|
+
| Hyphens for spaces | `spring-sale-2025` | `spring_sale_2025` or `spring sale` |
|
|
94
|
+
| No special chars | `product-launch` | `product_launch!` |
|
|
95
|
+
| Consistent source names | `newsletter` (always) | `email`, `newsletter`, `mail` (mixed) |
|
|
96
|
+
| Date in campaigns | `black-friday-2025` | `black-friday` (ambiguous year) |
|
|
97
|
+
|
|
98
|
+
**Recommended source taxonomy:**
|
|
99
|
+
|
|
100
|
+
| Source | Medium | When to Use |
|
|
101
|
+
|--------|--------|-------------|
|
|
102
|
+
| `blog` | `organic` | Internal blog post links to products |
|
|
103
|
+
| `blog` | `cta` | Blog post CTA buttons to products |
|
|
104
|
+
| `newsletter` | `email` | Email newsletter links |
|
|
105
|
+
| `google` | `cpc` | Google Ads campaigns |
|
|
106
|
+
| `facebook` | `social` | Facebook organic or paid posts |
|
|
107
|
+
| `instagram` | `social` | Instagram bio/stories links |
|
|
108
|
+
| `partner-name` | `referral` | Partner/affiliate links |
|
|
109
|
+
|
|
110
|
+
## Internal Link UTM Tagging
|
|
111
|
+
|
|
112
|
+
Tag all blog-to-product links with UTMs:
|
|
113
|
+
|
|
114
|
+
```html
|
|
115
|
+
<!-- Blog post CTA linking to product -->
|
|
116
|
+
<a href="/product/cactus-water/?utm_source=blog&utm_medium=cta&utm_campaign=cactus-water-benefits&utm_content=post-footer-button">
|
|
117
|
+
Buy Cactus Water
|
|
118
|
+
</a>
|
|
119
|
+
|
|
120
|
+
<!-- Sidebar widget linking to product -->
|
|
121
|
+
<a href="/product/cactus-water/?utm_source=blog&utm_medium=sidebar&utm_campaign=always-on&utm_content=product-widget">
|
|
122
|
+
Try Cactus Water
|
|
123
|
+
</a>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Automated tagging approach:** Use a WordPress filter to append UTMs to all internal product links in post content:
|
|
127
|
+
|
|
128
|
+
```php
|
|
129
|
+
add_filter('the_content', function ($content) {
|
|
130
|
+
if (!is_singular('post')) return $content;
|
|
131
|
+
$post_slug = get_post_field('post_name', get_the_ID());
|
|
132
|
+
// Append UTM to internal /product/ links that don't already have UTM
|
|
133
|
+
$content = preg_replace_callback(
|
|
134
|
+
'#href="(/product/[^"]*?)(?<!\?[^"]*utm_source[^"]*)"#',
|
|
135
|
+
function ($matches) use ($post_slug) {
|
|
136
|
+
$sep = strpos($matches[1], '?') !== false ? '&' : '?';
|
|
137
|
+
return 'href="' . $matches[1] . $sep . 'utm_source=blog&utm_medium=organic&utm_campaign=' . $post_slug . '"';
|
|
138
|
+
},
|
|
139
|
+
$content
|
|
140
|
+
);
|
|
141
|
+
return $content;
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Verification
|
|
146
|
+
|
|
147
|
+
After installing the mu-plugin:
|
|
148
|
+
|
|
149
|
+
1. **Test visit:** Navigate to `yoursite.com/product/example/?utm_source=test&utm_medium=test&utm_campaign=test`
|
|
150
|
+
2. **Place test order** through WooCommerce checkout
|
|
151
|
+
3. **Check order meta:** Use `wc_list_orders` MCP tool or WooCommerce admin → Orders → order details
|
|
152
|
+
4. **Verify fields:** `_first_utm_source`, `_last_utm_source`, `_first_utm_campaign`, etc. should be populated
|
|
153
|
+
5. **Check cookies:** Browser dev tools → Application → Cookies → look for `_wc_utm_*` cookies
|
|
154
|
+
|
|
155
|
+
## Decision Checklist
|
|
156
|
+
|
|
157
|
+
1. Is the mu-plugin installed in `wp-content/mu-plugins/`? → Verify file exists
|
|
158
|
+
2. Are UTM naming conventions documented for the team? → Share taxonomy table
|
|
159
|
+
3. Are internal blog→product links tagged with UTMs? → Audit sample posts
|
|
160
|
+
4. Has a test order been placed with UTM params to verify capture? → Must pass before going live
|
|
161
|
+
5. Is cookie duration appropriate (30 days default)? → Adjust for sales cycle length
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* attribution_inspect.mjs — Detect content-commerce attribution readiness.
|
|
3
|
+
*
|
|
4
|
+
* Scans for WooCommerce presence, analytics plugins, UTM tracking setup,
|
|
5
|
+
* content/product volume, and existing order meta with source fields.
|
|
6
|
+
* Outputs a JSON report to stdout.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node attribution_inspect.mjs [--cwd=/path/to/check]
|
|
10
|
+
*
|
|
11
|
+
* Exit codes:
|
|
12
|
+
* 0 — attribution indicators detected
|
|
13
|
+
* 1 — no attribution 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 WooCommerce
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
function detectWooCommerce(cwd) {
|
|
83
|
+
const pluginsDir = path.join(cwd, "wp-content", "plugins");
|
|
84
|
+
if (statSafe(path.join(pluginsDir, "woocommerce"))?.isDirectory()) return true;
|
|
85
|
+
|
|
86
|
+
const composer = readJsonSafe(path.join(cwd, "composer.json"));
|
|
87
|
+
if (composer) {
|
|
88
|
+
const allDeps = { ...composer.require, ...composer["require-dev"] };
|
|
89
|
+
if (allDeps["woocommerce/woocommerce"] || allDeps["wpackagist-plugin/woocommerce"]) return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Detect analytics plugins
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
function detectAnalyticsPlugin(cwd) {
|
|
100
|
+
const result = { detected: false, plugin: null };
|
|
101
|
+
|
|
102
|
+
const pluginsDir = path.join(cwd, "wp-content", "plugins");
|
|
103
|
+
const analyticsPlugins = [
|
|
104
|
+
{ dir: "google-analytics-for-wordpress", name: "MonsterInsights" },
|
|
105
|
+
{ dir: "google-site-kit", name: "Google Site Kit" },
|
|
106
|
+
{ dir: "woocommerce-google-analytics-integration", name: "WooCommerce Google Analytics" },
|
|
107
|
+
{ dir: "ga-google-analytics", name: "GA Google Analytics" },
|
|
108
|
+
{ dir: "analytify", name: "Analytify" },
|
|
109
|
+
{ dir: "matomo", name: "Matomo Analytics" },
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
for (const plugin of analyticsPlugins) {
|
|
113
|
+
if (statSafe(path.join(pluginsDir, plugin.dir))?.isDirectory()) {
|
|
114
|
+
result.detected = true;
|
|
115
|
+
result.plugin = plugin.name;
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Detect UTM tracking
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
function detectUtmTracking(cwd) {
|
|
128
|
+
const result = { detected: false, sources: [] };
|
|
129
|
+
|
|
130
|
+
// Check mu-plugins for UTM capture
|
|
131
|
+
const muPluginsDir = path.join(cwd, "wp-content", "mu-plugins");
|
|
132
|
+
const muFiles = readdirSafe(muPluginsDir);
|
|
133
|
+
for (const file of muFiles) {
|
|
134
|
+
if (!file.endsWith(".php")) continue;
|
|
135
|
+
const content = readFileSafe(path.join(muPluginsDir, file));
|
|
136
|
+
if (content && /utm_source|utm_campaign|utm_medium/i.test(content)) {
|
|
137
|
+
result.detected = true;
|
|
138
|
+
result.sources.push(`mu-plugin: ${file}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check plugins for UTM tracking
|
|
143
|
+
const pluginsDir = path.join(cwd, "wp-content", "plugins");
|
|
144
|
+
const utmPlugins = [
|
|
145
|
+
{ dir: "utm-dot-io", name: "UTM.io" },
|
|
146
|
+
{ dir: "campaign-url-builder", name: "Campaign URL Builder" },
|
|
147
|
+
{ dir: "leadin", name: "HubSpot (UTM tracking)" },
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
for (const plugin of utmPlugins) {
|
|
151
|
+
if (statSafe(path.join(pluginsDir, plugin.dir))?.isDirectory()) {
|
|
152
|
+
result.detected = true;
|
|
153
|
+
result.sources.push(`plugin: ${plugin.name}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check theme functions.php for UTM capture
|
|
158
|
+
const themesDir = path.join(cwd, "wp-content", "themes");
|
|
159
|
+
const themes = readdirSafe(themesDir);
|
|
160
|
+
for (const theme of themes) {
|
|
161
|
+
const functionsPhp = readFileSafe(path.join(themesDir, theme, "functions.php"));
|
|
162
|
+
if (functionsPhp && /utm_source|utm_campaign/i.test(functionsPhp)) {
|
|
163
|
+
result.detected = true;
|
|
164
|
+
result.sources.push(`theme: ${theme}/functions.php`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Detect content volume
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
function detectContentVolume(cwd) {
|
|
176
|
+
const result = { content_count: 0, product_count: 0 };
|
|
177
|
+
|
|
178
|
+
const postCount = execSafe("wp post list --post_type=post --post_status=publish --format=count 2>/dev/null", cwd);
|
|
179
|
+
if (postCount) result.content_count = parseInt(postCount) || 0;
|
|
180
|
+
|
|
181
|
+
const productCount = execSafe("wp post list --post_type=product --post_status=publish --format=count 2>/dev/null", cwd);
|
|
182
|
+
if (productCount) result.product_count = parseInt(productCount) || 0;
|
|
183
|
+
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Detect existing order meta with source fields
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
function detectOrderMeta(cwd) {
|
|
192
|
+
// Check if any completed orders have UTM meta
|
|
193
|
+
const orderMeta = execSafe(
|
|
194
|
+
`wp db query "SELECT COUNT(*) as c FROM $(wp db prefix 2>/dev/null)postmeta WHERE meta_key LIKE '%utm_source%'" --format=csv 2>/dev/null`,
|
|
195
|
+
cwd
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
if (orderMeta && /\d+/.test(orderMeta)) {
|
|
199
|
+
const count = parseInt(orderMeta.match(/\d+/)[0]);
|
|
200
|
+
return count > 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Main
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
function main() {
|
|
211
|
+
const cwd = parseCwd();
|
|
212
|
+
|
|
213
|
+
if (!statSafe(cwd)?.isDirectory()) {
|
|
214
|
+
console.error(`Error: directory not found: ${cwd}`);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const hasWoocommerce = detectWooCommerce(cwd);
|
|
219
|
+
const analytics = detectAnalyticsPlugin(cwd);
|
|
220
|
+
const utm = detectUtmTracking(cwd);
|
|
221
|
+
const volume = detectContentVolume(cwd);
|
|
222
|
+
const hasOrderMeta = detectOrderMeta(cwd);
|
|
223
|
+
|
|
224
|
+
const detected = hasWoocommerce && (analytics.detected || utm.detected || volume.content_count > 0);
|
|
225
|
+
|
|
226
|
+
const report = {
|
|
227
|
+
tool: "attribution_inspect",
|
|
228
|
+
version: TOOL_VERSION,
|
|
229
|
+
cwd,
|
|
230
|
+
detected,
|
|
231
|
+
has_woocommerce: hasWoocommerce,
|
|
232
|
+
analytics_plugin: analytics.plugin,
|
|
233
|
+
has_utm_tracking: utm.detected,
|
|
234
|
+
utm_sources: utm.sources,
|
|
235
|
+
content_count: volume.content_count,
|
|
236
|
+
product_count: volume.product_count,
|
|
237
|
+
has_order_attribution_meta: hasOrderMeta,
|
|
238
|
+
attribution_readiness: "unknown",
|
|
239
|
+
recommendations: [],
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// Assess readiness
|
|
243
|
+
if (hasWoocommerce && utm.detected && analytics.detected && volume.content_count > 10) {
|
|
244
|
+
report.attribution_readiness = "high";
|
|
245
|
+
} else if (hasWoocommerce && (utm.detected || analytics.detected) && volume.content_count > 0) {
|
|
246
|
+
report.attribution_readiness = "medium";
|
|
247
|
+
} else if (hasWoocommerce && volume.content_count > 0) {
|
|
248
|
+
report.attribution_readiness = "low";
|
|
249
|
+
} else {
|
|
250
|
+
report.attribution_readiness = "not_ready";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Recommendations
|
|
254
|
+
if (!hasWoocommerce) {
|
|
255
|
+
report.recommendations.push("WooCommerce not detected. Content-commerce attribution requires WooCommerce for sales data.");
|
|
256
|
+
}
|
|
257
|
+
if (hasWoocommerce && !utm.detected) {
|
|
258
|
+
report.recommendations.push("No UTM tracking detected. Install the UTM capture mu-plugin to link content visits to orders.");
|
|
259
|
+
}
|
|
260
|
+
if (hasWoocommerce && !analytics.detected) {
|
|
261
|
+
report.recommendations.push("No analytics plugin detected. Install MonsterInsights or Google Site Kit for traffic data.");
|
|
262
|
+
}
|
|
263
|
+
if (volume.content_count === 0) {
|
|
264
|
+
report.recommendations.push("No published content found. Create blog posts/content to drive traffic to products.");
|
|
265
|
+
}
|
|
266
|
+
if (volume.content_count > 0 && volume.product_count === 0) {
|
|
267
|
+
report.recommendations.push("Content exists but no products found. Verify WooCommerce products are published.");
|
|
268
|
+
}
|
|
269
|
+
if (hasWoocommerce && utm.detected && !hasOrderMeta) {
|
|
270
|
+
report.recommendations.push("UTM tracking is set up but no order attribution meta found yet. Place a test order to verify.");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
console.log(JSON.stringify(report, null, 2));
|
|
274
|
+
process.exit(detected ? 0 : 1);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
main();
|
|
@@ -167,3 +167,4 @@ Read: `references/webhooks.md`
|
|
|
167
167
|
- For REST endpoint development, use the `wp-rest-api` skill
|
|
168
168
|
- For authentication security, use the `wp-security` skill
|
|
169
169
|
- For webhook configuration and management, use the `wp-webhooks` skill
|
|
170
|
+
- For scalable programmatic page generation with ISR/SSG, use the `wp-programmatic-seo` skill
|
package/skills/wp-i18n/SKILL.md
CHANGED
|
@@ -168,3 +168,4 @@ Re-run: `node skills/wp-i18n/scripts/i18n_inspect.mjs --cwd=/path`
|
|
|
168
168
|
- WordPress i18n Handbook: https://developer.wordpress.org/plugins/internationalization/
|
|
169
169
|
- CLDR Plural Rules: https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
|
|
170
170
|
- Translator Handbook: https://make.wordpress.org/polyglots/handbook/
|
|
171
|
+
- For multisite multi-language network orchestration (hreflang, content sync, international SEO), use the `wp-multilang-network` skill
|