bmad-plus 0.4.0 → 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.
- package/CHANGELOG.md +18 -0
- package/README.md +12 -56
- package/osint-agent-package/skills/bmad-osint-investigate/osint/SKILL.md +452 -452
- package/osint-agent-package/skills/bmad-osint-investigate/osint/assets/dossier-template.md +116 -116
- package/osint-agent-package/skills/bmad-osint-investigate/osint/references/content-extraction.md +100 -100
- package/osint-agent-package/skills/bmad-osint-investigate/osint/references/platforms.md +130 -130
- package/osint-agent-package/skills/bmad-osint-investigate/osint/references/psychoprofile.md +69 -69
- package/osint-agent-package/skills/bmad-osint-investigate/osint/references/tools.md +281 -281
- package/osint-agent-package/skills/bmad-osint-investigate/osint/scripts/mcp-client.py +136 -136
- package/package.json +1 -1
- package/readme-international/README.de.md +1 -1
- package/readme-international/README.es.md +1 -1
- package/readme-international/README.fr.md +1 -1
- package/tools/cli/commands/install.js +74 -46
- package/tools/cli/i18n.js +501 -0
- package/oveanet-pack/animated-website/DEPLOYMENT.md +0 -104
- package/oveanet-pack/animated-website/README.md +0 -63
- package/oveanet-pack/animated-website/agent/animated-website-agent.md +0 -325
- package/oveanet-pack/animated-website/agent.yaml +0 -63
- package/oveanet-pack/animated-website/templates/animated-website-workflow.md +0 -55
- package/oveanet-pack/seo-audit-360/DEPLOYMENT.md +0 -115
- package/oveanet-pack/seo-audit-360/README.md +0 -66
- package/oveanet-pack/seo-audit-360/SKILL.md +0 -171
- package/oveanet-pack/seo-audit-360/agent/seo-chief.md +0 -294
- package/oveanet-pack/seo-audit-360/agent/seo-judge.md +0 -241
- package/oveanet-pack/seo-audit-360/agent/seo-scout.md +0 -171
- package/oveanet-pack/seo-audit-360/agent.yaml +0 -70
- package/oveanet-pack/seo-audit-360/checklist.md +0 -140
- package/oveanet-pack/seo-audit-360/extensions/google-analytics/EXTENSION.md +0 -79
- package/oveanet-pack/seo-audit-360/extensions/google-analytics/ga4_client.py +0 -200
- package/oveanet-pack/seo-audit-360/extensions/google-analytics/requirements.txt +0 -4
- package/oveanet-pack/seo-audit-360/extensions/google-search-console/EXTENSION.md +0 -109
- package/oveanet-pack/seo-audit-360/extensions/google-search-console/gsc_client.py +0 -186
- package/oveanet-pack/seo-audit-360/extensions/google-search-console/requirements.txt +0 -4
- package/oveanet-pack/seo-audit-360/hooks/seo-check.sh +0 -95
- package/oveanet-pack/seo-audit-360/pagespeed-playbook.md +0 -320
- package/oveanet-pack/seo-audit-360/ref/audit-schema.json +0 -187
- package/oveanet-pack/seo-audit-360/ref/cwv-thresholds.md +0 -87
- package/oveanet-pack/seo-audit-360/ref/eeat-criteria.md +0 -123
- package/oveanet-pack/seo-audit-360/ref/geo-signals.md +0 -167
- package/oveanet-pack/seo-audit-360/ref/hreflang-rules.md +0 -153
- package/oveanet-pack/seo-audit-360/ref/quality-gates.md +0 -133
- package/oveanet-pack/seo-audit-360/ref/schema-catalog.md +0 -91
- package/oveanet-pack/seo-audit-360/ref/schema-templates.json +0 -356
- package/oveanet-pack/seo-audit-360/requirements.txt +0 -14
- 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/install.ps1 +0 -53
- package/oveanet-pack/seo-audit-360/scripts/install.sh +0 -48
- package/oveanet-pack/seo-audit-360/scripts/seo_apis.py +0 -464
- package/oveanet-pack/seo-audit-360/scripts/seo_crawl.py +0 -282
- package/oveanet-pack/seo-audit-360/scripts/seo_fetch.py +0 -231
- package/oveanet-pack/seo-audit-360/scripts/seo_parse.py +0 -255
- package/oveanet-pack/seo-audit-360/scripts/seo_report.py +0 -403
- package/oveanet-pack/seo-audit-360/scripts/seo_screenshot.py +0 -202
- package/oveanet-pack/seo-audit-360/templates/seo-audit-workflow.md +0 -241
- 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 +0 -62
- package/oveanet-pack/seo-audit-360/tests/test_apis.py +0 -75
- package/oveanet-pack/seo-audit-360/tests/test_crawl.py +0 -121
- package/oveanet-pack/seo-audit-360/tests/test_fetch.py +0 -70
- package/oveanet-pack/seo-audit-360/tests/test_parse.py +0 -184
- package/oveanet-pack/universal-backup/DEPLOYMENT.md +0 -80
- package/oveanet-pack/universal-backup/README.md +0 -58
- package/oveanet-pack/universal-backup/agent/backup-agent.md +0 -71
- package/oveanet-pack/universal-backup/agent.yaml +0 -45
- package/oveanet-pack/universal-backup/templates/backup-workflow.md +0 -51
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
# Google Search Console Extension — BMAD+ SEO Engine
|
|
2
|
-
|
|
3
|
-
> Author: Laurent Rochetta | BMAD+ SEO Engine v2.1
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
This extension connects the SEO Engine to Google Search Console API v3 for accessing real organic search performance data. Requires OAuth2 authentication with a Google Cloud project.
|
|
8
|
-
|
|
9
|
-
## Setup Guide
|
|
10
|
-
|
|
11
|
-
### 1. Create Google Cloud Project
|
|
12
|
-
1. Go to [Google Cloud Console](https://console.cloud.google.com)
|
|
13
|
-
2. Create a new project or select existing
|
|
14
|
-
3. Enable **Google Search Console API**
|
|
15
|
-
|
|
16
|
-
### 2. Create OAuth2 Credentials
|
|
17
|
-
1. Go to **APIs & Services > Credentials**
|
|
18
|
-
2. Click **Create Credentials > OAuth 2.0 Client ID**
|
|
19
|
-
3. Application type: **Desktop app**
|
|
20
|
-
4. Download the JSON → save as `credentials.json` in this directory
|
|
21
|
-
|
|
22
|
-
### 3. First Run
|
|
23
|
-
```bash
|
|
24
|
-
python gsc_client.py --setup
|
|
25
|
-
```
|
|
26
|
-
This opens a browser for OAuth consent. The refresh token is saved to `token.json` for subsequent runs.
|
|
27
|
-
|
|
28
|
-
### 4. Verify Access
|
|
29
|
-
```bash
|
|
30
|
-
python gsc_client.py --sites
|
|
31
|
-
```
|
|
32
|
-
Should list all verified Search Console properties.
|
|
33
|
-
|
|
34
|
-
## Commands
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
# List verified sites
|
|
38
|
-
python gsc_client.py --sites
|
|
39
|
-
|
|
40
|
-
# Top queries (default: 28 days)
|
|
41
|
-
python gsc_client.py --queries https://example.com --days 28
|
|
42
|
-
|
|
43
|
-
# Top pages by organic traffic
|
|
44
|
-
python gsc_client.py --pages https://example.com --days 28
|
|
45
|
-
|
|
46
|
-
# Index coverage (errors, valid, excluded)
|
|
47
|
-
python gsc_client.py --coverage https://example.com
|
|
48
|
-
|
|
49
|
-
# Sitemap status
|
|
50
|
-
python gsc_client.py --sitemaps https://example.com
|
|
51
|
-
|
|
52
|
-
# Full export (all data, JSON)
|
|
53
|
-
python gsc_client.py --all https://example.com --json > gsc-data.json
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## Output Examples
|
|
57
|
-
|
|
58
|
-
### Queries
|
|
59
|
-
```
|
|
60
|
-
Top Organic Queries (28 days):
|
|
61
|
-
1. "bmad framework" — Pos: 3.2, Clicks: 450, CTR: 12.3%, Imp: 3,658
|
|
62
|
-
2. "ai development tool" — Pos: 8.1, Clicks: 120, CTR: 3.5%, Imp: 3,428
|
|
63
|
-
3. "multi-agent coding" — Pos: 5.4, Clicks: 95, CTR: 7.8%, Imp: 1,218
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### Coverage
|
|
67
|
-
```
|
|
68
|
-
Index Coverage:
|
|
69
|
-
✅ Valid: 142 pages
|
|
70
|
-
⚠️ Valid with warnings: 8 pages
|
|
71
|
-
❌ Error: 3 pages
|
|
72
|
-
⛔ Excluded: 45 pages (noindex, canonical, etc.)
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## Integration with SEO Engine
|
|
76
|
-
|
|
77
|
-
When this extension is installed, the SKILL.md orchestrator can:
|
|
78
|
-
- Include real organic data in Phase 4 scoring
|
|
79
|
-
- Compare GSC impressions with crawled pages to find content gaps
|
|
80
|
-
- Detect indexed pages with declining CTR → priority optimization targets
|
|
81
|
-
- Cross-reference sitemap URLs with actually indexed pages
|
|
82
|
-
|
|
83
|
-
## Files
|
|
84
|
-
|
|
85
|
-
| File | Purpose |
|
|
86
|
-
|------|---------|
|
|
87
|
-
| `EXTENSION.md` | This documentation |
|
|
88
|
-
| `gsc_client.py` | OAuth2 flow + API calls |
|
|
89
|
-
| `requirements.txt` | Python dependencies |
|
|
90
|
-
|
|
91
|
-
## Dependencies SUPPLÉMENTAIRES
|
|
92
|
-
|
|
93
|
-
```
|
|
94
|
-
google-auth>=2.0.0
|
|
95
|
-
google-auth-oauthlib>=1.0.0
|
|
96
|
-
google-api-python-client>=2.0.0
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
## Security Notes
|
|
100
|
-
|
|
101
|
-
- `credentials.json` — Contains client ID/secret. **Do not commit to Git.**
|
|
102
|
-
- `token.json` — Contains refresh token. **Do not commit to Git.**
|
|
103
|
-
- Add both to `.gitignore`
|
|
104
|
-
|
|
105
|
-
## Rate Limits
|
|
106
|
-
|
|
107
|
-
- 1,200 queries per minute per project
|
|
108
|
-
- 25,000 rows per request maximum
|
|
109
|
-
- Data typically 2-3 days delayed
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Google Search Console Client — OAuth2 API client for organic search data.
|
|
4
|
-
|
|
5
|
-
Features:
|
|
6
|
-
- OAuth2 flow with credential persistence
|
|
7
|
-
- Query performance data (queries, pages, devices, countries)
|
|
8
|
-
- Index coverage status
|
|
9
|
-
- Sitemap submissions
|
|
10
|
-
|
|
11
|
-
Author: Laurent Rochetta
|
|
12
|
-
License: MIT
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
import argparse
|
|
16
|
-
import json
|
|
17
|
-
import os
|
|
18
|
-
import sys
|
|
19
|
-
from datetime import datetime, timedelta
|
|
20
|
-
|
|
21
|
-
SCOPES = ["https://www.googleapis.com/auth/webmasters.readonly"]
|
|
22
|
-
CREDENTIALS_FILE = os.path.join(os.path.dirname(__file__), "credentials.json")
|
|
23
|
-
TOKEN_FILE = os.path.join(os.path.dirname(__file__), "token.json")
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def get_service():
|
|
27
|
-
"""Authenticate and return a Search Console service object."""
|
|
28
|
-
try:
|
|
29
|
-
from google.oauth2.credentials import Credentials
|
|
30
|
-
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
31
|
-
from googleapiclient.discovery import build
|
|
32
|
-
from google.auth.transport.requests import Request
|
|
33
|
-
except ImportError:
|
|
34
|
-
print(
|
|
35
|
-
"Error: Missing dependencies. Install:\n"
|
|
36
|
-
" pip install google-auth google-auth-oauthlib google-api-python-client",
|
|
37
|
-
file=sys.stderr,
|
|
38
|
-
)
|
|
39
|
-
sys.exit(1)
|
|
40
|
-
|
|
41
|
-
creds = None
|
|
42
|
-
if os.path.exists(TOKEN_FILE):
|
|
43
|
-
creds = Credentials.from_authorized_user_file(TOKEN_FILE, SCOPES)
|
|
44
|
-
|
|
45
|
-
if not creds or not creds.valid:
|
|
46
|
-
if creds and creds.expired and creds.refresh_token:
|
|
47
|
-
creds.refresh(Request())
|
|
48
|
-
else:
|
|
49
|
-
if not os.path.exists(CREDENTIALS_FILE):
|
|
50
|
-
print(
|
|
51
|
-
f"Error: {CREDENTIALS_FILE} not found.\n"
|
|
52
|
-
"Download OAuth2 credentials from Google Cloud Console.\n"
|
|
53
|
-
"See EXTENSION.md for setup guide.",
|
|
54
|
-
file=sys.stderr,
|
|
55
|
-
)
|
|
56
|
-
sys.exit(1)
|
|
57
|
-
flow = InstalledAppFlow.from_client_secrets_file(CREDENTIALS_FILE, SCOPES)
|
|
58
|
-
creds = flow.run_local_server(port=0)
|
|
59
|
-
|
|
60
|
-
with open(TOKEN_FILE, "w") as f:
|
|
61
|
-
f.write(creds.to_json())
|
|
62
|
-
|
|
63
|
-
return build("searchconsole", "v1", credentials=creds)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def list_sites(service):
|
|
67
|
-
"""List all verified Search Console properties."""
|
|
68
|
-
result = service.sites().list().execute()
|
|
69
|
-
sites = result.get("siteEntry", [])
|
|
70
|
-
return [{"url": s["siteUrl"], "level": s["permissionLevel"]} for s in sites]
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def query_performance(service, site_url: str, days: int = 28, dimensions: list = None, row_limit: int = 25):
|
|
74
|
-
"""Query Search Analytics for organic performance data."""
|
|
75
|
-
if dimensions is None:
|
|
76
|
-
dimensions = ["query"]
|
|
77
|
-
|
|
78
|
-
end_date = datetime.now().date()
|
|
79
|
-
start_date = end_date - timedelta(days=days)
|
|
80
|
-
|
|
81
|
-
body = {
|
|
82
|
-
"startDate": start_date.isoformat(),
|
|
83
|
-
"endDate": end_date.isoformat(),
|
|
84
|
-
"dimensions": dimensions,
|
|
85
|
-
"rowLimit": row_limit,
|
|
86
|
-
"dataState": "all",
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
result = service.searchanalytics().query(siteUrl=site_url, body=body).execute()
|
|
90
|
-
rows = result.get("rows", [])
|
|
91
|
-
|
|
92
|
-
return [{
|
|
93
|
-
"keys": row["keys"],
|
|
94
|
-
"clicks": row["clicks"],
|
|
95
|
-
"impressions": row["impressions"],
|
|
96
|
-
"ctr": round(row["ctr"] * 100, 1),
|
|
97
|
-
"position": round(row["position"], 1),
|
|
98
|
-
} for row in rows]
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def get_sitemaps(service, site_url: str):
|
|
102
|
-
"""Get sitemap submission status."""
|
|
103
|
-
result = service.sitemaps().list(siteUrl=site_url).execute()
|
|
104
|
-
sitemaps = result.get("sitemap", [])
|
|
105
|
-
return [{
|
|
106
|
-
"path": s["path"],
|
|
107
|
-
"type": s.get("type", ""),
|
|
108
|
-
"submitted": s.get("lastSubmitted", ""),
|
|
109
|
-
"warnings": s.get("warnings", 0),
|
|
110
|
-
"errors": s.get("errors", 0),
|
|
111
|
-
} for s in sitemaps]
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
# ── CLI ────────────────────────────────────────────────────────────
|
|
115
|
-
|
|
116
|
-
def main():
|
|
117
|
-
parser = argparse.ArgumentParser(
|
|
118
|
-
description="Google Search Console Client (BMAD+ SEO Engine)"
|
|
119
|
-
)
|
|
120
|
-
parser.add_argument("site", nargs="?", help="Site URL (e.g. https://example.com)")
|
|
121
|
-
parser.add_argument("--setup", action="store_true", help="Run OAuth2 setup flow")
|
|
122
|
-
parser.add_argument("--sites", action="store_true", help="List verified sites")
|
|
123
|
-
parser.add_argument("--queries", action="store_true", help="Top queries")
|
|
124
|
-
parser.add_argument("--pages", action="store_true", help="Top pages")
|
|
125
|
-
parser.add_argument("--sitemaps", action="store_true", help="Sitemap status")
|
|
126
|
-
parser.add_argument("--all", action="store_true", help="All data")
|
|
127
|
-
parser.add_argument("--days", type=int, default=28, help="Days lookback (default: 28)")
|
|
128
|
-
parser.add_argument("--limit", type=int, default=25, help="Max rows (default: 25)")
|
|
129
|
-
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
|
|
130
|
-
|
|
131
|
-
args = parser.parse_args()
|
|
132
|
-
|
|
133
|
-
service = get_service()
|
|
134
|
-
|
|
135
|
-
if args.setup:
|
|
136
|
-
print("✅ OAuth2 setup complete. Token saved.")
|
|
137
|
-
return
|
|
138
|
-
|
|
139
|
-
if args.sites:
|
|
140
|
-
sites = list_sites(service)
|
|
141
|
-
if args.json:
|
|
142
|
-
print(json.dumps(sites, indent=2))
|
|
143
|
-
else:
|
|
144
|
-
print("\nVerified Sites:")
|
|
145
|
-
for s in sites:
|
|
146
|
-
print(f" {s['url']} ({s['level']})")
|
|
147
|
-
return
|
|
148
|
-
|
|
149
|
-
if not args.site:
|
|
150
|
-
parser.print_help()
|
|
151
|
-
return
|
|
152
|
-
|
|
153
|
-
if args.queries or args.all:
|
|
154
|
-
data = query_performance(service, args.site, args.days, ["query"], args.limit)
|
|
155
|
-
if args.json:
|
|
156
|
-
print(json.dumps({"queries": data}, indent=2))
|
|
157
|
-
else:
|
|
158
|
-
print(f"\nTop Queries ({args.days} days):")
|
|
159
|
-
for i, row in enumerate(data, 1):
|
|
160
|
-
q = row["keys"][0]
|
|
161
|
-
print(f" {i:2}. \"{q[:50]}\" — Pos: {row['position']}, "
|
|
162
|
-
f"Clicks: {row['clicks']:,}, CTR: {row['ctr']}%, Imp: {row['impressions']:,}")
|
|
163
|
-
|
|
164
|
-
if args.pages or args.all:
|
|
165
|
-
data = query_performance(service, args.site, args.days, ["page"], args.limit)
|
|
166
|
-
if args.json:
|
|
167
|
-
print(json.dumps({"pages": data}, indent=2))
|
|
168
|
-
else:
|
|
169
|
-
print(f"\nTop Pages ({args.days} days):")
|
|
170
|
-
for i, row in enumerate(data, 1):
|
|
171
|
-
page = row["keys"][0]
|
|
172
|
-
print(f" {i:2}. {page[:60]} — Clicks: {row['clicks']:,}, CTR: {row['ctr']}%")
|
|
173
|
-
|
|
174
|
-
if args.sitemaps or args.all:
|
|
175
|
-
data = get_sitemaps(service, args.site)
|
|
176
|
-
if args.json:
|
|
177
|
-
print(json.dumps({"sitemaps": data}, indent=2))
|
|
178
|
-
else:
|
|
179
|
-
print(f"\nSitemaps:")
|
|
180
|
-
for s in data:
|
|
181
|
-
status = "✅" if s["errors"] == 0 else "❌"
|
|
182
|
-
print(f" {status} {s['path']} (type: {s['type']}, errors: {s['errors']})")
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if __name__ == "__main__":
|
|
186
|
-
main()
|
|
@@ -1,95 +0,0 @@
|
|
|
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
|