bmad-plus 0.3.3 → 0.4.1

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.
Files changed (62) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +12 -56
  3. package/osint-agent-package/skills/bmad-osint-investigate/osint/SKILL.md +452 -452
  4. package/osint-agent-package/skills/bmad-osint-investigate/osint/assets/dossier-template.md +116 -116
  5. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/content-extraction.md +100 -100
  6. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/platforms.md +130 -130
  7. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/psychoprofile.md +69 -69
  8. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/tools.md +281 -281
  9. package/osint-agent-package/skills/bmad-osint-investigate/osint/scripts/mcp-client.py +136 -136
  10. package/package.json +1 -1
  11. package/readme-international/README.de.md +1 -1
  12. package/readme-international/README.es.md +1 -1
  13. package/readme-international/README.fr.md +1 -1
  14. package/tools/cli/commands/install.js +74 -46
  15. package/tools/cli/i18n.js +501 -0
  16. package/oveanet-pack/animated-website/DEPLOYMENT.md +0 -104
  17. package/oveanet-pack/animated-website/README.md +0 -63
  18. package/oveanet-pack/animated-website/agent/animated-website-agent.md +0 -325
  19. package/oveanet-pack/animated-website/agent.yaml +0 -63
  20. package/oveanet-pack/animated-website/templates/animated-website-workflow.md +0 -55
  21. package/oveanet-pack/seo-audit-360/DEPLOYMENT.md +0 -115
  22. package/oveanet-pack/seo-audit-360/README.md +0 -66
  23. package/oveanet-pack/seo-audit-360/SKILL.md +0 -171
  24. package/oveanet-pack/seo-audit-360/agent/seo-chief.md +0 -294
  25. package/oveanet-pack/seo-audit-360/agent/seo-judge.md +0 -241
  26. package/oveanet-pack/seo-audit-360/agent/seo-scout.md +0 -171
  27. package/oveanet-pack/seo-audit-360/agent.yaml +0 -70
  28. package/oveanet-pack/seo-audit-360/checklist.md +0 -140
  29. package/oveanet-pack/seo-audit-360/hooks/seo-check.sh +0 -95
  30. package/oveanet-pack/seo-audit-360/pagespeed-playbook.md +0 -320
  31. package/oveanet-pack/seo-audit-360/ref/audit-schema.json +0 -187
  32. package/oveanet-pack/seo-audit-360/ref/cwv-thresholds.md +0 -87
  33. package/oveanet-pack/seo-audit-360/ref/eeat-criteria.md +0 -123
  34. package/oveanet-pack/seo-audit-360/ref/geo-signals.md +0 -167
  35. package/oveanet-pack/seo-audit-360/ref/hreflang-rules.md +0 -153
  36. package/oveanet-pack/seo-audit-360/ref/quality-gates.md +0 -133
  37. package/oveanet-pack/seo-audit-360/ref/schema-catalog.md +0 -91
  38. package/oveanet-pack/seo-audit-360/ref/schema-templates.json +0 -356
  39. package/oveanet-pack/seo-audit-360/requirements.txt +0 -14
  40. package/oveanet-pack/seo-audit-360/scripts/__pycache__/seo_crawl.cpython-314.pyc +0 -0
  41. package/oveanet-pack/seo-audit-360/scripts/__pycache__/seo_parse.cpython-314.pyc +0 -0
  42. package/oveanet-pack/seo-audit-360/scripts/install.ps1 +0 -53
  43. package/oveanet-pack/seo-audit-360/scripts/install.sh +0 -48
  44. package/oveanet-pack/seo-audit-360/scripts/seo_apis.py +0 -464
  45. package/oveanet-pack/seo-audit-360/scripts/seo_crawl.py +0 -282
  46. package/oveanet-pack/seo-audit-360/scripts/seo_fetch.py +0 -231
  47. package/oveanet-pack/seo-audit-360/scripts/seo_parse.py +0 -255
  48. package/oveanet-pack/seo-audit-360/scripts/seo_report.py +0 -403
  49. package/oveanet-pack/seo-audit-360/scripts/seo_screenshot.py +0 -202
  50. package/oveanet-pack/seo-audit-360/templates/seo-audit-workflow.md +0 -241
  51. package/oveanet-pack/seo-audit-360/tests/__pycache__/test_crawl.cpython-314-pytest-9.0.2.pyc +0 -0
  52. package/oveanet-pack/seo-audit-360/tests/__pycache__/test_parse.cpython-314-pytest-9.0.2.pyc +0 -0
  53. package/oveanet-pack/seo-audit-360/tests/fixtures/sample_page.html +0 -62
  54. package/oveanet-pack/seo-audit-360/tests/test_apis.py +0 -75
  55. package/oveanet-pack/seo-audit-360/tests/test_crawl.py +0 -121
  56. package/oveanet-pack/seo-audit-360/tests/test_fetch.py +0 -70
  57. package/oveanet-pack/seo-audit-360/tests/test_parse.py +0 -184
  58. package/oveanet-pack/universal-backup/DEPLOYMENT.md +0 -80
  59. package/oveanet-pack/universal-backup/README.md +0 -58
  60. package/oveanet-pack/universal-backup/agent/backup-agent.md +0 -71
  61. package/oveanet-pack/universal-backup/agent.yaml +0 -45
  62. package/oveanet-pack/universal-backup/templates/backup-workflow.md +0 -51
@@ -1,202 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- SEO Screenshot — Viewport screenshot capture for visual SEO analysis.
4
-
5
- Features:
6
- - Mobile and desktop viewport presets
7
- - Above-the-fold element detection
8
- - Full-page capture option
9
- - PNG output with configurable quality
10
-
11
- Requires: playwright (pip install playwright && playwright install chromium)
12
-
13
- Author: Laurent Rochetta
14
- License: MIT
15
- """
16
-
17
- import argparse
18
- import sys
19
-
20
-
21
- VIEWPORTS = {
22
- "mobile": {"width": 375, "height": 812, "device_scale_factor": 3, "is_mobile": True},
23
- "tablet": {"width": 768, "height": 1024, "device_scale_factor": 2, "is_mobile": True},
24
- "desktop": {"width": 1440, "height": 900, "device_scale_factor": 1, "is_mobile": False},
25
- "desktop-hd": {"width": 1920, "height": 1080, "device_scale_factor": 1, "is_mobile": False},
26
- }
27
-
28
-
29
- def capture_screenshot(
30
- url: str,
31
- output: str = "screenshot.png",
32
- viewport: str = "desktop",
33
- full_page: bool = False,
34
- wait_ms: int = 2000,
35
- ):
36
- """
37
- Capture a viewport screenshot of a URL using Playwright.
38
-
39
- Args:
40
- url: URL to capture
41
- output: Output file path (.png)
42
- viewport: Viewport preset (mobile, tablet, desktop, desktop-hd)
43
- full_page: Capture full page scroll or just viewport
44
- wait_ms: Wait time after page load (ms)
45
- """
46
- try:
47
- from playwright.sync_api import sync_playwright
48
- except ImportError:
49
- print(
50
- "Error: playwright required.\n"
51
- "Install: pip install playwright && playwright install chromium",
52
- file=sys.stderr,
53
- )
54
- sys.exit(1)
55
-
56
- vp = VIEWPORTS.get(viewport, VIEWPORTS["desktop"])
57
-
58
- with sync_playwright() as p:
59
- browser = p.chromium.launch(headless=True)
60
- context = browser.new_context(
61
- viewport={"width": vp["width"], "height": vp["height"]},
62
- device_scale_factor=vp["device_scale_factor"],
63
- is_mobile=vp["is_mobile"],
64
- user_agent=(
65
- "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) "
66
- "AppleWebKit/605.1.15 Mobile/15E148 Safari/604.1"
67
- if vp["is_mobile"]
68
- else "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
69
- "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 BMADSEOEngine/2.0"
70
- ),
71
- )
72
-
73
- page = context.new_page()
74
-
75
- try:
76
- page.goto(url, wait_until="networkidle", timeout=30000)
77
- except Exception:
78
- # Fallback: wait for load event instead
79
- page.goto(url, wait_until="load", timeout=30000)
80
-
81
- # Wait for dynamic content
82
- page.wait_for_timeout(wait_ms)
83
-
84
- # Capture screenshot
85
- page.screenshot(path=output, full_page=full_page)
86
-
87
- # Gather above-the-fold metrics
88
- metrics = page.evaluate("""() => {
89
- const viewportHeight = window.innerHeight;
90
- const viewportWidth = window.innerWidth;
91
-
92
- // Find CTAs above the fold
93
- const ctas = [];
94
- const buttons = document.querySelectorAll('a, button, [role="button"]');
95
- buttons.forEach(el => {
96
- const rect = el.getBoundingClientRect();
97
- if (rect.top < viewportHeight && rect.bottom > 0) {
98
- const text = el.textContent.trim().substring(0, 50);
99
- if (text && (
100
- /sign.?up|get.?start|try|buy|contact|demo|free|download|subscribe/i.test(text)
101
- )) {
102
- ctas.push({
103
- text: text,
104
- tag: el.tagName,
105
- top: Math.round(rect.top),
106
- visible: rect.width > 0 && rect.height > 0,
107
- });
108
- }
109
- }
110
- });
111
-
112
- // Find hero/LCP candidate
113
- const images = document.querySelectorAll('img');
114
- let largestImage = null;
115
- let largestArea = 0;
116
- images.forEach(img => {
117
- const rect = img.getBoundingClientRect();
118
- const area = rect.width * rect.height;
119
- if (area > largestArea && rect.top < viewportHeight) {
120
- largestArea = area;
121
- largestImage = {
122
- src: img.src.substring(0, 100),
123
- width: Math.round(rect.width),
124
- height: Math.round(rect.height),
125
- top: Math.round(rect.top),
126
- };
127
- }
128
- });
129
-
130
- // Check for horizontal scroll
131
- const hasHorizontalScroll = document.documentElement.scrollWidth > viewportWidth;
132
-
133
- // Font size check
134
- const body = document.body;
135
- const bodyFontSize = body ? parseFloat(getComputedStyle(body).fontSize) : 16;
136
-
137
- return {
138
- viewportWidth,
139
- viewportHeight,
140
- ctas_above_fold: ctas.length,
141
- cta_details: ctas.slice(0, 5),
142
- largest_image_above_fold: largestImage,
143
- has_horizontal_scroll: hasHorizontalScroll,
144
- body_font_size_px: bodyFontSize,
145
- dom_element_count: document.querySelectorAll('*').length,
146
- };
147
- }""")
148
-
149
- browser.close()
150
-
151
- return metrics
152
-
153
-
154
- # ── CLI ────────────────────────────────────────────────────────────
155
-
156
- def main():
157
- parser = argparse.ArgumentParser(
158
- description="SEO Screenshot — Viewport capture (BMAD+ SEO Engine)"
159
- )
160
- parser.add_argument("url", help="URL to capture")
161
- parser.add_argument("--output", "-o", default="screenshot.png", help="Output file path")
162
- parser.add_argument(
163
- "--viewport", "-v",
164
- choices=list(VIEWPORTS.keys()), default="desktop",
165
- help="Viewport preset"
166
- )
167
- parser.add_argument("--full", action="store_true", help="Capture full page (not just viewport)")
168
- parser.add_argument("--wait", "-w", type=int, default=2000, help="Wait after load (ms)")
169
- parser.add_argument("--json", "-j", action="store_true", help="Output metrics as JSON")
170
-
171
- args = parser.parse_args()
172
-
173
- import json
174
-
175
- metrics = capture_screenshot(
176
- url=args.url,
177
- output=args.output,
178
- viewport=args.viewport,
179
- full_page=args.full,
180
- wait_ms=args.wait,
181
- )
182
-
183
- print(f"Screenshot saved: {args.output}", file=sys.stderr)
184
-
185
- if args.json:
186
- print(json.dumps(metrics, indent=2))
187
- else:
188
- print(f"\nAbove-the-Fold Analysis ({args.viewport}):")
189
- print(f" Viewport: {metrics['viewportWidth']}×{metrics['viewportHeight']}")
190
- print(f" CTAs above fold: {metrics['ctas_above_fold']}")
191
- for cta in metrics.get("cta_details", []):
192
- print(f" - \"{cta['text']}\" ({cta['tag']}, top: {cta['top']}px)")
193
- if metrics.get("largest_image_above_fold"):
194
- img = metrics["largest_image_above_fold"]
195
- print(f" Largest image: {img['width']}×{img['height']} at y={img['top']}px")
196
- print(f" Horizontal scroll: {'⚠️ YES' if metrics['has_horizontal_scroll'] else '✅ No'}")
197
- print(f" Body font size: {metrics['body_font_size_px']}px {'✅' if metrics['body_font_size_px'] >= 16 else '⚠️ <16px'}")
198
- print(f" DOM elements: {metrics['dom_element_count']:,}")
199
-
200
-
201
- if __name__ == "__main__":
202
- main()
@@ -1,241 +0,0 @@
1
- # SEO Audit Workflow — BMAD+ SEO Engine v2.0
2
-
3
- > Author: Laurent Rochetta | By Oveanet × Laurent Rochetta
4
-
5
- ## Overview
6
-
7
- This workflow orchestrates the 3 SEO Engine agents (Scout, Judge, Chief) through 6 phases to produce a comprehensive audit with scored results and actionable fixes.
8
-
9
- ---
10
-
11
- ## Phase 1 — Reconnaissance (Scout solo)
12
-
13
- **Duration**: ~2 min | **Agent**: Scout (Crawler role)
14
-
15
- 1. Fetch homepage + `/robots.txt` + `/sitemap.xml`
16
- 2. Detect business type (SaaS, e-commerce, local, publisher, agency)
17
- 3. Mini-crawl: discover 10–25 pages via sitemap + internal links
18
- 4. Detect rendering architecture (SSR / CSR / hybrid)
19
- 5. Check for `/llms.txt` and `/llms-full.txt`
20
-
21
- **Checkpoint**: "Identified a [type] site with [N] pages. Continue full audit?"
22
-
23
- **Tools**:
24
- ```bash
25
- python scripts/seo_fetch.py [URL] --json
26
- python scripts/seo_crawl.py [URL] --depth 2 --max 25 --json
27
- ```
28
-
29
- ---
30
-
31
- ## Phase 2 — Deep Scan (Scout + Judge in PARALLEL)
32
-
33
- **Duration**: ~5 min | **Agents**: Scout (Inspector) + Judge (Content Expert + Schema Master)
34
-
35
- ### Scout inspects (9 categories):
36
- 1. Crawlability (robots.txt, noindex, crawl depth)
37
- 2. Indexability (canonicals, duplicates, pagination)
38
- 3. Security (HTTPS, HSTS, CSP, X-Frame-Options)
39
- 4. URL Structure (clean URLs, redirects, trailing slashes)
40
- 5. Mobile (viewport, touch targets, font size)
41
- 6. Core Web Vitals signals from source HTML
42
- 7. Structured Data detection (pass to Judge)
43
- 8. JavaScript rendering (CSR/SSR, SPA detection)
44
- 9. IndexNow protocol
45
-
46
- ### Judge analyzes (in parallel):
47
- 1. E-E-A-T evaluation (Experience, Expertise, Authority, Trust)
48
- 2. Content quality (word count, readability, keyword optimization)
49
- 3. Schema validation (JSON-LD, types, deprecation status)
50
- 4. Image audit (alt text, dimensions, format, lazy loading)
51
- 5. Internal/external link analysis
52
- 6. AI content detection markers
53
-
54
- **Tools**:
55
- ```bash
56
- python scripts/seo_parse.py page.html --url [URL] --json
57
- python scripts/seo_screenshot.py [URL] --viewport mobile --json
58
- python scripts/seo_screenshot.py [URL] --viewport desktop --json
59
- ```
60
-
61
- ---
62
-
63
- ## Phase 3 — AI Readiness & GEO (Judge solo)
64
-
65
- **Duration**: ~3 min | **Agent**: Judge (GEO Analyst role)
66
-
67
- 1. Check AI crawler access (GPTBot, ClaudeBot, PerplexityBot, OAI-SearchBot)
68
- 2. Verify llms.txt compliance
69
- 3. Check RSL 1.0 licensing
70
- 4. Score passage-level citability (134–167 word blocks)
71
- 5. Analyze brand mention signals
72
- 6. Compute **AI Readiness Score (0–100)**
73
-
74
- Reference: `ref/geo-signals.md`
75
-
76
- ---
77
-
78
- ## Phase 4 — Scoring & Synthesis (Chief solo)
79
-
80
- **Duration**: ~2 min | **Agent**: Chief (Scorer role)
81
-
82
- Compute **SEO Health Score (0–100)** from weighted categories:
83
-
84
- | Category | Weight |
85
- |----------|--------|
86
- | Technical SEO | 20% |
87
- | Content & E-E-A-T | 22% |
88
- | On-Page SEO | 18% |
89
- | Schema | 10% |
90
- | Performance (CWV) | 12% |
91
- | AI Readiness (GEO) | 12% |
92
- | Images | 6% |
93
-
94
- Output: Score breakdown with visual indicators per category.
95
-
96
- ---
97
-
98
- ## Phase 5 — Action Plan & Auto-Fixes (Chief solo)
99
-
100
- **Duration**: ~3 min | **Agent**: Chief (Strategist role)
101
-
102
- 1. Classify all issues: 🔴 Critical → 🟠 High → 🟡 Medium → 🟢 Low
103
- 2. Identify Quick Wins (high impact / low effort)
104
- 3. Generate 30/60/90-day roadmap
105
- 4. **Auto-generate code fixes** for:
106
- - Missing/broken meta tags (title, description)
107
- - Schema JSON-LD blocks (from templates)
108
- - robots.txt improvements (AI crawler access)
109
- - llms.txt file generation
110
- - Image alt text suggestions
111
- 5. **Checkpoint**: "Here's the plan. Want me to apply the fixes automatically?"
112
-
113
- ---
114
-
115
- ## Phase 5b — PageSpeed Perfection Loop (Scout + Chief iterative)
116
-
117
- > **This is the battle-tested loop from our oveanet.ch optimization.**
118
- > Goal: Achieve **100% on all 4 PageSpeed Insights categories** (Performance, Accessibility, Best Practices, SEO).
119
-
120
- ### The Loop
121
-
122
- ```
123
- ┌─────────────────────────────────────┐
124
- │ 1. Run PageSpeed Insights audit │
125
- │ (via API or manual) │
126
- │ │ │
127
- │ ▼ │
128
- │ 2. Parse failing audits │
129
- │ Group by category + priority │
130
- │ │ │
131
- │ ▼ │
132
- │ 3. Apply top-priority fix │
133
- │ (one fix at a time) │
134
- │ │ │
135
- │ ▼ │
136
- │ 4. Re-run PageSpeed │
137
- │ Verify fix + no regressions │
138
- │ │ │
139
- │ ▼ │
140
- │ 5. Score improved? │
141
- │ YES → Continue to next fix │
142
- │ NO → Revert and try different │
143
- │ approach │
144
- │ │ │
145
- │ ▼ │
146
- │ 6. All 4 categories = 100%? │
147
- │ YES → Done ✅ │
148
- │ NO → Go to step 2 │
149
- └─────────────────────────────────────┘
150
- ```
151
-
152
- ### PageSpeed Fix Priority Order
153
- 1. **Performance** (hardest — tackle first):
154
- - Eliminate render-blocking resources
155
- - Properly size images (WebP/AVIF + responsive)
156
- - Reduce unused CSS/JS
157
- - Defer offscreen images
158
- - Minimize main-thread work
159
- - Reduce server response time (TTFB)
160
- - Preload LCP image
161
-
162
- 2. **Accessibility** (usually quick wins):
163
- - Add alt text to all images
164
- - Fix color contrast ratios (4.5:1 minimum)
165
- - Add ARIA labels to interactive elements
166
- - Ensure heading hierarchy
167
- - Add lang attribute to HTML
168
- - Ensure form labels
169
-
170
- 3. **Best Practices**:
171
- - HTTPS + no mixed content
172
- - No browser errors in console
173
- - Remove deprecated APIs
174
- - Add proper CSP headers
175
-
176
- 4. **SEO** (usually easiest):
177
- - Add meta description
178
- - Ensure crawlable links
179
- - Valid robots.txt
180
- - Proper viewport meta
181
- - Descriptive link text
182
-
183
- ### Key Rules
184
- - **One fix at a time** — never batch multiple changes, you need to isolate impact
185
- - **Always re-test** — PageSpeed scores can regress with seemingly unrelated changes
186
- - **Mobile first** — always test mobile viewport (Google uses mobile for indexing)
187
- - **Field vs Lab** — Lab scores (Lighthouse) can differ from field data (CrUX). Target lab 100% first
188
-
189
- ---
190
-
191
- ## Phase 6 — Monitoring (Scout, optional)
192
-
193
- **Duration**: ongoing | **Agent**: Scout (Crawler role)
194
-
195
- 1. Save audit results to `.bmad-seo/history/[domain]-[date].json`
196
- 2. On re-audit: compare with previous results
197
- 3. Track: issues resolved, new issues, score evolution
198
- 4. Generate progress report with deltas
199
-
200
- ---
201
-
202
- ## Command Quick Reference
203
-
204
- ```bash
205
- # Full audit (all 6 phases)
206
- /seo full https://example.com
207
-
208
- # Quick audit (Phases 1-4 only)
209
- /seo quick https://example.com
210
-
211
- # Individual commands
212
- /seo technical https://example.com
213
- /seo content https://example.com
214
- /seo geo https://example.com
215
- /seo schema https://example.com
216
- /seo images https://example.com
217
- /seo hreflang https://example.com
218
-
219
- # PageSpeed perfection loop
220
- /seo pagespeed https://example.com
221
-
222
- # Strategic planning
223
- /seo plan saas|ecommerce|local|publisher|agency
224
-
225
- # Auto-fix generation
226
- /seo fix
227
-
228
- # Monitoring
229
- /seo history
230
- /seo compare
231
- ```
232
-
233
- ---
234
-
235
- ## Tips for Best Results
236
-
237
- 1. **Start with `/seo full`** for the first audit — it gives you the complete picture
238
- 2. **Use `/seo pagespeed`** after fixing major issues to chase 100% scores
239
- 3. **Re-run monthly** with `/seo compare` to track progress
240
- 4. **Feed the AI crawlers**: Allow GPTBot + ClaudeBot + PerplexityBot in robots.txt
241
- 5. **Check GEO separately**: AI search visibility evolves fast, audit quarterly with `/seo geo`
@@ -1,62 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <title>SEO Test Page — BMAD+ Fixture</title>
6
- <meta name="description" content="A test page for validating the SEO parse module with known elements.">
7
- <meta name="robots" content="index, follow">
8
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
9
- <meta property="og:title" content="SEO Test Page">
10
- <meta property="og:type" content="website">
11
- <meta property="og:url" content="https://example.com/test">
12
- <meta name="twitter:card" content="summary_large_image">
13
- <meta name="twitter:title" content="SEO Test Page">
14
- <link rel="canonical" href="https://example.com/test">
15
- <link rel="alternate" hreflang="en" href="https://example.com/en/test">
16
- <link rel="alternate" hreflang="fr" href="https://example.com/fr/test">
17
- <link rel="alternate" hreflang="x-default" href="https://example.com/test">
18
- </head>
19
- <body>
20
- <h1>Main Heading of the Page</h1>
21
- <p>This is a test paragraph with enough words to verify word count functionality in the parser module. We need at least a few sentences to make the test meaningful and realistic.</p>
22
-
23
- <h2>Second Level Heading One</h2>
24
- <p>Content under the first H2. This paragraph adds more text to increase the word count.</p>
25
-
26
- <h2>Second Level Heading Two</h2>
27
- <p>Another section with different content about SEO analysis and testing.</p>
28
-
29
- <h3>Third Level Heading</h3>
30
- <p>Detailed information under the H3 heading for testing hierarchy detection.</p>
31
-
32
- <img src="/images/hero.jpg" alt="Hero image for testing" width="800" height="400" loading="lazy">
33
- <img src="/images/no-alt.jpg" width="200" height="200">
34
- <img src="/images/empty-alt.jpg" alt="" width="100" height="100">
35
-
36
- <a href="https://example.com/about">About Us</a>
37
- <a href="https://example.com/services">Our Services</a>
38
- <a href="https://external.com/partner" rel="nofollow" target="_blank">Partner Link</a>
39
- <a href="/relative-link">Relative Link</a>
40
-
41
- <script type="application/ld+json">
42
- {
43
- "@context": "https://schema.org",
44
- "@type": "Organization",
45
- "name": "Test Company",
46
- "url": "https://example.com",
47
- "logo": "https://example.com/logo.png"
48
- }
49
- </script>
50
-
51
- <script type="application/ld+json">
52
- {
53
- "@context": "https://schema.org",
54
- "@type": "BreadcrumbList",
55
- "itemListElement": [
56
- {"@type": "ListItem", "position": 1, "name": "Home", "item": "https://example.com"},
57
- {"@type": "ListItem", "position": 2, "name": "Test", "item": "https://example.com/test"}
58
- ]
59
- }
60
- </script>
61
- </body>
62
- </html>
@@ -1,75 +0,0 @@
1
- """
2
- Tests for seo_apis.py — API response parsing and error handling.
3
-
4
- Author: Laurent Rochetta
5
- """
6
-
7
- import sys
8
- import os
9
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))
10
-
11
- # Temporarily unset API key for error tests
12
- original_key = os.environ.get("GOOGLE_API_KEY", "")
13
-
14
-
15
- class TestAPIKeyMissing:
16
- """Test behavior when GOOGLE_API_KEY is not set."""
17
-
18
- def setup_method(self):
19
- os.environ.pop("GOOGLE_API_KEY", None)
20
- # Reimport to pick up empty key
21
- import importlib
22
- import seo_apis
23
- importlib.reload(seo_apis)
24
- self.seo_apis = seo_apis
25
-
26
- def teardown_method(self):
27
- if original_key:
28
- os.environ["GOOGLE_API_KEY"] = original_key
29
-
30
- def test_pagespeed_without_key(self):
31
- # Force the module to use an empty key
32
- self.seo_apis.API_KEY = ""
33
- result = self.seo_apis.run_pagespeed("https://example.com")
34
- assert result.get("error") is not None
35
- assert "GOOGLE_API_KEY" in result["error"]
36
-
37
- def test_crux_without_key(self):
38
- self.seo_apis.API_KEY = ""
39
- result = self.seo_apis.run_crux("https://example.com")
40
- assert result.get("error") is not None
41
-
42
- def test_rich_results_without_key(self):
43
- self.seo_apis.API_KEY = ""
44
- result = self.seo_apis.run_rich_results_test("https://example.com")
45
- assert result.get("error") is not None
46
-
47
-
48
- class TestResultStructure:
49
- """Test that API functions return expected structures."""
50
-
51
- def setup_method(self):
52
- import importlib
53
- import seo_apis
54
- importlib.reload(seo_apis)
55
- self.seo_apis = seo_apis
56
-
57
- def test_pagespeed_result_keys(self):
58
- self.seo_apis.API_KEY = ""
59
- result = self.seo_apis.run_pagespeed("https://example.com")
60
- # Even on error, should have expected structure
61
- assert "error" in result
62
-
63
- def test_crux_result_keys(self):
64
- self.seo_apis.API_KEY = ""
65
- result = self.seo_apis.run_crux("https://example.com")
66
- assert "error" in result
67
-
68
- def test_run_all_structure(self):
69
- self.seo_apis.API_KEY = ""
70
- result = self.seo_apis.run_all("https://example.com")
71
- assert "pagespeed" in result
72
- assert "crux" in result
73
- assert "mobile_friendly" in result
74
- assert "url" in result
75
- assert "timestamp" in result