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,134 @@
|
|
|
1
|
+
# Location-Based SEO
|
|
2
|
+
|
|
3
|
+
Use this file when creating city/location pages at scale — service-area combinations, LocalBusiness schema, geo-targeting, and NAP consistency.
|
|
4
|
+
|
|
5
|
+
## City/Location Page Patterns
|
|
6
|
+
|
|
7
|
+
Location pages combine a service or product with a geographic area:
|
|
8
|
+
|
|
9
|
+
| Pattern | Template | Scale |
|
|
10
|
+
|---------|----------|-------|
|
|
11
|
+
| Service + City | "Plumbing in {city}" | services × cities |
|
|
12
|
+
| Store + City | "{brand} Store in {city}" | stores × locations |
|
|
13
|
+
| Restaurant + Area | "Best {cuisine} in {neighborhood}" | cuisines × neighborhoods |
|
|
14
|
+
| Real estate + City | "Homes for Sale in {city}" | property types × cities |
|
|
15
|
+
|
|
16
|
+
**Content formula for each page:**
|
|
17
|
+
1. H1: `{Service} in {City}`
|
|
18
|
+
2. Intro paragraph (50–80 words) with local context
|
|
19
|
+
3. Service details section (150–200 words)
|
|
20
|
+
4. Local information (population, area facts, nearby landmarks)
|
|
21
|
+
5. FAQ section (3–5 questions) with local keywords
|
|
22
|
+
6. CTA with phone number and address
|
|
23
|
+
|
|
24
|
+
## LocalBusiness Schema Markup
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"@context": "https://schema.org",
|
|
29
|
+
"@type": "LocalBusiness",
|
|
30
|
+
"name": "{business_name} — {city}",
|
|
31
|
+
"description": "{service_description} in {city}",
|
|
32
|
+
"url": "https://example.com/{service}/{city}",
|
|
33
|
+
"telephone": "{phone}",
|
|
34
|
+
"address": {
|
|
35
|
+
"@type": "PostalAddress",
|
|
36
|
+
"streetAddress": "{street}",
|
|
37
|
+
"addressLocality": "{city}",
|
|
38
|
+
"addressRegion": "{state}",
|
|
39
|
+
"postalCode": "{zip}",
|
|
40
|
+
"addressCountry": "{country_code}"
|
|
41
|
+
},
|
|
42
|
+
"geo": {
|
|
43
|
+
"@type": "GeoCoordinates",
|
|
44
|
+
"latitude": "{lat}",
|
|
45
|
+
"longitude": "{lng}"
|
|
46
|
+
},
|
|
47
|
+
"openingHoursSpecification": [
|
|
48
|
+
{
|
|
49
|
+
"@type": "OpeningHoursSpecification",
|
|
50
|
+
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
|
|
51
|
+
"opens": "09:00",
|
|
52
|
+
"closes": "17:00"
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"areaServed": {
|
|
56
|
+
"@type": "City",
|
|
57
|
+
"name": "{city}"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Schema variations by business type:**
|
|
63
|
+
- `@type: "Restaurant"` — add `servesCuisine`, `menu`, `priceRange`
|
|
64
|
+
- `@type: "MedicalBusiness"` — add `medicalSpecialty`
|
|
65
|
+
- `@type: "Store"` — add `paymentAccepted`, `currenciesAccepted`
|
|
66
|
+
|
|
67
|
+
## Geo-Targeting Configuration
|
|
68
|
+
|
|
69
|
+
### Google Search Console
|
|
70
|
+
1. Set geographic target per property (if using country-specific domains)
|
|
71
|
+
2. Submit location-specific sitemap: `/sitemap-locations.xml`
|
|
72
|
+
3. Monitor "Search results" by country/city in Performance report
|
|
73
|
+
|
|
74
|
+
### WordPress Implementation
|
|
75
|
+
- Use ACF or custom fields for lat/lng storage
|
|
76
|
+
- Store city, state, zip as separate meta fields (not combined string)
|
|
77
|
+
- Create a `location` taxonomy for hierarchical grouping (country → state → city)
|
|
78
|
+
|
|
79
|
+
## NAP Consistency
|
|
80
|
+
|
|
81
|
+
**NAP = Name, Address, Phone** — must be identical across all location pages.
|
|
82
|
+
|
|
83
|
+
| Element | Rule | Example |
|
|
84
|
+
|---------|------|---------|
|
|
85
|
+
| Name | Exact legal business name | "Smith Plumbing LLC" (not "Smith's Plumbing") |
|
|
86
|
+
| Address | USPS-standardized format | "123 Main St, Ste 100" (not "123 Main Street Suite 100") |
|
|
87
|
+
| Phone | E.164 format in schema, formatted for display | `+15551234567` schema, `(555) 123-4567` display |
|
|
88
|
+
|
|
89
|
+
**Verification:** Cross-check NAP against Google Business Profile, Yelp, and BBB listings.
|
|
90
|
+
|
|
91
|
+
## Local Keyword Research Methodology
|
|
92
|
+
|
|
93
|
+
1. **Seed keywords:** core service terms (plumbing, dentist, attorney)
|
|
94
|
+
2. **Location modifiers:** city names, neighborhoods, zip codes, "near me"
|
|
95
|
+
3. **Intent modifiers:** "best", "affordable", "emergency", "24/7"
|
|
96
|
+
4. **Combine:** `{intent} {service} in {location}` → "emergency plumber in Miami"
|
|
97
|
+
5. **Validate:** Check search volume via keyword tools; prioritize cities with volume > 100/mo
|
|
98
|
+
6. **Cluster:** Group by parent topic to avoid cannibalization
|
|
99
|
+
|
|
100
|
+
## WordPress CPT for Locations
|
|
101
|
+
|
|
102
|
+
```php
|
|
103
|
+
// fields: city, state, zip, lat, lng, population, local_description
|
|
104
|
+
register_post_type('service_location', [
|
|
105
|
+
'public' => true,
|
|
106
|
+
'show_in_rest' => true,
|
|
107
|
+
'supports' => ['title', 'editor', 'custom-fields', 'thumbnail'],
|
|
108
|
+
'taxonomies' => ['service_type', 'region'],
|
|
109
|
+
'rewrite' => ['slug' => 'services'],
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
// Expose custom fields via REST API
|
|
113
|
+
register_rest_field('service_location', 'location_meta', [
|
|
114
|
+
'get_callback' => function ($post) {
|
|
115
|
+
return [
|
|
116
|
+
'city' => get_post_meta($post['id'], 'city', true),
|
|
117
|
+
'state' => get_post_meta($post['id'], 'state', true),
|
|
118
|
+
'zip' => get_post_meta($post['id'], 'zip', true),
|
|
119
|
+
'lat' => get_post_meta($post['id'], 'lat', true),
|
|
120
|
+
'lng' => get_post_meta($post['id'], 'lng', true),
|
|
121
|
+
'population' => get_post_meta($post['id'], 'population', true),
|
|
122
|
+
];
|
|
123
|
+
},
|
|
124
|
+
'schema' => ['type' => 'object'],
|
|
125
|
+
]);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Decision Checklist
|
|
129
|
+
|
|
130
|
+
1. Does the business serve multiple geographic areas? → Yes = location pages justified
|
|
131
|
+
2. Is there search volume for `{service} in {city}` queries? → Validate before building
|
|
132
|
+
3. Are NAP details consistent across all listings? → Audit before publishing
|
|
133
|
+
4. Does each location page have 300+ words of unique content? → Avoid thin pages
|
|
134
|
+
5. Is LocalBusiness schema valid per Google's Rich Results Test? → Test before deploy
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Product Programmatic SEO
|
|
2
|
+
|
|
3
|
+
Use this file when generating product variant pages, comparison pages, or category landing pages at scale using WooCommerce product data as the SEO content source.
|
|
4
|
+
|
|
5
|
+
## Product Variant Pages
|
|
6
|
+
|
|
7
|
+
Generate pages for every meaningful combination of product attributes:
|
|
8
|
+
|
|
9
|
+
| Attribute Combination | URL Pattern | Example |
|
|
10
|
+
|----------------------|-------------|---------|
|
|
11
|
+
| Product + Size | `/{product}/{size}` | `/running-shoes/size-10` |
|
|
12
|
+
| Product + Color | `/{product}/{color}` | `/t-shirt/navy-blue` |
|
|
13
|
+
| Product + Size + Color | `/{product}/{size}-{color}` | `/dress/medium-red` |
|
|
14
|
+
| Brand + Category | `/{brand}/{category}` | `/nike/running-shoes` |
|
|
15
|
+
| Product + Material | `/{product}/{material}` | `/jacket/leather` |
|
|
16
|
+
|
|
17
|
+
**When to create variant pages (vs single page with selector):**
|
|
18
|
+
- Each variant has unique search volume (e.g., "red Nike Air Max")
|
|
19
|
+
- Variants differ significantly in content/images
|
|
20
|
+
- Variants target different buyer intents
|
|
21
|
+
|
|
22
|
+
**When NOT to create variant pages:**
|
|
23
|
+
- Variants are trivial (only size differs, no unique content)
|
|
24
|
+
- Low search volume for specific combinations
|
|
25
|
+
- Risk of thin content / doorway pages
|
|
26
|
+
|
|
27
|
+
## WooCommerce Product Data as SEO Source
|
|
28
|
+
|
|
29
|
+
Pull product attributes via WooCommerce REST API:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# List products with attributes — use wc_list_products MCP tool
|
|
33
|
+
wc_list_products(per_page=100, status="publish")
|
|
34
|
+
|
|
35
|
+
# Get product variations
|
|
36
|
+
wc_list_product_variations(product_id=123)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Useful WooCommerce fields for programmatic pages:**
|
|
40
|
+
- `name`, `description`, `short_description` → page content
|
|
41
|
+
- `attributes` → variant dimensions (color, size, material)
|
|
42
|
+
- `categories`, `tags` → taxonomy clustering
|
|
43
|
+
- `regular_price`, `sale_price` → pricing content
|
|
44
|
+
- `average_rating`, `rating_count` → social proof
|
|
45
|
+
- `images` → product visuals
|
|
46
|
+
- `stock_status` → availability signals
|
|
47
|
+
|
|
48
|
+
## Comparison Pages
|
|
49
|
+
|
|
50
|
+
Generate "Product A vs Product B" pages from product pairs:
|
|
51
|
+
|
|
52
|
+
**Template structure:**
|
|
53
|
+
```
|
|
54
|
+
Title: "{Product A} vs {Product B} — Which Is Better?"
|
|
55
|
+
H1: "{Product A} vs {Product B}"
|
|
56
|
+
|
|
57
|
+
Comparison table:
|
|
58
|
+
| Feature | Product A | Product B |
|
|
59
|
+
|---------------|---------------|---------------|
|
|
60
|
+
| Price | {price_a} | {price_b} |
|
|
61
|
+
| Rating | {rating_a}/5 | {rating_b}/5 |
|
|
62
|
+
| {attribute_1} | {value_a} | {value_b} |
|
|
63
|
+
|
|
64
|
+
Summary: "Choose {Product A} if... Choose {Product B} if..."
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Scale calculation:** N products → N×(N-1)/2 comparison pages. 50 products = 1,225 pages.
|
|
68
|
+
|
|
69
|
+
**Quality gate:** Only generate comparisons where products share a category and have meaningful differences.
|
|
70
|
+
|
|
71
|
+
## Category/Tag Landing Pages
|
|
72
|
+
|
|
73
|
+
Programmatic category pages aggregate products with editorial content:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
Title: "Best {Category} in {Year} — Top {count} Picks"
|
|
77
|
+
H1: "Best {Category}"
|
|
78
|
+
|
|
79
|
+
Content:
|
|
80
|
+
- Category introduction (100–150 words)
|
|
81
|
+
- Top products grid (from WooCommerce query)
|
|
82
|
+
- Buying guide section (200–300 words, template with dynamic fields)
|
|
83
|
+
- FAQ (3–5 questions from keyword research)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Implementation:**
|
|
87
|
+
1. Query WooCommerce categories via `wc_list_products(category=ID)`
|
|
88
|
+
2. Sort by `average_rating` or `total_sales` for "top picks"
|
|
89
|
+
3. Generate buying guide from category attributes
|
|
90
|
+
|
|
91
|
+
## Product Schema Markup
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"@context": "https://schema.org",
|
|
96
|
+
"@type": "Product",
|
|
97
|
+
"name": "{product_name}",
|
|
98
|
+
"description": "{product_description}",
|
|
99
|
+
"image": "{image_url}",
|
|
100
|
+
"brand": {
|
|
101
|
+
"@type": "Brand",
|
|
102
|
+
"name": "{brand_name}"
|
|
103
|
+
},
|
|
104
|
+
"sku": "{sku}",
|
|
105
|
+
"offers": {
|
|
106
|
+
"@type": "Offer",
|
|
107
|
+
"url": "{page_url}",
|
|
108
|
+
"priceCurrency": "{currency}",
|
|
109
|
+
"price": "{price}",
|
|
110
|
+
"availability": "https://schema.org/{InStock|OutOfStock}",
|
|
111
|
+
"seller": {
|
|
112
|
+
"@type": "Organization",
|
|
113
|
+
"name": "{store_name}"
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"aggregateRating": {
|
|
117
|
+
"@type": "AggregateRating",
|
|
118
|
+
"ratingValue": "{avg_rating}",
|
|
119
|
+
"reviewCount": "{review_count}"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Canonical URL Strategy
|
|
125
|
+
|
|
126
|
+
Prevent duplicate content across variant and comparison pages:
|
|
127
|
+
|
|
128
|
+
| Scenario | Canonical Rule |
|
|
129
|
+
|----------|---------------|
|
|
130
|
+
| Color variants with same content | Canonical → parent product page |
|
|
131
|
+
| Size variants with unique content | Self-canonical (each variant is canonical) |
|
|
132
|
+
| Comparison A vs B and B vs A | Canonical → alphabetically first (A vs B) |
|
|
133
|
+
| Category + filtered view | Canonical → unfiltered category page |
|
|
134
|
+
| Paginated category pages | `rel="next"` / `rel="prev"` + canonical to page 1 |
|
|
135
|
+
|
|
136
|
+
**Implementation in headless frontend:**
|
|
137
|
+
```html
|
|
138
|
+
<link rel="canonical" href="{computed_canonical_url}" />
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Decision Checklist
|
|
142
|
+
|
|
143
|
+
1. Do product variants have unique search volume? → Yes = variant pages; No = single page
|
|
144
|
+
2. Is comparison data meaningful (shared category, different attributes)? → Yes = comparison pages
|
|
145
|
+
3. Does each generated page have 300+ words of unique content? → Verify template output
|
|
146
|
+
4. Are canonical URLs set correctly to avoid duplicate indexing? → Test with Google URL Inspection
|
|
147
|
+
5. Is Product schema valid? → Test with Rich Results Test
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Technical SEO for Programmatic Pages
|
|
2
|
+
|
|
3
|
+
Use this file when managing sitemaps, crawl budget, canonicals, internal linking, and Core Web Vitals for large-scale programmatic page generation.
|
|
4
|
+
|
|
5
|
+
## XML Sitemap Generation
|
|
6
|
+
|
|
7
|
+
### Yoast SEO Integration
|
|
8
|
+
|
|
9
|
+
Yoast auto-generates sitemaps for all public post types:
|
|
10
|
+
- Main sitemap index: `/sitemap_index.xml`
|
|
11
|
+
- Post type sitemaps: `/post_type-sitemap.xml` (paginated at 1000 URLs per file)
|
|
12
|
+
|
|
13
|
+
**Configure for programmatic CPTs:**
|
|
14
|
+
```php
|
|
15
|
+
// Ensure CPT appears in Yoast sitemap
|
|
16
|
+
register_post_type('location', [
|
|
17
|
+
'public' => true,
|
|
18
|
+
'show_in_rest' => true,
|
|
19
|
+
// Yoast auto-includes public CPTs in sitemap
|
|
20
|
+
]);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Rank Math Integration
|
|
24
|
+
|
|
25
|
+
Rank Math uses similar auto-generation. Configure via:
|
|
26
|
+
- Settings → Sitemap → enable the CPT
|
|
27
|
+
- Pagination: 200 URLs per sitemap file (configurable)
|
|
28
|
+
|
|
29
|
+
### Dynamic Sitemap Index for 1000s+ Pages
|
|
30
|
+
|
|
31
|
+
For very large sites, create a custom sitemap index:
|
|
32
|
+
|
|
33
|
+
```php
|
|
34
|
+
// Custom sitemap for programmatic pages (mu-plugin)
|
|
35
|
+
add_filter('wp_sitemaps_post_types', function ($post_types) {
|
|
36
|
+
$post_types['location'] = get_post_type_object('location');
|
|
37
|
+
return $post_types;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Custom max URLs per sitemap page
|
|
41
|
+
add_filter('wp_sitemaps_max_urls', function () {
|
|
42
|
+
return 2000; // Default is 2000, reduce if pages are slow to crawl
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Sitemap submission:**
|
|
47
|
+
1. Submit sitemap index URL in Google Search Console
|
|
48
|
+
2. Reference sitemap in `robots.txt`: `Sitemap: https://example.com/sitemap_index.xml`
|
|
49
|
+
3. Monitor "Sitemaps" report in GSC for errors
|
|
50
|
+
|
|
51
|
+
## Crawl Budget Optimization
|
|
52
|
+
|
|
53
|
+
Crawl budget = how many pages Googlebot will crawl per session. Critical for 1000s+ pages.
|
|
54
|
+
|
|
55
|
+
### Pages to Index (doindex)
|
|
56
|
+
|
|
57
|
+
- Programmatic pages with unique, valuable content (300+ words)
|
|
58
|
+
- Category landing pages with editorial content
|
|
59
|
+
- Comparison pages with meaningful differentiation
|
|
60
|
+
|
|
61
|
+
### Pages to Noindex
|
|
62
|
+
|
|
63
|
+
| Page Type | Action | Reason |
|
|
64
|
+
|-----------|--------|--------|
|
|
65
|
+
| Thin variant pages (< 300 words) | `noindex, follow` | Low content value |
|
|
66
|
+
| Paginated archives (page 2+) | `noindex, follow` | Duplicate of page 1 |
|
|
67
|
+
| Faceted navigation results | `noindex` or block via `robots.txt` | Parameter-based duplicates |
|
|
68
|
+
| Internal search results | `noindex` | Dynamic, low SEO value |
|
|
69
|
+
| Tag pages with < 3 posts | `noindex, follow` | Thin taxonomy pages |
|
|
70
|
+
|
|
71
|
+
### Implementation
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<!-- Noindex via meta tag (headless frontend) -->
|
|
75
|
+
<meta name="robots" content="noindex, follow" />
|
|
76
|
+
|
|
77
|
+
<!-- Or via X-Robots-Tag HTTP header -->
|
|
78
|
+
X-Robots-Tag: noindex, follow
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Canonical URL Management
|
|
82
|
+
|
|
83
|
+
### Headless Architecture Canonicals
|
|
84
|
+
|
|
85
|
+
In headless setups, the canonical must point to the frontend URL, not the WordPress admin URL:
|
|
86
|
+
|
|
87
|
+
```html
|
|
88
|
+
<!-- Frontend page canonical -->
|
|
89
|
+
<link rel="canonical" href="https://www.example.com/services/miami" />
|
|
90
|
+
|
|
91
|
+
<!-- NOT the WordPress REST source -->
|
|
92
|
+
<!-- Wrong: https://wp.example.com/wp-json/wp/v2/location/123 -->
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Configuration in headless frontend:**
|
|
96
|
+
```javascript
|
|
97
|
+
// Next.js metadata
|
|
98
|
+
export function generateMetadata({ params }) {
|
|
99
|
+
return {
|
|
100
|
+
alternates: {
|
|
101
|
+
canonical: `https://www.example.com/${params.service}/${params.city}`,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Canonical Rules for Programmatic Pages
|
|
108
|
+
|
|
109
|
+
| Scenario | Canonical Target |
|
|
110
|
+
|----------|-----------------|
|
|
111
|
+
| Standard programmatic page | Self-canonical |
|
|
112
|
+
| Variant with identical content to parent | Parent product page |
|
|
113
|
+
| Paginated series | Page 1 of the series |
|
|
114
|
+
| HTTP vs HTTPS | HTTPS version |
|
|
115
|
+
| www vs non-www | Preferred domain version |
|
|
116
|
+
| Trailing slash vs no trailing slash | Consistent chosen format |
|
|
117
|
+
|
|
118
|
+
## robots.txt Configuration
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
# robots.txt for headless WordPress
|
|
122
|
+
User-agent: *
|
|
123
|
+
Allow: /
|
|
124
|
+
|
|
125
|
+
# Block WordPress admin (if exposed)
|
|
126
|
+
Disallow: /wp-admin/
|
|
127
|
+
Allow: /wp-admin/admin-ajax.php
|
|
128
|
+
|
|
129
|
+
# Block faceted navigation / filter parameters
|
|
130
|
+
Disallow: /*?filter=
|
|
131
|
+
Disallow: /*?sort=
|
|
132
|
+
Disallow: /*?page=
|
|
133
|
+
|
|
134
|
+
# Sitemap reference
|
|
135
|
+
Sitemap: https://www.example.com/sitemap_index.xml
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Internal Linking Strategy
|
|
139
|
+
|
|
140
|
+
Programmatic pages need structured internal linking for crawlability and authority flow:
|
|
141
|
+
|
|
142
|
+
### Hub-and-Spoke Model
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
Category hub page (e.g., /plumbing/)
|
|
146
|
+
├── /plumbing/miami
|
|
147
|
+
├── /plumbing/orlando
|
|
148
|
+
├── /plumbing/tampa
|
|
149
|
+
└── /plumbing/jacksonville
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Implementation:**
|
|
153
|
+
- Hub page links to all spoke pages (or top N by relevance)
|
|
154
|
+
- Each spoke page links back to hub + 3–5 related spokes
|
|
155
|
+
- Breadcrumbs: Home → Service → City
|
|
156
|
+
|
|
157
|
+
### Cross-Linking Between Clusters
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
/plumbing/miami ←→ /electrical/miami (same city, different service)
|
|
161
|
+
/plumbing/miami ←→ /plumbing/fort-lauderdale (same service, nearby city)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Automated linking (template-based):**
|
|
165
|
+
```html
|
|
166
|
+
<!-- Related services in {city} -->
|
|
167
|
+
<h2>Other Services in {city}</h2>
|
|
168
|
+
<ul>
|
|
169
|
+
{for service in other_services}
|
|
170
|
+
<li><a href="/{service.slug}/{city.slug}">{service.name} in {city.name}</a></li>
|
|
171
|
+
{/for}
|
|
172
|
+
</ul>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Core Web Vitals for Template Pages
|
|
176
|
+
|
|
177
|
+
| Metric | Target | Programmatic Page Risk |
|
|
178
|
+
|--------|--------|----------------------|
|
|
179
|
+
| LCP | < 2.5s | Large hero images from WP media library |
|
|
180
|
+
| INP | < 200ms | Heavy JS for dynamic content |
|
|
181
|
+
| CLS | < 0.1 | Late-loading images without dimensions |
|
|
182
|
+
|
|
183
|
+
**Optimizations:**
|
|
184
|
+
1. **LCP:** Preload hero image, use `next/image` or equivalent with width/height
|
|
185
|
+
2. **INP:** Minimize client-side JS; SSG/ISR means most content is pre-rendered
|
|
186
|
+
3. **CLS:** Set explicit `width` and `height` on all images; use CSS `aspect-ratio`
|
|
187
|
+
4. **Font loading:** `font-display: swap` for web fonts
|
|
188
|
+
5. **Third-party scripts:** Defer analytics/tracking below the fold
|
|
189
|
+
|
|
190
|
+
## Decision Checklist
|
|
191
|
+
|
|
192
|
+
1. Is the sitemap index submitted and error-free in GSC? → Monitor weekly
|
|
193
|
+
2. Are thin pages noindexed to preserve crawl budget? → Audit monthly
|
|
194
|
+
3. Do all programmatic pages have correct self-canonicals? → Verify in bulk
|
|
195
|
+
4. Is robots.txt blocking faceted navigation and parameters? → Test with robots.txt tester
|
|
196
|
+
5. Does every page link to its hub and 3–5 related pages? → Template includes these links
|
|
197
|
+
6. Are Core Web Vitals passing for template pages? → Test with PageSpeed Insights on sample
|
|
@@ -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`
|