bmad-plus 0.3.1 โ 0.3.3
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/CHANGELOG.md +23 -0
- package/oveanet-pack/seo-audit-360/agent/seo-chief.md +19 -0
- package/oveanet-pack/seo-audit-360/hooks/seo-check.sh +95 -0
- package/oveanet-pack/seo-audit-360/ref/audit-schema.json +187 -0
- package/oveanet-pack/seo-audit-360/ref/hreflang-rules.md +153 -0
- package/oveanet-pack/seo-audit-360/scripts/__pycache__/seo_crawl.cpython-314.pyc +0 -0
- package/oveanet-pack/seo-audit-360/scripts/__pycache__/seo_parse.cpython-314.pyc +0 -0
- package/oveanet-pack/seo-audit-360/scripts/seo_report.py +403 -0
- package/oveanet-pack/seo-audit-360/tests/__pycache__/test_crawl.cpython-314-pytest-9.0.2.pyc +0 -0
- package/oveanet-pack/seo-audit-360/tests/__pycache__/test_parse.cpython-314-pytest-9.0.2.pyc +0 -0
- package/oveanet-pack/seo-audit-360/tests/fixtures/sample_page.html +62 -0
- package/oveanet-pack/seo-audit-360/tests/test_apis.py +75 -0
- package/oveanet-pack/seo-audit-360/tests/test_crawl.py +121 -0
- package/oveanet-pack/seo-audit-360/tests/test_fetch.py +70 -0
- package/oveanet-pack/seo-audit-360/tests/test_parse.py +184 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ All notable changes to BMAD+ will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.3] โ 2026-03-19
|
|
9
|
+
|
|
10
|
+
### ๐งช SEO Engine โ Quality & Security (Sprint 3)
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Unit tests** โ 50 pytest tests covering all Python scripts (fetch, parse, crawl, APIs)
|
|
14
|
+
- **Pre-commit hook** โ `hooks/seo-check.sh` validates HTML for title, meta, alt, H1 before commit
|
|
15
|
+
- **Audit JSON schema** โ `ref/audit-schema.json` standardized export format for dashboard/API integration
|
|
16
|
+
- **Test fixture** โ `tests/fixtures/sample_page.html` with known SEO elements
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## [0.3.2] โ 2026-03-19
|
|
21
|
+
|
|
22
|
+
### ๐ SEO Engine โ Reports, Competitor & Hreflang (Sprint 2)
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- **seo_report.py** โ Professional HTML report generator with inline SVG radar chart, color-coded issue cards, quick wins section, and print-friendly CSS
|
|
26
|
+
- **Benchmarker role** โ Added to Chief agent for `/seo competitor` command (side-by-side site comparison with delta scoring)
|
|
27
|
+
- **hreflang-rules.md** โ Complete hreflang audit reference with 7 validation rules, 6 common error patterns, and 12-point checklist
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
8
31
|
## [0.3.1] โ 2026-03-19
|
|
9
32
|
|
|
10
33
|
### ๐ง SEO Engine Enhancements (Sprint 1)
|
|
@@ -29,6 +29,25 @@ You are **Chief**, the strategist and reporting agent of the BMAD+ SEO Engine. Y
|
|
|
29
29
|
- Generate executive summary for non-technical stakeholders
|
|
30
30
|
- Create monitoring comparison reports (vs previous audit)
|
|
31
31
|
- Format reports for different audiences (developer, marketing, executive)
|
|
32
|
+
- Generate **HTML reports** via `scripts/seo_report.py` from audit JSON
|
|
33
|
+
|
|
34
|
+
### Role: Benchmarker
|
|
35
|
+
**Trigger**: `/seo competitor`, competitive analysis, benchmark
|
|
36
|
+
- Run full audit on **two sites simultaneously** (Scout + Judge on each)
|
|
37
|
+
- Compare scores side-by-side with delta indicators:
|
|
38
|
+
|
|
39
|
+
| Metric | My Site | Competitor | Delta |
|
|
40
|
+
|--------|---------|-----------|-------|
|
|
41
|
+
| SEO Score | 72 | 85 | -13 ๐ด |
|
|
42
|
+
| E-E-A-T | 65 | 78 | -13 ๐ด |
|
|
43
|
+
| Schema types | 3 | 7 | -4 ๐ |
|
|
44
|
+
| GEO/AI Score | 55 | 70 | -15 ๐ด |
|
|
45
|
+
| PageSpeed | 92 | 88 | +4 ๐ข |
|
|
46
|
+
|
|
47
|
+
- Identify **competitive gaps** (where rival is better)
|
|
48
|
+
- Identify **competitive advantages** (where we're better)
|
|
49
|
+
- Generate actionable plan: "To match competitor, prioritize: ..."
|
|
50
|
+
- Output: Markdown comparison report + optional HTML via `seo_report.py`
|
|
32
51
|
|
|
33
52
|
---
|
|
34
53
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# SEO Pre-Commit Hook โ Catches common SEO issues before commit.
|
|
3
|
+
# Install: cp hooks/seo-check.sh .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit
|
|
4
|
+
#
|
|
5
|
+
# Author: Laurent Rochetta | BMAD+ SEO Engine
|
|
6
|
+
|
|
7
|
+
ERRORS=0
|
|
8
|
+
WARNINGS=0
|
|
9
|
+
|
|
10
|
+
echo "๐ BMAD+ SEO Pre-Commit Check..."
|
|
11
|
+
|
|
12
|
+
# Only check staged HTML files
|
|
13
|
+
HTML_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -iE '\.(html|htm|php|jsx|tsx)$')
|
|
14
|
+
|
|
15
|
+
if [ -z "$HTML_FILES" ]; then
|
|
16
|
+
echo " No HTML files staged, skipping SEO check."
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
for FILE in $HTML_FILES; do
|
|
21
|
+
# Skip node_modules and vendor
|
|
22
|
+
if echo "$FILE" | grep -qE '(node_modules|vendor|dist|build|\.min\.)'; then
|
|
23
|
+
continue
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
CONTENT=$(git show ":$FILE" 2>/dev/null)
|
|
27
|
+
if [ -z "$CONTENT" ]; then
|
|
28
|
+
continue
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Check 1: Missing <title> tag
|
|
32
|
+
if ! echo "$CONTENT" | grep -qi '<title'; then
|
|
33
|
+
echo " ๐ด $FILE โ Missing <title> tag"
|
|
34
|
+
ERRORS=$((ERRORS + 1))
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# Check 2: Empty <title> tag
|
|
38
|
+
if echo "$CONTENT" | grep -qiE '<title>\s*</title>'; then
|
|
39
|
+
echo " ๐ด $FILE โ Empty <title> tag"
|
|
40
|
+
ERRORS=$((ERRORS + 1))
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Check 3: Missing meta description
|
|
44
|
+
if ! echo "$CONTENT" | grep -qi 'name="description"'; then
|
|
45
|
+
echo " ๐ $FILE โ Missing <meta name=\"description\">"
|
|
46
|
+
WARNINGS=$((WARNINGS + 1))
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Check 4: Images without alt attribute
|
|
50
|
+
IMG_NO_ALT=$(echo "$CONTENT" | grep -ciE '<img[^>]*(?!alt)[^>]*>' 2>/dev/null || echo "0")
|
|
51
|
+
# More reliable: count <img> without alt
|
|
52
|
+
TOTAL_IMGS=$(echo "$CONTENT" | grep -ci '<img' 2>/dev/null || echo "0")
|
|
53
|
+
IMGS_WITH_ALT=$(echo "$CONTENT" | grep -ci '<img[^>]*alt=' 2>/dev/null || echo "0")
|
|
54
|
+
MISSING_ALT=$((TOTAL_IMGS - IMGS_WITH_ALT))
|
|
55
|
+
|
|
56
|
+
if [ "$MISSING_ALT" -gt 0 ]; then
|
|
57
|
+
echo " ๐ $FILE โ $MISSING_ALT image(s) without alt attribute"
|
|
58
|
+
WARNINGS=$((WARNINGS + 1))
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# Check 5: Multiple H1 tags
|
|
62
|
+
H1_COUNT=$(echo "$CONTENT" | grep -ci '<h1' 2>/dev/null || echo "0")
|
|
63
|
+
if [ "$H1_COUNT" -gt 1 ]; then
|
|
64
|
+
echo " ๐ก $FILE โ Multiple H1 tags ($H1_COUNT found, should be 1)"
|
|
65
|
+
WARNINGS=$((WARNINGS + 1))
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# Check 6: No H1 tag at all
|
|
69
|
+
if [ "$H1_COUNT" -eq 0 ]; then
|
|
70
|
+
echo " ๐ $FILE โ No H1 tag found"
|
|
71
|
+
WARNINGS=$((WARNINGS + 1))
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# Check 7: "Click here" or "Learn more" anchor text
|
|
75
|
+
BAD_ANCHORS=$(echo "$CONTENT" | grep -ciE '>click here<|>learn more<|>read more<|>here<' 2>/dev/null || echo "0")
|
|
76
|
+
if [ "$BAD_ANCHORS" -gt 0 ]; then
|
|
77
|
+
echo " ๐ก $FILE โ $BAD_ANCHORS link(s) with generic anchor text (\"click here\", \"learn more\")"
|
|
78
|
+
WARNINGS=$((WARNINGS + 1))
|
|
79
|
+
fi
|
|
80
|
+
done
|
|
81
|
+
|
|
82
|
+
echo ""
|
|
83
|
+
echo " Results: $ERRORS error(s), $WARNINGS warning(s)"
|
|
84
|
+
|
|
85
|
+
if [ "$ERRORS" -gt 0 ]; then
|
|
86
|
+
echo " โ Commit blocked โ fix critical SEO issues first!"
|
|
87
|
+
exit 1
|
|
88
|
+
else
|
|
89
|
+
if [ "$WARNINGS" -gt 0 ]; then
|
|
90
|
+
echo " โ ๏ธ Commit allowed with warnings โ consider fixing these issues."
|
|
91
|
+
else
|
|
92
|
+
echo " โ
All SEO checks passed!"
|
|
93
|
+
fi
|
|
94
|
+
exit 0
|
|
95
|
+
fi
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "BMAD+ SEO Audit Result",
|
|
4
|
+
"description": "Standardized JSON format for SEO audit results. Compatible with dashboards, MCP Server, and external tools.",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"author": "Laurent Rochetta",
|
|
7
|
+
"type": "object",
|
|
8
|
+
"required": ["engine", "version", "domain", "timestamp", "score", "issues"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"engine": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"const": "bmad-seo-engine",
|
|
13
|
+
"description": "Engine identifier"
|
|
14
|
+
},
|
|
15
|
+
"version": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "Engine version",
|
|
18
|
+
"examples": ["2.1.0"]
|
|
19
|
+
},
|
|
20
|
+
"domain": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Audited domain (without protocol)",
|
|
23
|
+
"examples": ["example.com"]
|
|
24
|
+
},
|
|
25
|
+
"url": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"format": "uri",
|
|
28
|
+
"description": "Full URL audited"
|
|
29
|
+
},
|
|
30
|
+
"timestamp": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"format": "date-time",
|
|
33
|
+
"description": "ISO 8601 timestamp of audit completion"
|
|
34
|
+
},
|
|
35
|
+
"business_type": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"enum": ["saas", "ecommerce", "local", "publisher", "agency", "other"],
|
|
38
|
+
"description": "Detected business type"
|
|
39
|
+
},
|
|
40
|
+
"pages_analyzed": {
|
|
41
|
+
"type": "integer",
|
|
42
|
+
"minimum": 1,
|
|
43
|
+
"description": "Number of pages analyzed"
|
|
44
|
+
},
|
|
45
|
+
"score": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"required": ["total", "categories"],
|
|
48
|
+
"properties": {
|
|
49
|
+
"total": {
|
|
50
|
+
"type": "integer",
|
|
51
|
+
"minimum": 0,
|
|
52
|
+
"maximum": 100,
|
|
53
|
+
"description": "Weighted SEO Health Score"
|
|
54
|
+
},
|
|
55
|
+
"rating": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"enum": ["excellent", "good", "needs_work", "poor", "critical"],
|
|
58
|
+
"description": "Score interpretation"
|
|
59
|
+
},
|
|
60
|
+
"categories": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"properties": {
|
|
63
|
+
"technical": { "type": "integer", "minimum": 0, "maximum": 100 },
|
|
64
|
+
"content_eeat": { "type": "integer", "minimum": 0, "maximum": 100 },
|
|
65
|
+
"on_page": { "type": "integer", "minimum": 0, "maximum": 100 },
|
|
66
|
+
"schema": { "type": "integer", "minimum": 0, "maximum": 100 },
|
|
67
|
+
"performance": { "type": "integer", "minimum": 0, "maximum": 100 },
|
|
68
|
+
"ai_readiness": { "type": "integer", "minimum": 0, "maximum": 100 },
|
|
69
|
+
"images": { "type": "integer", "minimum": 0, "maximum": 100 }
|
|
70
|
+
},
|
|
71
|
+
"description": "Score per category (0-100)"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"issues": {
|
|
76
|
+
"type": "array",
|
|
77
|
+
"items": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"required": ["id", "severity", "category", "title"],
|
|
80
|
+
"properties": {
|
|
81
|
+
"id": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"description": "Unique issue identifier",
|
|
84
|
+
"examples": ["missing-meta-description", "multiple-h1"]
|
|
85
|
+
},
|
|
86
|
+
"severity": {
|
|
87
|
+
"type": "string",
|
|
88
|
+
"enum": ["critical", "high", "medium", "low"]
|
|
89
|
+
},
|
|
90
|
+
"category": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"enum": ["technical", "content", "on_page", "schema", "performance", "geo", "images"]
|
|
93
|
+
},
|
|
94
|
+
"title": {
|
|
95
|
+
"type": "string",
|
|
96
|
+
"description": "Human-readable issue title"
|
|
97
|
+
},
|
|
98
|
+
"description": {
|
|
99
|
+
"type": "string",
|
|
100
|
+
"description": "Detailed explanation"
|
|
101
|
+
},
|
|
102
|
+
"affected_urls": {
|
|
103
|
+
"type": "array",
|
|
104
|
+
"items": { "type": "string", "format": "uri" },
|
|
105
|
+
"description": "Pages affected by this issue"
|
|
106
|
+
},
|
|
107
|
+
"fix": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"description": "Auto-generated fix code (HTML, JSON-LD, etc.)"
|
|
110
|
+
},
|
|
111
|
+
"quick_win": {
|
|
112
|
+
"type": "boolean",
|
|
113
|
+
"description": "True if high impact / low effort"
|
|
114
|
+
},
|
|
115
|
+
"impact": {
|
|
116
|
+
"type": "string",
|
|
117
|
+
"enum": ["high", "medium", "low"]
|
|
118
|
+
},
|
|
119
|
+
"effort": {
|
|
120
|
+
"type": "string",
|
|
121
|
+
"enum": ["high", "medium", "low"]
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"pages": {
|
|
127
|
+
"type": "array",
|
|
128
|
+
"items": {
|
|
129
|
+
"type": "object",
|
|
130
|
+
"properties": {
|
|
131
|
+
"url": { "type": "string", "format": "uri" },
|
|
132
|
+
"status": { "type": "integer" },
|
|
133
|
+
"title": { "type": "string" },
|
|
134
|
+
"word_count": { "type": "integer" },
|
|
135
|
+
"schema_types": { "type": "array", "items": { "type": "string" } },
|
|
136
|
+
"eeat_score": { "type": "integer", "minimum": 0, "maximum": 100 }
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
"description": "Per-page analysis data"
|
|
140
|
+
},
|
|
141
|
+
"geo": {
|
|
142
|
+
"type": "object",
|
|
143
|
+
"properties": {
|
|
144
|
+
"ai_readiness_score": { "type": "integer", "minimum": 0, "maximum": 100 },
|
|
145
|
+
"ai_crawlers_allowed": { "type": "array", "items": { "type": "string" } },
|
|
146
|
+
"has_llms_txt": { "type": "boolean" },
|
|
147
|
+
"citability_score": { "type": "integer", "minimum": 0, "maximum": 100 }
|
|
148
|
+
},
|
|
149
|
+
"description": "GEO / AI search readiness metrics"
|
|
150
|
+
},
|
|
151
|
+
"pagespeed": {
|
|
152
|
+
"type": "object",
|
|
153
|
+
"properties": {
|
|
154
|
+
"mobile": {
|
|
155
|
+
"type": "object",
|
|
156
|
+
"properties": {
|
|
157
|
+
"performance": { "type": "integer" },
|
|
158
|
+
"accessibility": { "type": "integer" },
|
|
159
|
+
"best_practices": { "type": "integer" },
|
|
160
|
+
"seo": { "type": "integer" }
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
"desktop": {
|
|
164
|
+
"type": "object",
|
|
165
|
+
"properties": {
|
|
166
|
+
"performance": { "type": "integer" },
|
|
167
|
+
"accessibility": { "type": "integer" },
|
|
168
|
+
"best_practices": { "type": "integer" },
|
|
169
|
+
"seo": { "type": "integer" }
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
"description": "PageSpeed Insights scores"
|
|
174
|
+
},
|
|
175
|
+
"monitoring": {
|
|
176
|
+
"type": "object",
|
|
177
|
+
"properties": {
|
|
178
|
+
"previous_score": { "type": "integer" },
|
|
179
|
+
"previous_date": { "type": "string", "format": "date" },
|
|
180
|
+
"delta": { "type": "integer" },
|
|
181
|
+
"issues_resolved": { "type": "integer" },
|
|
182
|
+
"issues_new": { "type": "integer" }
|
|
183
|
+
},
|
|
184
|
+
"description": "Comparison with previous audit (if available)"
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Hreflang โ Audit Rules & Best Practices (March 2026)
|
|
2
|
+
|
|
3
|
+
> Author: Laurent Rochetta | BMAD+ SEO Engine v2.0
|
|
4
|
+
|
|
5
|
+
## What is Hreflang?
|
|
6
|
+
|
|
7
|
+
`hreflang` tells search engines which language/region version of a page to serve.
|
|
8
|
+
Errors cause wrong language indexing, duplicate content penalties, and lost organic traffic.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Implementation Methods
|
|
13
|
+
|
|
14
|
+
| Method | Best For | Max Pages |
|
|
15
|
+
|--------|----------|-----------|
|
|
16
|
+
| `<link>` in `<head>` | Small sites (<50 pages) | ~50 |
|
|
17
|
+
| HTTP header `Link:` | Non-HTML files (PDFs) | ~50 |
|
|
18
|
+
| Sitemap `<xhtml:link>` | Large sites (50+) | Unlimited |
|
|
19
|
+
|
|
20
|
+
> **Recommendation**: Use sitemap for sites with 50+ pages. HTTP headers for non-HTML resources.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Validation Rules
|
|
25
|
+
|
|
26
|
+
### Rule 1: Valid Language Codes
|
|
27
|
+
- Use **ISO 639-1** (2-letter): `en`, `fr`, `de`, `es`, `ja`, `zh`
|
|
28
|
+
- Optional **ISO 3166-1 Alpha-2** for region: `en-US`, `en-GB`, `fr-FR`, `fr-CA`, `pt-BR`
|
|
29
|
+
- **Case insensitive** but convention is lowercase lang, uppercase country
|
|
30
|
+
|
|
31
|
+
| โ
Valid | โ Invalid | Why |
|
|
32
|
+
|---------|-----------|-----|
|
|
33
|
+
| `en` | `english` | Must be ISO 639-1 |
|
|
34
|
+
| `fr-FR` | `fr-FRA` | Country must be 2-letter |
|
|
35
|
+
| `zh-Hans` | `cn` | `cn` is not a valid language code |
|
|
36
|
+
| `x-default` | `default` | Must use exact `x-default` |
|
|
37
|
+
|
|
38
|
+
### Rule 2: Self-Referencing (MANDATORY)
|
|
39
|
+
Every page MUST include a hreflang tag pointing to itself.
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<!-- On the English page (example.com/en/) -->
|
|
43
|
+
<link rel="alternate" hreflang="en" href="https://example.com/en/" />
|
|
44
|
+
<link rel="alternate" hreflang="fr" href="https://example.com/fr/" />
|
|
45
|
+
<link rel="alternate" hreflang="x-default" href="https://example.com/en/" />
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Error if missing**: Google may ignore all hreflang tags on that page.
|
|
49
|
+
|
|
50
|
+
### Rule 3: Return Tags (MANDATORY)
|
|
51
|
+
If page A links to page B with hreflang, page B MUST link back to page A.
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
Page A (en) โ hreflang="fr" โ Page B (fr)
|
|
55
|
+
Page B (fr) โ hreflang="en" โ Page A (en) โ MANDATORY
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Error if missing**: Called "orphan hreflang" โ Google ignores the one-way tag.
|
|
59
|
+
|
|
60
|
+
### Rule 4: x-default (STRONGLY RECOMMENDED)
|
|
61
|
+
Designate a fallback page for users whose language/region doesn't match any variant.
|
|
62
|
+
|
|
63
|
+
```html
|
|
64
|
+
<link rel="alternate" hreflang="x-default" href="https://example.com/" />
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Common choices for x-default:
|
|
68
|
+
- Language selector/redirect page
|
|
69
|
+
- English version (most common)
|
|
70
|
+
- Homepage of the main domain
|
|
71
|
+
|
|
72
|
+
### Rule 5: Canonical + Hreflang Consistency
|
|
73
|
+
- Each hreflang URL **must be the canonical version** (not a redirect, not a URL with parameters)
|
|
74
|
+
- If a page has `rel="canonical"` pointing elsewhere, hreflang tags on that page are **ignored**
|
|
75
|
+
- Canonical and hreflang must agree: don't have hreflang point to a URL that canonicalizes to a different URL
|
|
76
|
+
|
|
77
|
+
### Rule 6: Absolute URLs Only
|
|
78
|
+
```html
|
|
79
|
+
<!-- โ
Correct -->
|
|
80
|
+
<link rel="alternate" hreflang="fr" href="https://example.com/fr/page" />
|
|
81
|
+
|
|
82
|
+
<!-- โ Wrong -->
|
|
83
|
+
<link rel="alternate" hreflang="fr" href="/fr/page" />
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Rule 7: No Hreflang on Non-200 Pages
|
|
87
|
+
- Don't include hreflang tags on pages that return 3xx, 4xx, or 5xx
|
|
88
|
+
- Don't point hreflang to pages that redirect
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Common Error Patterns
|
|
93
|
+
|
|
94
|
+
### Error 1: Missing Return Tags
|
|
95
|
+
**Symptom**: hreflang is configured on the main language but not on alternate versions.
|
|
96
|
+
**Fix**: Add reciprocal hreflang tags on ALL language variants.
|
|
97
|
+
|
|
98
|
+
### Error 2: Wrong Canonical + Hreflang
|
|
99
|
+
**Symptom**: Page A hreflang โ Page B, but Page B canonical โ Page C.
|
|
100
|
+
**Fix**: Align canonical and hreflang targets.
|
|
101
|
+
|
|
102
|
+
### Error 3: Missing Self-Reference
|
|
103
|
+
**Symptom**: Page lists other language versions but not itself.
|
|
104
|
+
**Fix**: Add `hreflang` tag with the current page's own language/URL.
|
|
105
|
+
|
|
106
|
+
### Error 4: Inconsistent URLs
|
|
107
|
+
**Symptom**: hreflang uses `http://` but site is on `https://`, or trailing slash mismatch.
|
|
108
|
+
**Fix**: Use exact canonical URL (protocol, www/non-www, trailing slash).
|
|
109
|
+
|
|
110
|
+
### Error 5: Language vs Region Confusion
|
|
111
|
+
**Symptom**: Using `hreflang="fr"` for France and `hreflang="fr"` for Canada.
|
|
112
|
+
**Fix**: Use `hreflang="fr-FR"` and `hreflang="fr-CA"` to differentiate.
|
|
113
|
+
|
|
114
|
+
### Error 6: Missing x-default
|
|
115
|
+
**Symptom**: Users in unsupported regions see random language version.
|
|
116
|
+
**Fix**: Add `x-default` pointing to language selector or English version.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Sitemap Implementation (Recommended for 50+ pages)
|
|
121
|
+
|
|
122
|
+
```xml
|
|
123
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
124
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
|
125
|
+
xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
|
126
|
+
<url>
|
|
127
|
+
<loc>https://example.com/en/page</loc>
|
|
128
|
+
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/page"/>
|
|
129
|
+
<xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr/page"/>
|
|
130
|
+
<xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/page"/>
|
|
131
|
+
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en/page"/>
|
|
132
|
+
</url>
|
|
133
|
+
</urlset>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Audit Checklist
|
|
139
|
+
|
|
140
|
+
| # | Check | Priority |
|
|
141
|
+
|---|-------|----------|
|
|
142
|
+
| 1 | All hreflang language codes are valid ISO 639-1 | ๐ด Critical |
|
|
143
|
+
| 2 | All hreflang country codes are valid ISO 3166-1 | ๐ด Critical |
|
|
144
|
+
| 3 | Every page has self-referencing hreflang | ๐ด Critical |
|
|
145
|
+
| 4 | All hreflang tags have return tags | ๐ด Critical |
|
|
146
|
+
| 5 | All hreflang URLs are absolute | ๐ด Critical |
|
|
147
|
+
| 6 | x-default is specified | ๐ High |
|
|
148
|
+
| 7 | Hreflang URLs match canonical URLs | ๐ High |
|
|
149
|
+
| 8 | No hreflang on non-200 pages | ๐ High |
|
|
150
|
+
| 9 | No hreflang pointing to redirecting URLs | ๐ High |
|
|
151
|
+
| 10 | Consistent protocol (https) and www/non-www | ๐ก Medium |
|
|
152
|
+
| 11 | Language/region differentiation correct | ๐ก Medium |
|
|
153
|
+
| 12 | Sitemap implementation for 50+ pages | ๐ก Medium |
|