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,198 @@
|
|
|
1
|
+
# Hreflang Configuration
|
|
2
|
+
|
|
3
|
+
Use this file when implementing hreflang tags across a WordPress Multisite network — tag format, mu-plugin auto-generation, validation tools, and common mistakes.
|
|
4
|
+
|
|
5
|
+
## Hreflang Tag Format
|
|
6
|
+
|
|
7
|
+
Hreflang tells search engines which language/region a page targets and where alternate versions exist:
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<!-- On the English page -->
|
|
11
|
+
<link rel="alternate" hreflang="en" href="https://example.com/about/" />
|
|
12
|
+
<link rel="alternate" hreflang="it" href="https://example.com/it/chi-siamo/" />
|
|
13
|
+
<link rel="alternate" hreflang="de" href="https://example.com/de/ueber-uns/" />
|
|
14
|
+
<link rel="alternate" hreflang="x-default" href="https://example.com/about/" />
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Language Code Reference
|
|
18
|
+
|
|
19
|
+
| Code | Language | Notes |
|
|
20
|
+
|------|----------|-------|
|
|
21
|
+
| `en` | English | Use `en-us`, `en-gb` for regional |
|
|
22
|
+
| `it` | Italian | |
|
|
23
|
+
| `de` | German | Use `de-at` for Austrian German |
|
|
24
|
+
| `fr` | French | Use `fr-ca` for Canadian French |
|
|
25
|
+
| `es` | Spanish | Use `es-mx` for Mexican Spanish |
|
|
26
|
+
| `pt` | Portuguese | Use `pt-br` for Brazilian Portuguese |
|
|
27
|
+
| `zh-hans` | Chinese (Simplified) | |
|
|
28
|
+
| `zh-hant` | Chinese (Traditional) | |
|
|
29
|
+
| `x-default` | Fallback/default | One per page set |
|
|
30
|
+
|
|
31
|
+
### Rules
|
|
32
|
+
|
|
33
|
+
1. **Self-referencing required:** Every page must include an hreflang tag pointing to itself
|
|
34
|
+
2. **Reciprocal required:** If page A links to page B with hreflang, page B must link back to page A
|
|
35
|
+
3. **x-default:** One page per set should be designated as the default (usually English or main language)
|
|
36
|
+
4. **Absolute URLs:** Always use full absolute URLs in href attributes
|
|
37
|
+
5. **One tag per language-region:** Don't duplicate language codes
|
|
38
|
+
|
|
39
|
+
## Implementation Methods
|
|
40
|
+
|
|
41
|
+
### Method 1: HTML `<head>` Tags (Recommended)
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<head>
|
|
45
|
+
<link rel="alternate" hreflang="en" href="https://example.com/products/" />
|
|
46
|
+
<link rel="alternate" hreflang="it" href="https://example.com/it/prodotti/" />
|
|
47
|
+
<link rel="alternate" hreflang="x-default" href="https://example.com/products/" />
|
|
48
|
+
</head>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Method 2: HTTP Headers (for non-HTML content)
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
Link: <https://example.com/products/>; rel="alternate"; hreflang="en",
|
|
55
|
+
<https://example.com/it/prodotti/>; rel="alternate"; hreflang="it"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Method 3: XML Sitemap
|
|
59
|
+
|
|
60
|
+
```xml
|
|
61
|
+
<url>
|
|
62
|
+
<loc>https://example.com/products/</loc>
|
|
63
|
+
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/products/" />
|
|
64
|
+
<xhtml:link rel="alternate" hreflang="it" href="https://example.com/it/prodotti/" />
|
|
65
|
+
</url>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## mu-plugin for Automatic Hreflang Generation
|
|
69
|
+
|
|
70
|
+
This mu-plugin automatically generates hreflang tags across a multisite network by matching content via slug:
|
|
71
|
+
|
|
72
|
+
```php
|
|
73
|
+
<?php
|
|
74
|
+
/**
|
|
75
|
+
* Plugin Name: Multisite Hreflang Generator
|
|
76
|
+
* Description: Automatically generates hreflang tags across multisite network.
|
|
77
|
+
* Version: 1.0.0
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
add_action('wp_head', function () {
|
|
81
|
+
if (!is_multisite()) return;
|
|
82
|
+
|
|
83
|
+
$current_blog_id = get_current_blog_id();
|
|
84
|
+
$current_path = trailingslashit(parse_url(get_permalink(), PHP_URL_PATH));
|
|
85
|
+
|
|
86
|
+
// Map blog IDs to language codes
|
|
87
|
+
$language_map = [
|
|
88
|
+
1 => 'en', // Main site = English
|
|
89
|
+
2 => 'it', // /it/ sub-site
|
|
90
|
+
3 => 'de', // /de/ sub-site
|
|
91
|
+
4 => 'fr', // /fr/ sub-site
|
|
92
|
+
// Add more as needed
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
// Get current post slug for matching
|
|
96
|
+
$current_slug = '';
|
|
97
|
+
if (is_singular()) {
|
|
98
|
+
$current_slug = get_post_field('post_name', get_queried_object_id());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (empty($current_slug)) return;
|
|
102
|
+
|
|
103
|
+
$sites = get_sites(['number' => 100]);
|
|
104
|
+
$alternates = [];
|
|
105
|
+
|
|
106
|
+
foreach ($sites as $site) {
|
|
107
|
+
$lang = $language_map[$site->blog_id] ?? null;
|
|
108
|
+
if (!$lang) continue;
|
|
109
|
+
|
|
110
|
+
switch_to_blog($site->blog_id);
|
|
111
|
+
|
|
112
|
+
// Find matching content by slug
|
|
113
|
+
$matched_post = get_page_by_path($current_slug, OBJECT, ['post', 'page', 'product']);
|
|
114
|
+
if ($matched_post && $matched_post->post_status === 'publish') {
|
|
115
|
+
$alternates[] = [
|
|
116
|
+
'lang' => $lang,
|
|
117
|
+
'href' => get_permalink($matched_post->ID),
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
restore_current_blog();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Only output if we found at least 2 alternates (including self)
|
|
125
|
+
if (count($alternates) < 2) return;
|
|
126
|
+
|
|
127
|
+
// Output hreflang tags
|
|
128
|
+
foreach ($alternates as $alt) {
|
|
129
|
+
printf(
|
|
130
|
+
'<link rel="alternate" hreflang="%s" href="%s" />' . "\n",
|
|
131
|
+
esc_attr($alt['lang']),
|
|
132
|
+
esc_url($alt['href'])
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// x-default = main site (blog_id 1)
|
|
137
|
+
$default = array_filter($alternates, fn($a) => $a['lang'] === ($language_map[1] ?? 'en'));
|
|
138
|
+
if (!empty($default)) {
|
|
139
|
+
printf(
|
|
140
|
+
'<link rel="alternate" hreflang="x-default" href="%s" />' . "\n",
|
|
141
|
+
esc_url(reset($default)['href'])
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Installation:**
|
|
148
|
+
1. Save as `wp-content/mu-plugins/multisite-hreflang.php`
|
|
149
|
+
2. Update `$language_map` with your blog IDs and language codes
|
|
150
|
+
3. Content matching is by slug — ensure translated pages share the same slug or use a custom meta field for cross-referencing
|
|
151
|
+
|
|
152
|
+
## Hreflang Validation
|
|
153
|
+
|
|
154
|
+
### Google Search Console
|
|
155
|
+
|
|
156
|
+
1. Go to International Targeting report
|
|
157
|
+
2. Check for hreflang errors: missing return tags, unknown language codes, conflicting tags
|
|
158
|
+
3. Verify per-language properties are set up (if using subdomains/domains)
|
|
159
|
+
|
|
160
|
+
### Online Validators
|
|
161
|
+
|
|
162
|
+
| Tool | URL | Checks |
|
|
163
|
+
|------|-----|--------|
|
|
164
|
+
| Hreflang Tags Checker | `hreflang.org` | Tag syntax, return links, x-default |
|
|
165
|
+
| Ahrefs Hreflang Audit | Ahrefs Site Audit | Reciprocal links, missing alternates |
|
|
166
|
+
| Screaming Frog | Desktop crawler | Bulk hreflang validation |
|
|
167
|
+
|
|
168
|
+
### Manual Verification
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
# Check hreflang tags on a page
|
|
172
|
+
curl -s https://example.com/products/ | grep -i "hreflang"
|
|
173
|
+
|
|
174
|
+
# Verify reciprocal: check that the Italian page links back
|
|
175
|
+
curl -s https://example.com/it/prodotti/ | grep -i "hreflang"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Common Mistakes
|
|
179
|
+
|
|
180
|
+
| Mistake | Impact | Fix |
|
|
181
|
+
|---------|--------|-----|
|
|
182
|
+
| Missing self-referencing tag | Search engines ignore the set | Always include hreflang for current page |
|
|
183
|
+
| Missing reciprocal (A→B but not B→A) | Both tags get ignored | Ensure all pages in set link to each other |
|
|
184
|
+
| Wrong language codes | Tags treated as invalid | Use ISO 639-1 codes, verify spelling |
|
|
185
|
+
| Relative URLs in href | Tags get ignored | Always use absolute URLs with protocol |
|
|
186
|
+
| Mixing hreflang methods (head + sitemap) | Potential conflicts | Choose one method and use it consistently |
|
|
187
|
+
| Pointing hreflang to redirecting URLs | Tag gets ignored | Point to final destination URL |
|
|
188
|
+
| Duplicate language codes on same page | Ambiguous signal | One tag per language-region combination |
|
|
189
|
+
|
|
190
|
+
## Decision Checklist
|
|
191
|
+
|
|
192
|
+
1. Are all language sites created in the multisite network? → Verify with `list_sites`
|
|
193
|
+
2. Is the `$language_map` in the mu-plugin correct? → Map each blog_id to its language code
|
|
194
|
+
3. Do translated pages share slugs for matching? → If not, use custom meta field for cross-reference
|
|
195
|
+
4. Are hreflang tags present on all public pages? → Spot-check with `curl | grep hreflang`
|
|
196
|
+
5. Are reciprocal tags correct? → Check both directions for 3+ sample pages
|
|
197
|
+
6. Is `x-default` set to the main language site? → Verify in page source
|
|
198
|
+
7. Is Google Search Console showing no hreflang errors? → Check International Targeting report
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Language Routing
|
|
2
|
+
|
|
3
|
+
Use this file when configuring how users are directed to the correct language version — browser language detection, geo-IP redirect, language switcher widgets, and cookie-based preferences.
|
|
4
|
+
|
|
5
|
+
## Browser Language Detection
|
|
6
|
+
|
|
7
|
+
The `Accept-Language` HTTP header indicates the user's preferred languages:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Accept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### PHP Detection (mu-plugin)
|
|
14
|
+
|
|
15
|
+
```php
|
|
16
|
+
<?php
|
|
17
|
+
/**
|
|
18
|
+
* Language redirect based on Accept-Language header.
|
|
19
|
+
* Only redirects on first visit (no cookie set yet).
|
|
20
|
+
*/
|
|
21
|
+
add_action('template_redirect', function () {
|
|
22
|
+
if (!is_multisite() || is_admin()) return;
|
|
23
|
+
|
|
24
|
+
// Skip if user has language preference cookie
|
|
25
|
+
if (isset($_COOKIE['wp_lang_pref'])) return;
|
|
26
|
+
|
|
27
|
+
// Skip if already on a language sub-site
|
|
28
|
+
$current_blog = get_current_blog_id();
|
|
29
|
+
if ($current_blog !== 1) return; // Only redirect from main site
|
|
30
|
+
|
|
31
|
+
$accepted = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
|
|
32
|
+
$preferred_lang = substr($accepted, 0, 2); // Get primary language code
|
|
33
|
+
|
|
34
|
+
$language_sites = [
|
|
35
|
+
'it' => '/it/',
|
|
36
|
+
'de' => '/de/',
|
|
37
|
+
'fr' => '/fr/',
|
|
38
|
+
'es' => '/es/',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
if (isset($language_sites[$preferred_lang])) {
|
|
42
|
+
$target = home_url($language_sites[$preferred_lang]);
|
|
43
|
+
// Set cookie so we don't redirect again
|
|
44
|
+
setcookie('wp_lang_pref', $preferred_lang, time() + (365 * DAY_IN_SECONDS), '/');
|
|
45
|
+
wp_redirect($target, 302); // 302 = temporary (important for SEO)
|
|
46
|
+
exit;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Important:** Always use 302 (temporary) redirect for language detection, never 301 (permanent). A 301 would cache the redirect for all users, regardless of their language.
|
|
52
|
+
|
|
53
|
+
## Geo-IP Based Language Redirect
|
|
54
|
+
|
|
55
|
+
Use server-side geo-IP to redirect by country:
|
|
56
|
+
|
|
57
|
+
### Nginx Configuration
|
|
58
|
+
|
|
59
|
+
```nginx
|
|
60
|
+
# Requires ngx_http_geoip2_module
|
|
61
|
+
geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
|
|
62
|
+
$geoip2_country_code default=US country iso_code;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
server {
|
|
66
|
+
# Redirect Italian visitors to /it/
|
|
67
|
+
if ($geoip2_country_code = "IT") {
|
|
68
|
+
set $lang_redirect "/it/";
|
|
69
|
+
}
|
|
70
|
+
if ($geoip2_country_code = "DE") {
|
|
71
|
+
set $lang_redirect "/de/";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Only redirect if no cookie and on root
|
|
75
|
+
location = / {
|
|
76
|
+
if ($cookie_wp_lang_pref = "") {
|
|
77
|
+
return 302 $scheme://$host$lang_redirect;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### CloudFlare Workers
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
export default {
|
|
87
|
+
async fetch(request) {
|
|
88
|
+
const url = new URL(request.url);
|
|
89
|
+
const country = request.cf?.country || 'US';
|
|
90
|
+
const hasPref = request.headers.get('cookie')?.includes('wp_lang_pref');
|
|
91
|
+
|
|
92
|
+
if (url.pathname === '/' && !hasPref) {
|
|
93
|
+
const langMap = { IT: '/it/', DE: '/de/', FR: '/fr/', ES: '/es/' };
|
|
94
|
+
if (langMap[country]) {
|
|
95
|
+
return Response.redirect(url.origin + langMap[country], 302);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return fetch(request);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Language Switcher Widget
|
|
105
|
+
|
|
106
|
+
### HTML/CSS Language Switcher
|
|
107
|
+
|
|
108
|
+
```html
|
|
109
|
+
<nav class="language-switcher" aria-label="Language selection">
|
|
110
|
+
<ul>
|
|
111
|
+
<li><a href="https://example.com/about/" hreflang="en" lang="en">English</a></li>
|
|
112
|
+
<li><a href="https://example.com/it/chi-siamo/" hreflang="it" lang="it">Italiano</a></li>
|
|
113
|
+
<li><a href="https://example.com/de/ueber-uns/" hreflang="de" lang="de">Deutsch</a></li>
|
|
114
|
+
<li><a href="https://example.com/fr/a-propos/" hreflang="fr" lang="fr">Français</a></li>
|
|
115
|
+
</ul>
|
|
116
|
+
</nav>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### WordPress Widget (mu-plugin)
|
|
120
|
+
|
|
121
|
+
```php
|
|
122
|
+
add_action('widgets_init', function () {
|
|
123
|
+
register_widget('Multilang_Switcher_Widget');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
class Multilang_Switcher_Widget extends WP_Widget {
|
|
127
|
+
public function __construct() {
|
|
128
|
+
parent::__construct('multilang_switcher', 'Language Switcher');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public function widget($args, $instance) {
|
|
132
|
+
$sites = get_sites(['number' => 20]);
|
|
133
|
+
$language_names = [
|
|
134
|
+
1 => ['code' => 'en', 'name' => 'English'],
|
|
135
|
+
2 => ['code' => 'it', 'name' => 'Italiano'],
|
|
136
|
+
3 => ['code' => 'de', 'name' => 'Deutsch'],
|
|
137
|
+
// Add more...
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
echo $args['before_widget'];
|
|
141
|
+
echo '<nav class="language-switcher" aria-label="Language"><ul>';
|
|
142
|
+
foreach ($sites as $site) {
|
|
143
|
+
$lang = $language_names[$site->blog_id] ?? null;
|
|
144
|
+
if (!$lang) continue;
|
|
145
|
+
$url = get_home_url($site->blog_id);
|
|
146
|
+
$active = ($site->blog_id === get_current_blog_id()) ? ' class="active"' : '';
|
|
147
|
+
printf('<li%s><a href="%s" hreflang="%s" lang="%s">%s</a></li>',
|
|
148
|
+
$active, esc_url($url), esc_attr($lang['code']),
|
|
149
|
+
esc_attr($lang['code']), esc_html($lang['name'])
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
echo '</ul></nav>';
|
|
153
|
+
echo $args['after_widget'];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## URL Structure Consistency
|
|
159
|
+
|
|
160
|
+
Ensure consistent URL patterns across all language sites:
|
|
161
|
+
|
|
162
|
+
| English (primary) | Italian | German | Rule |
|
|
163
|
+
|-------------------|---------|--------|------|
|
|
164
|
+
| `/about/` | `/it/chi-siamo/` | `/de/ueber-uns/` | Translated slugs (better UX) |
|
|
165
|
+
| `/about/` | `/it/about/` | `/de/about/` | Same slugs (easier hreflang matching) |
|
|
166
|
+
| `/products/shoes/` | `/it/prodotti/scarpe/` | `/de/produkte/schuhe/` | Full translation |
|
|
167
|
+
|
|
168
|
+
**Recommendation:** Use **same slugs** for easier hreflang matching via the mu-plugin. If translated slugs are preferred for UX, maintain a cross-reference table or use the multilingual plugin's content connections.
|
|
169
|
+
|
|
170
|
+
## Cookie-Based Language Preference
|
|
171
|
+
|
|
172
|
+
Store user's explicit language choice to persist across sessions:
|
|
173
|
+
|
|
174
|
+
```php
|
|
175
|
+
// When user clicks language switcher, set preference cookie
|
|
176
|
+
add_action('init', function () {
|
|
177
|
+
if (isset($_GET['set_lang'])) {
|
|
178
|
+
$lang = sanitize_text_field($_GET['set_lang']);
|
|
179
|
+
$allowed = ['en', 'it', 'de', 'fr', 'es'];
|
|
180
|
+
if (in_array($lang, $allowed, true)) {
|
|
181
|
+
setcookie('wp_lang_pref', $lang, time() + (365 * DAY_IN_SECONDS), '/');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Priority order for language selection:**
|
|
188
|
+
1. Explicit choice (cookie from language switcher click) — highest
|
|
189
|
+
2. URL path (user navigated to `/it/` directly)
|
|
190
|
+
3. Browser `Accept-Language` header (auto-detect)
|
|
191
|
+
4. Geo-IP (server-side guess)
|
|
192
|
+
5. Default language (x-default / main site) — lowest
|
|
193
|
+
|
|
194
|
+
## Headless Frontend Language Routing
|
|
195
|
+
|
|
196
|
+
### Next.js i18n
|
|
197
|
+
|
|
198
|
+
```javascript
|
|
199
|
+
// next.config.js
|
|
200
|
+
module.exports = {
|
|
201
|
+
i18n: {
|
|
202
|
+
locales: ['en', 'it', 'de', 'fr'],
|
|
203
|
+
defaultLocale: 'en',
|
|
204
|
+
localeDetection: true, // Auto-detect from Accept-Language
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Nuxt i18n Module
|
|
210
|
+
|
|
211
|
+
```javascript
|
|
212
|
+
// nuxt.config.ts
|
|
213
|
+
export default defineNuxtConfig({
|
|
214
|
+
modules: ['@nuxtjs/i18n'],
|
|
215
|
+
i18n: {
|
|
216
|
+
locales: ['en', 'it', 'de', 'fr'],
|
|
217
|
+
defaultLocale: 'en',
|
|
218
|
+
strategy: 'prefix_except_default', // /it/about, /de/about, /about (en)
|
|
219
|
+
detectBrowserLanguage: {
|
|
220
|
+
useCookie: true,
|
|
221
|
+
cookieKey: 'i18n_redirected',
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Decision Checklist
|
|
228
|
+
|
|
229
|
+
1. Should language be detected automatically or only via switcher? → Auto-detect on first visit, respect explicit choice after
|
|
230
|
+
2. Is geo-IP available at the hosting/CDN level? → CloudFlare/nginx = yes; shared hosting = maybe not
|
|
231
|
+
3. Are redirect rules using 302 (not 301)? → 301 caches per-user, breaks multi-language
|
|
232
|
+
4. Is a language switcher present on all pages? → Add to header/footer template
|
|
233
|
+
5. Is cookie-based preference persisted? → Set on switcher click, check before redirect
|
|
234
|
+
6. For headless: is frontend i18n routing configured? → Next.js `i18n` or Nuxt i18n module
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Network Architecture
|
|
2
|
+
|
|
3
|
+
Use this file when designing the multi-language WordPress Multisite network — choosing between subdomains, subdirectories, or separate domains, selecting a multilingual plugin, and establishing site creation workflows.
|
|
4
|
+
|
|
5
|
+
## Architecture Patterns
|
|
6
|
+
|
|
7
|
+
| Pattern | Example | Pros | Cons |
|
|
8
|
+
|---------|---------|------|------|
|
|
9
|
+
| **Subdirectory** | `example.com/it/`, `example.com/de/` | Shares domain authority, easy SSL, one hosting | Less geographic signal, shared server resources |
|
|
10
|
+
| **Subdomain** | `it.example.com`, `de.example.com` | Separate GSC properties, clear language signal | SSL per subdomain, DNS configuration needed |
|
|
11
|
+
| **Separate domains** | `example.it`, `example.de` | Strongest geo-targeting signal, full independence | Multiple hosting/SSL/DNS, no shared authority |
|
|
12
|
+
|
|
13
|
+
### When to Choose Each
|
|
14
|
+
|
|
15
|
+
**Subdirectory (recommended for most):**
|
|
16
|
+
- Starting a multilingual site with shared content
|
|
17
|
+
- Budget-conscious (one hosting, one SSL)
|
|
18
|
+
- Want to inherit domain authority across languages
|
|
19
|
+
- 2–5 languages
|
|
20
|
+
|
|
21
|
+
**Subdomain:**
|
|
22
|
+
- Each language needs significant independence
|
|
23
|
+
- Different teams manage different languages
|
|
24
|
+
- Want separate Google Search Console properties
|
|
25
|
+
- 3–10 languages
|
|
26
|
+
|
|
27
|
+
**Separate domains:**
|
|
28
|
+
- Strong country-specific targeting needed (ccTLDs)
|
|
29
|
+
- Completely independent content per market
|
|
30
|
+
- Enterprise with per-country marketing teams
|
|
31
|
+
- Already owns country domains
|
|
32
|
+
|
|
33
|
+
## Sub-Site Creation Workflow
|
|
34
|
+
|
|
35
|
+
Using multisite MCP tools to create language sub-sites:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# 1. List existing sites
|
|
39
|
+
list_sites()
|
|
40
|
+
|
|
41
|
+
# 2. Create language sub-sites (one per language)
|
|
42
|
+
ms_create_site(domain="example.com", path="/it/", title="Example - Italiano")
|
|
43
|
+
ms_create_site(domain="example.com", path="/de/", title="Example - Deutsch")
|
|
44
|
+
ms_create_site(domain="example.com", path="/fr/", title="Example - Français")
|
|
45
|
+
ms_create_site(domain="example.com", path="/es/", title="Example - Español")
|
|
46
|
+
|
|
47
|
+
# 3. Verify all sites created
|
|
48
|
+
list_sites()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Naming Conventions
|
|
52
|
+
|
|
53
|
+
| Element | Convention | Example |
|
|
54
|
+
|---------|-----------|---------|
|
|
55
|
+
| Site slug/path | ISO 639-1 language code | `/it/`, `/de/`, `/fr/` |
|
|
56
|
+
| Site title | `{Brand} - {Language Name}` | "DolceZero - Italiano" |
|
|
57
|
+
| Admin email | Shared network admin email | `admin@example.com` |
|
|
58
|
+
| Language constant | WordPress locale code | `it_IT`, `de_DE`, `fr_FR` |
|
|
59
|
+
|
|
60
|
+
**For regional variants:**
|
|
61
|
+
- Use ISO 639-1 + country: `pt-br` (Brazilian Portuguese), `zh-cn` (Simplified Chinese)
|
|
62
|
+
- Path: `/pt-br/`, `/zh-cn/`
|
|
63
|
+
|
|
64
|
+
## Shared vs Independent Content
|
|
65
|
+
|
|
66
|
+
| Strategy | Description | Best For |
|
|
67
|
+
|----------|-------------|----------|
|
|
68
|
+
| **Shared + translated** | Same content structure, translated per language | Marketing sites, blogs |
|
|
69
|
+
| **Shared core + local** | Common pages translated, plus local-only content | E-commerce with local products |
|
|
70
|
+
| **Fully independent** | Each site has its own editorial calendar | News sites, regional publications |
|
|
71
|
+
|
|
72
|
+
## Network Plugin Management
|
|
73
|
+
|
|
74
|
+
### Network-Activated Plugins (shared across all sites)
|
|
75
|
+
|
|
76
|
+
Plugins that should be network-activated for consistency:
|
|
77
|
+
- Multilingual plugin (WPML, Polylang, MultilingualPress)
|
|
78
|
+
- SEO plugin (Yoast, Rank Math)
|
|
79
|
+
- Caching plugin (WP Super Cache, W3 Total Cache)
|
|
80
|
+
- Security plugin (Wordfence, Sucuri)
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Network-activate a plugin (WP-CLI)
|
|
84
|
+
wp plugin activate yoast-seo --network
|
|
85
|
+
|
|
86
|
+
# Or via MCP tool
|
|
87
|
+
ms_network_activate_plugin(plugin="wordpress-seo/wp-seo.php")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Per-Site Plugins
|
|
91
|
+
|
|
92
|
+
Plugins that may differ per language site:
|
|
93
|
+
- Payment gateways (region-specific)
|
|
94
|
+
- Legal compliance (GDPR, cookie consent varies by country)
|
|
95
|
+
- Local directory integrations
|
|
96
|
+
|
|
97
|
+
## Multilingual Plugin Comparison
|
|
98
|
+
|
|
99
|
+
| Feature | WPML | Polylang | MultilingualPress |
|
|
100
|
+
|---------|------|----------|-------------------|
|
|
101
|
+
| **Architecture** | Single-site (can work with multisite) | Single-site (multisite add-on available) | Native multisite |
|
|
102
|
+
| **License** | Paid ($39–$159/yr) | Free core + paid add-ons | Paid (€199/yr) |
|
|
103
|
+
| **Translation management** | Built-in TM, XLIFF export | Manual or XLIFF | Content connections between sites |
|
|
104
|
+
| **String translation** | Yes (wpml-string-translation) | Polylang Pro | Via WordPress i18n |
|
|
105
|
+
| **WooCommerce support** | WPML WooCommerce Multilingual | WooCommerce Polylang | Native multisite WC |
|
|
106
|
+
| **Performance impact** | Medium (additional DB queries) | Low | Low (native multisite) |
|
|
107
|
+
| **Best for** | Single-site multilingual | Small sites, budget-friendly | Multisite-native workflows |
|
|
108
|
+
|
|
109
|
+
### Recommendation
|
|
110
|
+
|
|
111
|
+
For a **Multisite multi-language network**, prefer **MultilingualPress** (native multisite design) or **WPML** (most feature-rich). Polylang is best for single-site setups.
|
|
112
|
+
|
|
113
|
+
## Decision Checklist
|
|
114
|
+
|
|
115
|
+
1. How many languages are needed now and in 2 years? → 2–5 = subdirectory; 5+ = subdomain
|
|
116
|
+
2. Do language sites share content or are they independent? → Shared = translation plugin; Independent = just multisite
|
|
117
|
+
3. Is the team centralized or per-country? → Centralized = shared admin; Per-country = per-site admins
|
|
118
|
+
4. Is WooCommerce involved? → Yes = verify plugin WC compatibility
|
|
119
|
+
5. What's the budget for multilingual plugin licensing? → Free = Polylang; Paid = WPML or MultilingualPress
|