antigravity-seo-kit 2.0.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.

Potentially problematic release.


This version of antigravity-seo-kit might be problematic. Click here for more details.

Files changed (135) hide show
  1. package/.agent/agent.md +96 -0
  2. package/.agent/skills/seo/SKILL.md +153 -0
  3. package/.agent/skills/seo/references/cwv-thresholds.md +108 -0
  4. package/.agent/skills/seo/references/eeat-framework.md +214 -0
  5. package/.agent/skills/seo/references/local-schema-types.md +230 -0
  6. package/.agent/skills/seo/references/local-seo-signals.md +218 -0
  7. package/.agent/skills/seo/references/maps-api-endpoints.md +160 -0
  8. package/.agent/skills/seo/references/maps-free-apis.md +176 -0
  9. package/.agent/skills/seo/references/maps-gbp-checklist.md +150 -0
  10. package/.agent/skills/seo/references/maps-geo-grid.md +154 -0
  11. package/.agent/skills/seo/references/quality-gates.md +155 -0
  12. package/.agent/skills/seo/references/schema-types.md +118 -0
  13. package/.agent/skills/seo/schema/templates.json +213 -0
  14. package/.agent/skills/seo/scripts/analyze_visual.py +217 -0
  15. package/.agent/skills/seo/scripts/capture_screenshot.py +181 -0
  16. package/.agent/skills/seo/scripts/fetch_page.py +196 -0
  17. package/.agent/skills/seo/scripts/parse_html.py +201 -0
  18. package/.agent/skills/seo-audit/SKILL.md +278 -0
  19. package/.agent/skills/seo-competitor-pages/SKILL.md +212 -0
  20. package/.agent/skills/seo-content/SKILL.md +230 -0
  21. package/.agent/skills/seo-dataforseo/SKILL.md +418 -0
  22. package/.agent/skills/seo-geo/SKILL.md +305 -0
  23. package/.agent/skills/seo-google/SKILL.md +405 -0
  24. package/.agent/skills/seo-google/assets/templates/cwv-audit-report.md +48 -0
  25. package/.agent/skills/seo-google/assets/templates/gsc-performance-report.md +44 -0
  26. package/.agent/skills/seo-google/assets/templates/indexation-status-report.md +43 -0
  27. package/.agent/skills/seo-google/references/auth-setup.md +154 -0
  28. package/.agent/skills/seo-google/references/ga4-data-api.md +184 -0
  29. package/.agent/skills/seo-google/references/indexing-api.md +107 -0
  30. package/.agent/skills/seo-google/references/keyword-planner-api.md +66 -0
  31. package/.agent/skills/seo-google/references/nlp-api.md +55 -0
  32. package/.agent/skills/seo-google/references/pagespeed-crux-api.md +204 -0
  33. package/.agent/skills/seo-google/references/rate-limits-quotas.md +75 -0
  34. package/.agent/skills/seo-google/references/search-console-api.md +156 -0
  35. package/.agent/skills/seo-google/references/supplementary-apis.md +99 -0
  36. package/.agent/skills/seo-google/references/youtube-api.md +49 -0
  37. package/.agent/skills/seo-google/scripts/crux_history.py +321 -0
  38. package/.agent/skills/seo-google/scripts/ga4_report.py +478 -0
  39. package/.agent/skills/seo-google/scripts/google_auth.py +795 -0
  40. package/.agent/skills/seo-google/scripts/google_report.py +2273 -0
  41. package/.agent/skills/seo-google/scripts/gsc_inspect.py +340 -0
  42. package/.agent/skills/seo-google/scripts/gsc_query.py +378 -0
  43. package/.agent/skills/seo-google/scripts/indexing_notify.py +313 -0
  44. package/.agent/skills/seo-google/scripts/keyword_planner.py +297 -0
  45. package/.agent/skills/seo-google/scripts/nlp_analyze.py +309 -0
  46. package/.agent/skills/seo-google/scripts/pagespeed_check.py +649 -0
  47. package/.agent/skills/seo-google/scripts/youtube_search.py +355 -0
  48. package/.agent/skills/seo-hreflang/SKILL.md +192 -0
  49. package/.agent/skills/seo-image-gen/SKILL.md +211 -0
  50. package/.agent/skills/seo-image-gen/references/cost-tracking.md +47 -0
  51. package/.agent/skills/seo-image-gen/references/gemini-models.md +200 -0
  52. package/.agent/skills/seo-image-gen/references/mcp-tools.md +115 -0
  53. package/.agent/skills/seo-image-gen/references/post-processing.md +192 -0
  54. package/.agent/skills/seo-image-gen/references/presets.md +69 -0
  55. package/.agent/skills/seo-image-gen/references/prompt-engineering.md +411 -0
  56. package/.agent/skills/seo-image-gen/references/seo-image-presets.md +137 -0
  57. package/.agent/skills/seo-image-gen/scripts/batch.py +97 -0
  58. package/.agent/skills/seo-image-gen/scripts/cost_tracker.py +191 -0
  59. package/.agent/skills/seo-image-gen/scripts/edit.py +141 -0
  60. package/.agent/skills/seo-image-gen/scripts/generate.py +149 -0
  61. package/.agent/skills/seo-image-gen/scripts/presets.py +153 -0
  62. package/.agent/skills/seo-image-gen/scripts/setup_mcp.py +151 -0
  63. package/.agent/skills/seo-image-gen/scripts/validate_setup.py +133 -0
  64. package/.agent/skills/seo-images/SKILL.md +176 -0
  65. package/.agent/skills/seo-local/SKILL.md +381 -0
  66. package/.agent/skills/seo-maps/SKILL.md +328 -0
  67. package/.agent/skills/seo-page/SKILL.md +86 -0
  68. package/.agent/skills/seo-plan/SKILL.md +118 -0
  69. package/.agent/skills/seo-plan/assets/agency.md +175 -0
  70. package/.agent/skills/seo-plan/assets/ecommerce.md +167 -0
  71. package/.agent/skills/seo-plan/assets/generic.md +144 -0
  72. package/.agent/skills/seo-plan/assets/local-service.md +160 -0
  73. package/.agent/skills/seo-plan/assets/publisher.md +153 -0
  74. package/.agent/skills/seo-plan/assets/saas.md +135 -0
  75. package/.agent/skills/seo-programmatic/SKILL.md +171 -0
  76. package/.agent/skills/seo-schema/SKILL.md +223 -0
  77. package/.agent/skills/seo-sitemap/SKILL.md +180 -0
  78. package/.agent/skills/seo-technical/SKILL.md +211 -0
  79. package/.agent/workflows/seo-audit.md +17 -0
  80. package/.agent/workflows/seo-competitor-pages.md +12 -0
  81. package/.agent/workflows/seo-content.md +14 -0
  82. package/.agent/workflows/seo-geo.md +12 -0
  83. package/.agent/workflows/seo-google.md +12 -0
  84. package/.agent/workflows/seo-hreflang.md +12 -0
  85. package/.agent/workflows/seo-images.md +13 -0
  86. package/.agent/workflows/seo-local.md +12 -0
  87. package/.agent/workflows/seo-maps.md +11 -0
  88. package/.agent/workflows/seo-page.md +13 -0
  89. package/.agent/workflows/seo-plan.md +13 -0
  90. package/.agent/workflows/seo-programmatic.md +12 -0
  91. package/.agent/workflows/seo-schema.md +11 -0
  92. package/.agent/workflows/seo-sitemap.md +9 -0
  93. package/.agent/workflows/seo-technical.md +18 -0
  94. package/LICENSE +88 -0
  95. package/README.md +122 -0
  96. package/bin/cli.js +117 -0
  97. package/docs/ARCHITECTURE.md +218 -0
  98. package/docs/COMMANDS.md +184 -0
  99. package/docs/INSTALLATION.md +100 -0
  100. package/docs/MCP-INTEGRATION.md +153 -0
  101. package/docs/TROUBLESHOOTING.md +151 -0
  102. package/docs/superpowers/plans/2026-03-13-github-audit-fixes.md +511 -0
  103. package/extensions/banana/README.md +95 -0
  104. package/extensions/banana/docs/BANANA-SETUP.md +86 -0
  105. package/extensions/banana/install.sh +170 -0
  106. package/extensions/banana/references/cost-tracking.md +47 -0
  107. package/extensions/banana/references/gemini-models.md +200 -0
  108. package/extensions/banana/references/mcp-tools.md +115 -0
  109. package/extensions/banana/references/post-processing.md +192 -0
  110. package/extensions/banana/references/presets.md +69 -0
  111. package/extensions/banana/references/prompt-engineering.md +411 -0
  112. package/extensions/banana/references/seo-image-presets.md +137 -0
  113. package/extensions/banana/scripts/batch.py +97 -0
  114. package/extensions/banana/scripts/cost_tracker.py +191 -0
  115. package/extensions/banana/scripts/edit.py +141 -0
  116. package/extensions/banana/scripts/generate.py +149 -0
  117. package/extensions/banana/scripts/presets.py +153 -0
  118. package/extensions/banana/scripts/setup_mcp.py +151 -0
  119. package/extensions/banana/scripts/validate_setup.py +133 -0
  120. package/extensions/banana/uninstall.sh +43 -0
  121. package/extensions/dataforseo/README.md +169 -0
  122. package/extensions/dataforseo/docs/DATAFORSEO-SETUP.md +74 -0
  123. package/extensions/dataforseo/field-config.json +280 -0
  124. package/extensions/dataforseo/install.ps1 +110 -0
  125. package/extensions/dataforseo/install.sh +161 -0
  126. package/extensions/dataforseo/uninstall.ps1 +35 -0
  127. package/extensions/dataforseo/uninstall.sh +39 -0
  128. package/lib/api.js +190 -0
  129. package/lib/fingerprint.js +68 -0
  130. package/lib/installer.js +486 -0
  131. package/lib/utils.js +254 -0
  132. package/package.json +40 -0
  133. package/pyproject.toml +11 -0
  134. package/requirements-google.txt +15 -0
  135. package/requirements.txt +11 -0
@@ -0,0 +1,355 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ YouTube Data API v3 - Search, video details, and channel data for SEO.
4
+
5
+ YouTube mentions have the strongest AI visibility correlation (0.737).
6
+ This script provides authoritative YouTube data directly from Google.
7
+
8
+ Usage:
9
+ python youtube_search.py search "seo best practices"
10
+ python youtube_search.py video dQw4w9WgXcQ --json
11
+ python youtube_search.py channel UCxxxxxx --json
12
+ """
13
+
14
+ import argparse
15
+ import json
16
+ import sys
17
+ from typing import Optional
18
+
19
+ try:
20
+ from googleapiclient.discovery import build
21
+ except ImportError:
22
+ print(
23
+ "Error: google-api-python-client required. "
24
+ "Install with: pip install google-api-python-client",
25
+ file=sys.stderr,
26
+ )
27
+ sys.exit(1)
28
+
29
+ try:
30
+ from google_auth import get_api_key
31
+ except ImportError:
32
+ import os
33
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
34
+ from google_auth import get_api_key
35
+
36
+ # YouTube Data API v3 quota costs:
37
+ # search.list = 100 units, videos.list = 1 unit, channels.list = 1 unit
38
+ # Default quota: 10,000 units/day = ~100 searches or ~10,000 video lookups
39
+ YOUTUBE_API_SERVICE = "youtube"
40
+ YOUTUBE_API_VERSION = "v3"
41
+
42
+
43
+ def _build_youtube_service(api_key: Optional[str] = None):
44
+ """Build the YouTube Data API v3 service."""
45
+ key = api_key or get_api_key()
46
+ if not key:
47
+ return None
48
+ try:
49
+ return build(YOUTUBE_API_SERVICE, YOUTUBE_API_VERSION, developerKey=key)
50
+ except Exception as e:
51
+ print(f"Error building YouTube service: {e}", file=sys.stderr)
52
+ return None
53
+
54
+
55
+ def search_videos(
56
+ query: str,
57
+ max_results: int = 10,
58
+ order: str = "relevance",
59
+ api_key: Optional[str] = None,
60
+ ) -> dict:
61
+ """
62
+ Search YouTube for videos matching a query.
63
+
64
+ Args:
65
+ query: Search query string.
66
+ max_results: Max results (1-50, default 10).
67
+ order: Sort order: relevance, date, rating, viewCount, title.
68
+ api_key: Optional API key override.
69
+
70
+ Returns:
71
+ Dictionary with videos list and metadata.
72
+ """
73
+ result = {"query": query, "videos": [], "total_results": 0, "error": None}
74
+
75
+ service = _build_youtube_service(api_key)
76
+ if not service:
77
+ result["error"] = "No API key. Set GOOGLE_API_KEY or add 'api_key' to config."
78
+ return result
79
+
80
+ try:
81
+ response = service.search().list(
82
+ q=query,
83
+ part="snippet",
84
+ type="video",
85
+ maxResults=min(max_results, 50),
86
+ order=order,
87
+ ).execute()
88
+
89
+ result["total_results"] = response.get("pageInfo", {}).get("totalResults", 0)
90
+
91
+ # Get video IDs for statistics
92
+ video_ids = []
93
+ snippets = {}
94
+ for item in response.get("items", []):
95
+ vid = item["id"].get("videoId")
96
+ if vid:
97
+ video_ids.append(vid)
98
+ snippets[vid] = item.get("snippet", {})
99
+
100
+ # Fetch statistics for all videos in one call (1 unit)
101
+ if video_ids:
102
+ stats_response = service.videos().list(
103
+ id=",".join(video_ids),
104
+ part="statistics,contentDetails",
105
+ ).execute()
106
+
107
+ stats_map = {}
108
+ for item in stats_response.get("items", []):
109
+ stats_map[item["id"]] = {
110
+ "views": int(item.get("statistics", {}).get("viewCount", 0)),
111
+ "likes": int(item.get("statistics", {}).get("likeCount", 0)),
112
+ "comments": int(item.get("statistics", {}).get("commentCount", 0)),
113
+ "duration": item.get("contentDetails", {}).get("duration", ""),
114
+ }
115
+
116
+ for vid in video_ids:
117
+ snip = snippets.get(vid, {})
118
+ stats = stats_map.get(vid, {})
119
+ result["videos"].append({
120
+ "video_id": vid,
121
+ "title": snip.get("title", ""),
122
+ "channel": snip.get("channelTitle", ""),
123
+ "channel_id": snip.get("channelId", ""),
124
+ "published": snip.get("publishedAt", ""),
125
+ "description": snip.get("description", "")[:300],
126
+ "thumbnail": snip.get("thumbnails", {}).get("high", {}).get("url", ""),
127
+ "views": stats.get("views", 0),
128
+ "likes": stats.get("likes", 0),
129
+ "comments": stats.get("comments", 0),
130
+ "duration": stats.get("duration", ""),
131
+ "url": f"https://www.youtube.com/watch?v={vid}",
132
+ })
133
+
134
+ except Exception as e:
135
+ error_str = str(e)
136
+ if "403" in error_str:
137
+ result["error"] = (
138
+ "YouTube Data API access denied. Ensure the API is enabled "
139
+ "in your GCP project (APIs & Services > Library > YouTube Data API v3)."
140
+ )
141
+ elif "429" in error_str:
142
+ result["error"] = "YouTube API quota exceeded (10,000 units/day). Search costs 100 units."
143
+ else:
144
+ result["error"] = f"YouTube API error: {e}"
145
+
146
+ return result
147
+
148
+
149
+ def get_video_details(
150
+ video_id: str,
151
+ api_key: Optional[str] = None,
152
+ ) -> dict:
153
+ """
154
+ Get detailed information about a specific YouTube video.
155
+
156
+ Args:
157
+ video_id: YouTube video ID.
158
+ api_key: Optional API key override.
159
+
160
+ Returns:
161
+ Dictionary with video details, statistics, and top comments.
162
+ """
163
+ result = {"video_id": video_id, "details": None, "comments": [], "error": None}
164
+
165
+ service = _build_youtube_service(api_key)
166
+ if not service:
167
+ result["error"] = "No API key configured."
168
+ return result
169
+
170
+ try:
171
+ # Video details (1 unit)
172
+ response = service.videos().list(
173
+ id=video_id,
174
+ part="snippet,statistics,contentDetails,topicDetails",
175
+ ).execute()
176
+
177
+ items = response.get("items", [])
178
+ if not items:
179
+ result["error"] = f"Video not found: {video_id}"
180
+ return result
181
+
182
+ item = items[0]
183
+ snip = item.get("snippet", {})
184
+ stats = item.get("statistics", {})
185
+ content = item.get("contentDetails", {})
186
+ topics = item.get("topicDetails", {})
187
+
188
+ result["details"] = {
189
+ "title": snip.get("title", ""),
190
+ "channel": snip.get("channelTitle", ""),
191
+ "channel_id": snip.get("channelId", ""),
192
+ "published": snip.get("publishedAt", ""),
193
+ "description": snip.get("description", ""),
194
+ "tags": snip.get("tags", []),
195
+ "category_id": snip.get("categoryId", ""),
196
+ "duration": content.get("duration", ""),
197
+ "definition": content.get("definition", ""),
198
+ "caption": content.get("caption", "false"),
199
+ "views": int(stats.get("viewCount", 0)),
200
+ "likes": int(stats.get("likeCount", 0)),
201
+ "comments_count": int(stats.get("commentCount", 0)),
202
+ "favorites": int(stats.get("favoriteCount", 0)),
203
+ "topic_categories": topics.get("topicCategories", []),
204
+ "url": f"https://www.youtube.com/watch?v={video_id}",
205
+ }
206
+
207
+ # Top comments (1 unit)
208
+ try:
209
+ comments_response = service.commentThreads().list(
210
+ videoId=video_id,
211
+ part="snippet",
212
+ maxResults=10,
213
+ order="relevance",
214
+ textFormat="plainText",
215
+ ).execute()
216
+
217
+ for thread in comments_response.get("items", []):
218
+ comment = thread.get("snippet", {}).get("topLevelComment", {}).get("snippet", {})
219
+ result["comments"].append({
220
+ "author": comment.get("authorDisplayName", ""),
221
+ "text": comment.get("textDisplay", "")[:500],
222
+ "likes": comment.get("likeCount", 0),
223
+ "published": comment.get("publishedAt", ""),
224
+ })
225
+ except Exception:
226
+ pass # Comments may be disabled
227
+
228
+ except Exception as e:
229
+ result["error"] = f"YouTube API error: {e}"
230
+
231
+ return result
232
+
233
+
234
+ def get_channel_info(
235
+ channel_id: str,
236
+ api_key: Optional[str] = None,
237
+ ) -> dict:
238
+ """
239
+ Get channel information.
240
+
241
+ Args:
242
+ channel_id: YouTube channel ID.
243
+ api_key: Optional API key override.
244
+
245
+ Returns:
246
+ Dictionary with channel details.
247
+ """
248
+ result = {"channel_id": channel_id, "channel": None, "error": None}
249
+
250
+ service = _build_youtube_service(api_key)
251
+ if not service:
252
+ result["error"] = "No API key configured."
253
+ return result
254
+
255
+ try:
256
+ response = service.channels().list(
257
+ id=channel_id,
258
+ part="snippet,statistics,brandingSettings",
259
+ ).execute()
260
+
261
+ items = response.get("items", [])
262
+ if not items:
263
+ result["error"] = f"Channel not found: {channel_id}"
264
+ return result
265
+
266
+ item = items[0]
267
+ snip = item.get("snippet", {})
268
+ stats = item.get("statistics", {})
269
+
270
+ result["channel"] = {
271
+ "title": snip.get("title", ""),
272
+ "description": snip.get("description", "")[:500],
273
+ "custom_url": snip.get("customUrl", ""),
274
+ "published": snip.get("publishedAt", ""),
275
+ "country": snip.get("country", ""),
276
+ "subscribers": int(stats.get("subscriberCount", 0)),
277
+ "videos": int(stats.get("videoCount", 0)),
278
+ "views": int(stats.get("viewCount", 0)),
279
+ "thumbnail": snip.get("thumbnails", {}).get("high", {}).get("url", ""),
280
+ }
281
+
282
+ except Exception as e:
283
+ result["error"] = f"YouTube API error: {e}"
284
+
285
+ return result
286
+
287
+
288
+ def main():
289
+ parser = argparse.ArgumentParser(
290
+ description="YouTube Data API v3 - Search and video analysis for SEO"
291
+ )
292
+ parser.add_argument(
293
+ "command",
294
+ choices=["search", "video", "channel"],
295
+ help="Command: search, video (details), channel (info)",
296
+ )
297
+ parser.add_argument("query", help="Search query, video ID, or channel ID")
298
+ parser.add_argument("--limit", type=int, default=10, help="Max results for search (default: 10)")
299
+ parser.add_argument(
300
+ "--order",
301
+ choices=["relevance", "date", "rating", "viewCount", "title"],
302
+ default="relevance",
303
+ help="Sort order for search (default: relevance)",
304
+ )
305
+ parser.add_argument("--api-key", help="API key override")
306
+ parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
307
+
308
+ args = parser.parse_args()
309
+
310
+ if args.command == "search":
311
+ result = search_videos(args.query, max_results=args.limit, order=args.order, api_key=args.api_key)
312
+ elif args.command == "video":
313
+ result = get_video_details(args.query, api_key=args.api_key)
314
+ elif args.command == "channel":
315
+ result = get_channel_info(args.query, api_key=args.api_key)
316
+
317
+ if result.get("error"):
318
+ print(f"Error: {result['error']}", file=sys.stderr)
319
+ if not args.json:
320
+ sys.exit(1)
321
+
322
+ if args.json:
323
+ print(json.dumps(result, indent=2))
324
+ else:
325
+ if args.command == "search":
326
+ print(f"=== YouTube Search: {args.query} ===")
327
+ print(f"Results: {result.get('total_results', 0):,}")
328
+ for i, v in enumerate(result.get("videos", []), 1):
329
+ print(f"\n {i}. {v['title']}")
330
+ print(f" {v['channel']} | {v['views']:,} views | {v['likes']:,} likes | {v['duration']}")
331
+ print(f" {v['url']}")
332
+ elif args.command == "video":
333
+ d = result.get("details", {})
334
+ if d:
335
+ print(f"=== {d.get('title')} ===")
336
+ print(f"Channel: {d.get('channel')}")
337
+ print(f"Views: {d.get('views', 0):,} | Likes: {d.get('likes', 0):,} | Comments: {d.get('comments_count', 0):,}")
338
+ print(f"Published: {d.get('published', '')[:10]} | Duration: {d.get('duration')}")
339
+ tags = d.get("tags", [])
340
+ if tags:
341
+ print(f"Tags: {', '.join(tags[:10])}")
342
+ comments = result.get("comments", [])
343
+ if comments:
344
+ print(f"\nTop Comments ({len(comments)}):")
345
+ for c in comments[:5]:
346
+ print(f" [{c['likes']} likes] {c['author']}: {c['text'][:100]}")
347
+ elif args.command == "channel":
348
+ ch = result.get("channel", {})
349
+ if ch:
350
+ print(f"=== {ch.get('title')} ===")
351
+ print(f"Subscribers: {ch.get('subscribers', 0):,} | Videos: {ch.get('videos', 0):,} | Views: {ch.get('views', 0):,}")
352
+
353
+
354
+ if __name__ == "__main__":
355
+ main()
@@ -0,0 +1,192 @@
1
+ ---
2
+ name: seo-hreflang
3
+ description: >
4
+ Hreflang and international SEO audit, validation, and generation. Detects
5
+ common mistakes, validates language/region codes, and generates correct
6
+ hreflang implementations. Use when user says "hreflang", "i18n SEO",
7
+ "international SEO", "multi-language", "multi-region", or "language tags".
8
+ ---
9
+
10
+ # Hreflang & International SEO
11
+
12
+ Validate existing hreflang implementations or generate correct hreflang tags
13
+ for multi-language and multi-region sites. Supports HTML, HTTP header, and
14
+ XML sitemap implementations.
15
+
16
+ ## Validation Checks
17
+
18
+ ### 1. Self-Referencing Tags
19
+ - Every page must include an hreflang tag pointing to itself
20
+ - The self-referencing URL must exactly match the page's canonical URL
21
+ - Missing self-referencing tags cause Google to ignore the entire hreflang set
22
+
23
+ ### 2. Return Tags
24
+ - If page A links to page B with hreflang, page B must link back to page A
25
+ - Every hreflang relationship must be bidirectional (A→B and B→A)
26
+ - Missing return tags invalidate the hreflang signal for both pages
27
+ - Check all language versions reference each other (full mesh)
28
+
29
+ ### 3. x-default Tag
30
+ - Required: designates the fallback page for unmatched languages/regions
31
+ - Typically points to the language selector page or English version
32
+ - Only one x-default per set of alternates
33
+ - Must also have return tags from all other language versions
34
+
35
+ ### 4. Language Code Validation
36
+ - Must use ISO 639-1 two-letter codes (e.g., `en`, `fr`, `de`, `ja`)
37
+ - Common errors:
38
+ - `eng` instead of `en` (ISO 639-2, not valid for hreflang)
39
+ - `jp` instead of `ja` (incorrect code for Japanese)
40
+ - `zh` without region qualifier (ambiguous; use `zh-Hans` or `zh-Hant`)
41
+
42
+ ### 5. Region Code Validation
43
+ - Optional region qualifier uses ISO 3166-1 Alpha-2 (e.g., `en-US`, `en-GB`, `pt-BR`)
44
+ - Format: `language-REGION` (lowercase language, uppercase region)
45
+ - Common errors:
46
+ - `en-uk` instead of `en-GB` (UK is not a valid ISO 3166-1 code)
47
+ - `es-LA` (Latin America is not a country; use specific countries)
48
+ - Region without language prefix
49
+
50
+ ### 6. Canonical URL Alignment
51
+ - Hreflang tags must only appear on canonical URLs
52
+ - If a page has `rel=canonical` pointing elsewhere, hreflang on that page is ignored
53
+ - The canonical URL and hreflang URL must match exactly (including trailing slashes)
54
+ - Non-canonical pages should not be in any hreflang set
55
+
56
+ ### 7. Protocol Consistency
57
+ - All URLs in an hreflang set must use the same protocol (HTTPS or HTTP)
58
+ - Mixed HTTP/HTTPS in hreflang sets causes validation failures
59
+ - After HTTPS migration, update all hreflang tags to HTTPS
60
+
61
+ ### 8. Cross-Domain Support
62
+ - Hreflang works across different domains (e.g., example.com and example.de)
63
+ - Cross-domain hreflang requires return tags on both domains
64
+ - Verify both domains are verified in Google Search Console
65
+ - Sitemap-based implementation recommended for cross-domain setups
66
+
67
+ ## Common Mistakes
68
+
69
+ | Issue | Severity | Fix |
70
+ |-------|----------|-----|
71
+ | Missing self-referencing tag | Critical | Add hreflang pointing to same page URL |
72
+ | Missing return tags (A→B but no B→A) | Critical | Add matching return tags on all alternates |
73
+ | Missing x-default | High | Add x-default pointing to fallback/selector page |
74
+ | Invalid language code (e.g., `eng`) | High | Use ISO 639-1 two-letter codes |
75
+ | Invalid region code (e.g., `en-uk`) | High | Use ISO 3166-1 Alpha-2 codes |
76
+ | Hreflang on non-canonical URL | High | Move hreflang to canonical URL only |
77
+ | HTTP/HTTPS mismatch in URLs | Medium | Standardize all URLs to HTTPS |
78
+ | Trailing slash inconsistency | Medium | Match canonical URL format exactly |
79
+ | Hreflang in both HTML and sitemap | Low | Choose one method (sitemap preferred for large sites) |
80
+ | Language without region when needed | Low | Add region qualifier for geo-targeted content |
81
+
82
+ ## Implementation Methods
83
+
84
+ ### Method 1: HTML Link Tags
85
+ Best for: Sites with <50 language/region variants per page.
86
+
87
+ ```html
88
+ <link rel="alternate" hreflang="en-US" href="https://example.com/page" />
89
+ <link rel="alternate" hreflang="en-GB" href="https://example.co.uk/page" />
90
+ <link rel="alternate" hreflang="fr" href="https://example.com/fr/page" />
91
+ <link rel="alternate" hreflang="x-default" href="https://example.com/page" />
92
+ ```
93
+
94
+ Place in `<head>` section. Every page must include all alternates including itself.
95
+
96
+ ### Method 2: HTTP Headers
97
+ Best for: Non-HTML files (PDFs, documents).
98
+
99
+ ```
100
+ Link: <https://example.com/page>; rel="alternate"; hreflang="en-US",
101
+ <https://example.com/fr/page>; rel="alternate"; hreflang="fr",
102
+ <https://example.com/page>; rel="alternate"; hreflang="x-default"
103
+ ```
104
+
105
+ Set via server configuration or CDN rules.
106
+
107
+ ### Method 3: XML Sitemap (Recommended for large sites)
108
+ Best for: Sites with many language variants, cross-domain setups, or 50+ pages.
109
+
110
+ See Hreflang Sitemap Generation section below.
111
+
112
+ ### Method Comparison
113
+ | Method | Best For | Pros | Cons |
114
+ |--------|----------|------|------|
115
+ | HTML link tags | Small sites (<50 variants) | Easy to implement, visible in source | Bloats `<head>`, hard to maintain at scale |
116
+ | HTTP headers | Non-HTML files | Works for PDFs, images | Complex server config, not visible in HTML |
117
+ | XML sitemap | Large sites, cross-domain | Scalable, centralized management | Not visible on page, requires sitemap maintenance |
118
+
119
+ ## Hreflang Generation
120
+
121
+ ### Process
122
+ 1. **Detect languages**: Scan site for language indicators (URL path, subdomain, TLD, HTML lang attribute)
123
+ 2. **Map page equivalents**: Match corresponding pages across languages/regions
124
+ 3. **Validate language codes**: Verify all codes against ISO 639-1 and ISO 3166-1
125
+ 4. **Generate tags**: Create hreflang tags for each page including self-referencing
126
+ 5. **Verify return tags**: Confirm all relationships are bidirectional
127
+ 6. **Add x-default**: Set fallback for each page set
128
+ 7. **Output**: Generate implementation code (HTML, HTTP headers, or sitemap XML)
129
+
130
+ ## Hreflang Sitemap Generation
131
+
132
+ ### Sitemap with Hreflang
133
+ ```xml
134
+ <?xml version="1.0" encoding="UTF-8"?>
135
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
136
+ xmlns:xhtml="http://www.w3.org/1999/xhtml">
137
+ <url>
138
+ <loc>https://example.com/page</loc>
139
+ <xhtml:link rel="alternate" hreflang="en-US" href="https://example.com/page" />
140
+ <xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr/page" />
141
+ <xhtml:link rel="alternate" hreflang="de" href="https://example.de/page" />
142
+ <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/page" />
143
+ </url>
144
+ <url>
145
+ <loc>https://example.com/fr/page</loc>
146
+ <xhtml:link rel="alternate" hreflang="en-US" href="https://example.com/page" />
147
+ <xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr/page" />
148
+ <xhtml:link rel="alternate" hreflang="de" href="https://example.de/page" />
149
+ <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/page" />
150
+ </url>
151
+ </urlset>
152
+ ```
153
+
154
+ Key rules:
155
+ - Include the `xmlns:xhtml` namespace declaration
156
+ - Every `<url>` entry must include ALL language alternates (including itself)
157
+ - Each alternate must appear as a separate `<url>` entry with its own full set
158
+ - Split at 50,000 URLs per sitemap file
159
+
160
+ ## Output
161
+
162
+ ### Hreflang Validation Report
163
+
164
+ #### Summary
165
+ - Total pages scanned: XX
166
+ - Language variants detected: XX
167
+ - Issues found: XX (Critical: X, High: X, Medium: X, Low: X)
168
+
169
+ #### Validation Results
170
+ | Language | URL | Self-Ref | Return Tags | x-default | Status |
171
+ |----------|-----|----------|-------------|-----------|--------|
172
+ | en-US | https://... | ✅ | ✅ | ✅ | ✅ |
173
+ | fr | https://... | ❌ | ⚠️ | ✅ | ❌ |
174
+ | de | https://... | ✅ | ❌ | ✅ | ❌ |
175
+
176
+ ### Generated Hreflang Tags
177
+ - HTML `<link>` tags (if HTML method chosen)
178
+ - HTTP header values (if header method chosen)
179
+ - `hreflang-sitemap.xml` (if sitemap method chosen)
180
+
181
+ ### Recommendations
182
+ - Missing implementations to add
183
+ - Incorrect codes to fix
184
+ - Method migration suggestions (e.g., HTML to sitemap for scale)
185
+
186
+ ## Error Handling
187
+
188
+ | Scenario | Action |
189
+ |----------|--------|
190
+ | URL unreachable (DNS failure, connection refused) | Report the error clearly. Do not guess site structure. Suggest the user verify the URL and try again. |
191
+ | No hreflang tags found | Report the absence. Check for other internationalization signals (subdirectories, subdomains, ccTLDs) and recommend the appropriate hreflang implementation method. |
192
+ | Invalid language/region codes detected | List each invalid code with the correct replacement. Provide a corrected hreflang tag set ready to implement. |