bmad-plus 0.3.1 → 0.3.2

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 CHANGED
@@ -5,6 +5,17 @@ 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.2] — 2026-03-19
9
+
10
+ ### 📊 SEO Engine — Reports, Competitor & Hreflang (Sprint 2)
11
+
12
+ ### Added
13
+ - **seo_report.py** — Professional HTML report generator with inline SVG radar chart, color-coded issue cards, quick wins section, and print-friendly CSS
14
+ - **Benchmarker role** — Added to Chief agent for `/seo competitor` command (side-by-side site comparison with delta scoring)
15
+ - **hreflang-rules.md** — Complete hreflang audit reference with 7 validation rules, 6 common error patterns, and 12-point checklist
16
+
17
+ ---
18
+
8
19
  ## [0.3.1] — 2026-03-19
9
20
 
10
21
  ### 🔧 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,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 |
@@ -0,0 +1,403 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ SEO Report — Professional HTML audit report generator.
4
+
5
+ Features:
6
+ - Single-file HTML with inline CSS (no external deps)
7
+ - SVG radar chart for score visualization
8
+ - Color-coded issue cards (Critical/High/Medium/Low)
9
+ - Quick Wins section
10
+ - Print-friendly (@media print)
11
+ - Responsive (mobile-readable)
12
+
13
+ Author: Laurent Rochetta
14
+ License: MIT
15
+ """
16
+
17
+ import argparse
18
+ import json
19
+ import math
20
+ import os
21
+ import sys
22
+ from datetime import datetime
23
+
24
+
25
+ def generate_radar_svg(scores: dict, size: int = 300) -> str:
26
+ """Generate an SVG radar chart for the 7 score categories."""
27
+ categories = list(scores.keys())
28
+ values = list(scores.values())
29
+ n = len(categories)
30
+
31
+ if n == 0:
32
+ return ""
33
+
34
+ cx, cy = size // 2, size // 2
35
+ radius = size // 2 - 40
36
+
37
+ # Short labels for display
38
+ short_labels = {
39
+ "technical": "Tech",
40
+ "content_eeat": "E-E-A-T",
41
+ "on_page": "On-Page",
42
+ "schema": "Schema",
43
+ "performance": "Perf",
44
+ "ai_readiness": "AI/GEO",
45
+ "images": "Images",
46
+ }
47
+
48
+ def point(angle_deg, r):
49
+ angle_rad = math.radians(angle_deg - 90)
50
+ x = cx + r * math.cos(angle_rad)
51
+ y = cy + r * math.sin(angle_rad)
52
+ return x, y
53
+
54
+ svg_parts = [f'<svg viewBox="0 0 {size} {size}" xmlns="http://www.w3.org/2000/svg" style="max-width:{size}px;margin:auto;display:block;">']
55
+
56
+ # Background circles
57
+ for pct in [25, 50, 75, 100]:
58
+ r = radius * pct / 100
59
+ svg_parts.append(f'<circle cx="{cx}" cy="{cy}" r="{r}" fill="none" stroke="#e2e8f0" stroke-width="1" opacity="0.5"/>')
60
+
61
+ # Axis lines + labels
62
+ for i in range(n):
63
+ angle = (360 / n) * i
64
+ x1, y1 = point(angle, 0)
65
+ x2, y2 = point(angle, radius)
66
+ svg_parts.append(f'<line x1="{cx}" y1="{cy}" x2="{x2}" y2="{y2}" stroke="#e2e8f0" stroke-width="1"/>')
67
+
68
+ lx, ly = point(angle, radius + 20)
69
+ label = short_labels.get(categories[i], categories[i][:6])
70
+ svg_parts.append(f'<text x="{lx}" y="{ly}" text-anchor="middle" font-size="11" fill="#64748b" font-family="Inter,sans-serif">{label}</text>')
71
+
72
+ # Data polygon
73
+ data_points = []
74
+ for i in range(n):
75
+ angle = (360 / n) * i
76
+ r = radius * min(values[i], 100) / 100
77
+ x, y = point(angle, r)
78
+ data_points.append(f"{x},{y}")
79
+
80
+ poly = " ".join(data_points)
81
+ svg_parts.append(f'<polygon points="{poly}" fill="rgba(59,130,246,0.2)" stroke="#3b82f6" stroke-width="2"/>')
82
+
83
+ # Data points
84
+ for i in range(n):
85
+ angle = (360 / n) * i
86
+ r = radius * min(values[i], 100) / 100
87
+ x, y = point(angle, r)
88
+ color = "#22c55e" if values[i] >= 80 else "#f59e0b" if values[i] >= 50 else "#ef4444"
89
+ svg_parts.append(f'<circle cx="{x}" cy="{y}" r="4" fill="{color}" stroke="white" stroke-width="2"/>')
90
+
91
+ svg_parts.append('</svg>')
92
+ return "\n".join(svg_parts)
93
+
94
+
95
+ def severity_color(severity: str) -> str:
96
+ """Get color for severity level."""
97
+ return {
98
+ "critical": "#ef4444",
99
+ "high": "#f97316",
100
+ "medium": "#f59e0b",
101
+ "low": "#22c55e",
102
+ }.get(severity, "#64748b")
103
+
104
+
105
+ def severity_icon(severity: str) -> str:
106
+ return {
107
+ "critical": "🔴",
108
+ "high": "🟠",
109
+ "medium": "🟡",
110
+ "low": "🟢",
111
+ }.get(severity, "⚪")
112
+
113
+
114
+ def score_color(score: int) -> str:
115
+ if score >= 90:
116
+ return "#22c55e"
117
+ elif score >= 70:
118
+ return "#84cc16"
119
+ elif score >= 50:
120
+ return "#f59e0b"
121
+ else:
122
+ return "#ef4444"
123
+
124
+
125
+ def generate_html_report(audit_data: dict) -> str:
126
+ """Generate a complete HTML report from audit JSON data."""
127
+
128
+ domain = audit_data.get("domain", "Unknown")
129
+ timestamp = audit_data.get("timestamp", datetime.now().isoformat())
130
+ total_score = audit_data.get("score", {}).get("total", 0)
131
+ categories = audit_data.get("score", {}).get("categories", {})
132
+ issues = audit_data.get("issues", [])
133
+ pages = audit_data.get("pages", [])
134
+
135
+ # Generate radar chart
136
+ radar_svg = generate_radar_svg(categories) if categories else ""
137
+
138
+ # Sort issues by severity
139
+ severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
140
+ sorted_issues = sorted(issues, key=lambda x: severity_order.get(x.get("severity", "low"), 4))
141
+
142
+ # Count by severity
143
+ counts = {"critical": 0, "high": 0, "medium": 0, "low": 0}
144
+ for issue in issues:
145
+ sev = issue.get("severity", "low")
146
+ counts[sev] = counts.get(sev, 0) + 1
147
+
148
+ # Quick wins
149
+ quick_wins = [i for i in issues if i.get("quick_win", False)][:5]
150
+
151
+ # Build issue cards HTML
152
+ issue_cards = ""
153
+ for issue in sorted_issues:
154
+ sev = issue.get("severity", "low")
155
+ fix_html = ""
156
+ if issue.get("fix"):
157
+ fix_html = f'<div class="fix-block"><strong>Fix:</strong><pre><code>{issue["fix"]}</code></pre></div>'
158
+
159
+ issue_cards += f'''
160
+ <div class="issue-card" style="border-left: 4px solid {severity_color(sev)}">
161
+ <div class="issue-header">
162
+ <span class="severity-badge" style="background:{severity_color(sev)}">{sev.upper()}</span>
163
+ <span class="issue-category">{issue.get("category", "")}</span>
164
+ </div>
165
+ <h4>{issue.get("title", "")}</h4>
166
+ <p>{issue.get("description", "")}</p>
167
+ {fix_html}
168
+ </div>'''
169
+
170
+ # Quick wins HTML
171
+ qw_html = ""
172
+ if quick_wins:
173
+ qw_items = ""
174
+ for qw in quick_wins:
175
+ qw_items += f'<li>{severity_icon(qw.get("severity", ""))} {qw.get("title", "")}</li>'
176
+ qw_html = f'<div class="quick-wins"><h3>⚡ Quick Wins</h3><ul>{qw_items}</ul></div>'
177
+
178
+ # Category scores table
179
+ cat_rows = ""
180
+ for cat, score in categories.items():
181
+ cat_name = cat.replace("_", " ").title()
182
+ cat_rows += f'''
183
+ <tr>
184
+ <td>{cat_name}</td>
185
+ <td>
186
+ <div class="score-bar-bg">
187
+ <div class="score-bar" style="width:{score}%;background:{score_color(score)}"></div>
188
+ </div>
189
+ </td>
190
+ <td style="color:{score_color(score)};font-weight:700">{score}</td>
191
+ </tr>'''
192
+
193
+ html = f'''<!DOCTYPE html>
194
+ <html lang="en">
195
+ <head>
196
+ <meta charset="UTF-8">
197
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
198
+ <title>SEO Audit Report — {domain}</title>
199
+ <style>
200
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
201
+
202
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
203
+ body {{
204
+ font-family: 'Inter', -apple-system, sans-serif;
205
+ background: #f8fafc;
206
+ color: #1e293b;
207
+ line-height: 1.6;
208
+ }}
209
+ .container {{ max-width: 900px; margin: 0 auto; padding: 2rem; }}
210
+
211
+ /* Header */
212
+ .header {{
213
+ background: linear-gradient(135deg, #0f172a 0%, #1e3a5f 100%);
214
+ color: white;
215
+ padding: 3rem 2rem;
216
+ border-radius: 16px;
217
+ margin-bottom: 2rem;
218
+ text-align: center;
219
+ }}
220
+ .header h1 {{ font-size: 2rem; margin-bottom: 0.5rem; }}
221
+ .header .domain {{ font-size: 1.2rem; opacity: 0.8; }}
222
+ .header .date {{ font-size: 0.85rem; opacity: 0.6; margin-top: 0.5rem; }}
223
+
224
+ /* Score circle */
225
+ .score-hero {{
226
+ display: flex;
227
+ align-items: center;
228
+ justify-content: center;
229
+ gap: 3rem;
230
+ margin: 2rem 0;
231
+ flex-wrap: wrap;
232
+ }}
233
+ .score-circle {{
234
+ width: 150px;
235
+ height: 150px;
236
+ border-radius: 50%;
237
+ display: flex;
238
+ flex-direction: column;
239
+ align-items: center;
240
+ justify-content: center;
241
+ border: 6px solid {score_color(total_score)};
242
+ background: white;
243
+ box-shadow: 0 4px 24px rgba(0,0,0,0.08);
244
+ }}
245
+ .score-number {{ font-size: 3rem; font-weight: 700; color: {score_color(total_score)}; }}
246
+ .score-label {{ font-size: 0.75rem; text-transform: uppercase; color: #64748b; letter-spacing: 1px; }}
247
+
248
+ /* Summary cards */
249
+ .summary-grid {{
250
+ display: grid;
251
+ grid-template-columns: repeat(4, 1fr);
252
+ gap: 1rem;
253
+ margin-bottom: 2rem;
254
+ }}
255
+ .summary-card {{
256
+ background: white;
257
+ border-radius: 12px;
258
+ padding: 1.2rem;
259
+ text-align: center;
260
+ box-shadow: 0 2px 8px rgba(0,0,0,0.04);
261
+ }}
262
+ .summary-card .count {{ font-size: 2rem; font-weight: 700; }}
263
+ .summary-card .label {{ font-size: 0.8rem; color: #64748b; }}
264
+
265
+ /* Sections */
266
+ .section {{ background: white; border-radius: 12px; padding: 2rem; margin-bottom: 1.5rem; box-shadow: 0 2px 8px rgba(0,0,0,0.04); }}
267
+ .section h2 {{ margin-bottom: 1rem; font-size: 1.3rem; }}
268
+ .section h3 {{ margin-bottom: 0.8rem; font-size: 1.1rem; }}
269
+
270
+ /* Score bars */
271
+ table {{ width: 100%; border-collapse: collapse; }}
272
+ td {{ padding: 0.6rem 0; }}
273
+ .score-bar-bg {{ width: 100%; height: 8px; background: #e2e8f0; border-radius: 4px; overflow: hidden; margin: 0 1rem; }}
274
+ .score-bar {{ height: 100%; border-radius: 4px; transition: width 0.5s ease; }}
275
+
276
+ /* Issue cards */
277
+ .issue-card {{
278
+ border: 1px solid #e2e8f0;
279
+ border-radius: 8px;
280
+ padding: 1rem;
281
+ margin-bottom: 0.8rem;
282
+ }}
283
+ .issue-header {{ display: flex; gap: 0.5rem; margin-bottom: 0.3rem; align-items: center; }}
284
+ .severity-badge {{ color: white; padding: 2px 8px; border-radius: 4px; font-size: 0.7rem; font-weight: 600; }}
285
+ .issue-category {{ font-size: 0.8rem; color: #64748b; }}
286
+ .issue-card h4 {{ margin-bottom: 0.3rem; }}
287
+ .issue-card p {{ color: #475569; font-size: 0.9rem; }}
288
+ .fix-block {{ background: #f1f5f9; border-radius: 6px; padding: 0.8rem; margin-top: 0.5rem; }}
289
+ .fix-block pre {{ overflow-x: auto; font-size: 0.8rem; }}
290
+
291
+ /* Quick wins */
292
+ .quick-wins {{ background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; }}
293
+ .quick-wins ul {{ list-style: none; padding: 0; }}
294
+ .quick-wins li {{ padding: 0.3rem 0; }}
295
+
296
+ /* Footer */
297
+ .footer {{ text-align: center; color: #94a3b8; font-size: 0.8rem; padding: 2rem 0; }}
298
+
299
+ /* Print */
300
+ @media print {{
301
+ body {{ background: white; }}
302
+ .container {{ max-width: 100%; padding: 0; }}
303
+ .header {{ break-after: avoid; }}
304
+ .section {{ break-inside: avoid; box-shadow: none; border: 1px solid #e2e8f0; }}
305
+ }}
306
+
307
+ /* Mobile */
308
+ @media (max-width: 640px) {{
309
+ .summary-grid {{ grid-template-columns: repeat(2, 1fr); }}
310
+ .score-hero {{ flex-direction: column; gap: 1.5rem; }}
311
+ }}
312
+ </style>
313
+ </head>
314
+ <body>
315
+ <div class="container">
316
+ <div class="header">
317
+ <h1>SEO Audit Report</h1>
318
+ <div class="domain">{domain}</div>
319
+ <div class="date">{timestamp[:10]}</div>
320
+ </div>
321
+
322
+ <div class="score-hero">
323
+ <div class="score-circle">
324
+ <div class="score-number">{total_score}</div>
325
+ <div class="score-label">SEO Score</div>
326
+ </div>
327
+ <div>
328
+ {radar_svg}
329
+ </div>
330
+ </div>
331
+
332
+ <div class="summary-grid">
333
+ <div class="summary-card">
334
+ <div class="count" style="color:#ef4444">{counts["critical"]}</div>
335
+ <div class="label">Critical</div>
336
+ </div>
337
+ <div class="summary-card">
338
+ <div class="count" style="color:#f97316">{counts["high"]}</div>
339
+ <div class="label">High</div>
340
+ </div>
341
+ <div class="summary-card">
342
+ <div class="count" style="color:#f59e0b">{counts["medium"]}</div>
343
+ <div class="label">Medium</div>
344
+ </div>
345
+ <div class="summary-card">
346
+ <div class="count" style="color:#22c55e">{counts["low"]}</div>
347
+ <div class="label">Low</div>
348
+ </div>
349
+ </div>
350
+
351
+ {qw_html}
352
+
353
+ <div class="section">
354
+ <h2>📊 Category Scores</h2>
355
+ <table>{cat_rows}</table>
356
+ </div>
357
+
358
+ <div class="section">
359
+ <h2>🔍 Issues ({len(issues)})</h2>
360
+ {issue_cards}
361
+ </div>
362
+
363
+ <div class="footer">
364
+ Generated by BMAD+ SEO Engine v2.1 — By Laurent Rochetta
365
+ </div>
366
+ </div>
367
+ </body>
368
+ </html>'''
369
+
370
+ return html
371
+
372
+
373
+ # ── CLI ────────────────────────────────────────────────────────────
374
+
375
+ def main():
376
+ parser = argparse.ArgumentParser(
377
+ description="SEO Report — HTML audit report generator (BMAD+ SEO Engine)"
378
+ )
379
+ parser.add_argument("input", help="Audit JSON file")
380
+ parser.add_argument("--output", "-o", default="seo-report.html", help="Output HTML file")
381
+
382
+ args = parser.parse_args()
383
+
384
+ if not os.path.isfile(args.input):
385
+ print(f"Error: File not found: {args.input}", file=sys.stderr)
386
+ sys.exit(1)
387
+
388
+ with open(args.input, "r", encoding="utf-8") as f:
389
+ audit_data = json.load(f)
390
+
391
+ html = generate_html_report(audit_data)
392
+
393
+ with open(args.output, "w", encoding="utf-8") as f:
394
+ f.write(html)
395
+
396
+ print(f"✅ Report generated: {args.output}", file=sys.stderr)
397
+ print(f" Domain: {audit_data.get('domain', 'Unknown')}")
398
+ print(f" Score: {audit_data.get('score', {}).get('total', 0)}/100")
399
+ print(f" Issues: {len(audit_data.get('issues', []))}")
400
+
401
+
402
+ if __name__ == "__main__":
403
+ main()
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-plus",
4
- "version": "0.3.1",
4
+ "version": "0.3.2",
5
5
  "description": "BMAD+ — Augmented AI-Driven Development Framework with multi-role agents, autopilot, and parallel execution",
6
6
  "keywords": [
7
7
  "bmad",