Skip to content

Commit cc0738f

Browse files
AgriciDanielclaude
andcommitted
v1.4.0: Supply chain security, GEO agent, URL normalization, SPA Phase 2
Security: - Replace irm|iex Windows install with git clone + powershell -File as primary method. Claude Code's own guardrails flagged the old pattern as supply chain risk (community-reported). Added collapsible review-before-running option. - Version pin install.sh and install.ps1 to REPO_TAG (v1.3.0 default) instead of cloning main branch silently - Harden Invoke-External in install.ps1 with PSNativeCommandUseErrorActionPreference handling (incorporates fixes from PR #13 and PR #15) GEO: - Deploy agents/seo-geo.md — /seo audit now spawns 7 parallel agents (was 6) - Add seo-geo to orchestrator spawn list and Subagents section in seo/SKILL.md - Increase AI Search Readiness weight from 5% to 10% (Technical 25→22%, Content 25→23%) - Nuanced FAQPage guidance in SKILL.md, seo-schema.md, schema-types.md — existing FAQPage on commercial sites is Info not Critical (GEO/LLM citation benefit) Fixes (from community PRs): - URL normalization in capture_screenshot.py and analyze_visual.py via shared normalize_url() helper — bare domains now work (from PR #16 by @shuofengzhang) - Googlebot UA flag in fetch_page.py for prerender/SPA detection (Issue #11 Phase 2) - Python requirement corrected to 3.10+ in README and docs/INSTALLATION.md - seo-geo added to uninstall.sh and uninstall.ps1 agent removal lists Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 25fda1d commit cc0738f

16 files changed

Lines changed: 284 additions & 35 deletions

.gitignore

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,24 @@ screenshots/
4747
# Temporary analysis files
4848
*-AUDIT-REPORT.md
4949
*-ACTION-PLAN.md
50+
FULL-AUDIT-REPORT.md
51+
ACTION-PLAN.md
52+
GEO-ANALYSIS.md
53+
SCHEMA-REPORT.md
54+
SITEMAP-REPORT*.md
55+
VALIDATION-REPORT.md
56+
57+
# Generated artifacts from audits
58+
charts/
59+
*.pdf
60+
report.html
61+
generated-schema.json
62+
rankenstein-schema-audit.md
63+
capture_closeups.py
64+
capture_desktop.py
65+
generate_charts.py
66+
generate_pdf.py
67+
68+
# Debug logs
69+
firebase-debug.log
70+
firebase-debug.*.log

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.4.0] - 2026-03-12
9+
10+
### Security
11+
- **Install script supply chain fix**: Replaced `irm | iex` Windows PowerShell one-liner with `git clone + powershell -File` as primary install method. Claude Code's own security guardrails flagged the old pattern as a supply chain risk (reported by community member). Added collapsible "review before running" section for Unix curl method.
12+
- **Version pinning**: `install.sh` and `install.ps1` now clone a specific release tag (`v1.3.0`) by default rather than `main`, preventing silent updates. Override with `CLAUDE_SEO_TAG=main`.
13+
- **PowerShell Invoke-External hardening**: Comprehensive `PSNativeCommandUseErrorActionPreference` handling in `Invoke-External` wrapper (fixes Windows git clone stderr false-positive termination, from PR #13 + PR #15).
14+
15+
### Added
16+
- **GEO agent deployed**: `agents/seo-geo.md` created — `/seo audit` now spawns 7 parallel agents (was 6). GEO analysis covers AI crawler access, llms.txt, passage-level citability, brand mention signals, platform-specific scoring (Google AI Overviews, ChatGPT, Perplexity, Bing Copilot).
17+
- **`--googlebot` flag in `fetch_page.py`**: Detect prerender/dynamic rendering services by comparing response size with default UA vs Googlebot UA. First phase of SPA/CSR support (Issue #11).
18+
19+
### Fixed
20+
- **URL normalization**: `capture_screenshot.py` and `analyze_visual.py` now accept bare domains (`example.com``https://example.com`) via shared `normalize_url()` helper (from PR #16 by @shuofengzhang).
21+
- **GEO weight**: AI Search Readiness weight increased from 5% to 10% in overall SEO Health Score. Technical SEO adjusted to 22%, Content Quality to 23%.
22+
- **FAQPage guidance**: Blanket "remove FAQPage on commercial sites" updated to nuanced guidance — existing FAQPage → Info priority (not Critical), noting AI/LLM citation benefit. Adding new FAQPage → not recommended for Google, note AI benefit. Updated in `seo/SKILL.md`, `agents/seo-schema.md`, `seo/references/schema-types.md`.
23+
- **Uninstall agents list**: Added `seo-geo` to `uninstall.sh` and `uninstall.ps1` removal lists.
24+
- **Python requirement**: Corrected from `3.8+` to `3.10+` in `README.md` and `docs/INSTALLATION.md`.
25+
26+
### Changed
27+
- Subagent count: 6 → 7 (added seo-geo to core audit pipeline)
28+
- `.gitignore`: Added generated audit artifacts (charts/, PDFs, report.html, firebase-debug.log, generated-schema.json)
29+
30+
---
31+
832
## [1.3.0] - 2026-03-06
933

1034
### Added

README.md

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,15 @@ Comprehensive SEO analysis skill for Claude Code. Covers technical SEO, on-page
1313

1414
## Installation
1515

16-
### One-Command Install (Unix/macOS/Linux)
16+
### Recommended Install (Unix/macOS/Linux)
17+
18+
```bash
19+
git clone --depth 1 https://github.com/AgriciDaniel/claude-seo.git
20+
bash claude-seo/install.sh
21+
```
22+
23+
<details>
24+
<summary>One-liner (curl)</summary>
1725

1826
```bash
1927
curl -fsSL https://raw.githubusercontent.com/AgriciDaniel/claude-seo/main/install.sh | bash
@@ -25,20 +33,26 @@ Or via [install.cat](https://install.cat):
2533
curl -fsSL install.cat/AgriciDaniel/claude-seo | bash
2634
```
2735

28-
### Manual Install
36+
Prefer to review the script before running?
2937

3038
```bash
31-
git clone https://github.com/AgriciDaniel/claude-seo.git
32-
cd claude-seo
33-
./install.sh
39+
curl -fsSL https://raw.githubusercontent.com/AgriciDaniel/claude-seo/main/install.sh > install.sh
40+
cat install.sh # review
41+
bash install.sh # run when satisfied
42+
rm install.sh
3443
```
3544

36-
### Windows
45+
</details>
46+
47+
### Windows (PowerShell)
3748

3849
```powershell
39-
irm https://raw.githubusercontent.com/AgriciDaniel/claude-seo/main/install.ps1 | iex
50+
git clone --depth 1 https://github.com/AgriciDaniel/claude-seo.git
51+
powershell -ExecutionPolicy Bypass -File claude-seo\install.ps1
4052
```
4153

54+
> **Why git clone instead of `irm | iex`?** Claude Code's own security guardrails flag `irm ... | iex` as a supply chain risk (downloading and executing remote code with no verification). The git clone approach lets you inspect the script at `claude-seo\install.ps1` before running it.
55+
4256
## Quick Start
4357

4458
```bash
@@ -186,16 +200,26 @@ See `schema/templates.json` for ready-to-use JSON-LD snippets.
186200

187201
## Requirements
188202

189-
- Python 3.8+
203+
- Python 3.10+
190204
- Claude Code CLI
191205
- Optional: Playwright for screenshots
192206

193207
## Uninstall
194208

209+
```bash
210+
git clone --depth 1 https://github.com/AgriciDaniel/claude-seo.git
211+
bash claude-seo/uninstall.sh
212+
```
213+
214+
<details>
215+
<summary>One-liner (curl)</summary>
216+
195217
```bash
196218
curl -fsSL https://raw.githubusercontent.com/AgriciDaniel/claude-seo/main/uninstall.sh | bash
197219
```
198220

221+
</details>
222+
199223
### MCP Integrations
200224

201225
Integrates with MCP servers for live SEO data — including official servers from **Ahrefs** (`@ahrefs/mcp`) and **Semrush**, plus community servers for Google Search Console, PageSpeed Insights, and DataForSEO. See [MCP Integration Guide](docs/MCP-INTEGRATION.md) for setup.

TODO.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,25 @@
4949
- [ ] **Brand mention analysis Python implementation** (Priority: Low)
5050
Currently documented in `seo-geo/SKILL.md` but no programmatic scoring.
5151

52+
## SPA / Client-Side Rendering Support (Issue #11)
53+
54+
Phase 2 (merged in v1.4.0):
55+
- [x] **`--googlebot` flag in fetch_page.py** — Compare response size with default UA vs Googlebot UA to detect prerender services
56+
57+
Remaining phases (deferred):
58+
59+
- [ ] **Phase 1: render_page.py** (Priority: High) — Playwright-based page renderer that exports fully-rendered DOM HTML. CLI: `python render_page.py https://example.com --output rendered.html`. Drop-in complement to `fetch_page.py`.
60+
61+
- [ ] **Phase 3: Screenshot DOM export** (Priority: Medium) — Add `--export-html` flag to `capture_screenshot.py` so rendered DOM is available to other agents without a second browser launch.
62+
63+
- [ ] **Phase 4: Orchestrator SPA detection** (Priority: High) — Add 6 SPA detection signals to `seo/SKILL.md` (empty root div, minimal body, prerenderReady, framework markers, large JS bundles, React/Vue/Angular attributes). Route agents to raw vs rendered HTML based on detection result.
64+
65+
- [ ] **Phase 5: Agent updates** (Priority: High) — Update seo-technical, seo-content, seo-schema, seo-performance, seo-visual to use rendered DOM when SPA detected. Add WRS (Web Rendering Service) dependency risk deductions.
66+
67+
- [ ] **Phase 6: SPA scoring methodology** (Priority: Medium) — Separate scoring paths for SPAs with/without prerender. New "Rendering Architecture Assessment" report section.
68+
69+
- [ ] **Phase 7: Reference file updates** (Priority: Low) — Add SPA section to quality-gates.md, eeat-framework.md, cwv-thresholds.md (Soft Navigations API guidance).
70+
5271
---
5372

54-
*Last updated: February 19, 2026*
73+
*Last updated: March 12, 2026*

agents/seo-geo.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
name: seo-geo
3+
description: GEO and AI search specialist. Analyzes AI crawler accessibility, llms.txt compliance, passage-level citability, brand mention signals, and platform-specific optimization for Google AI Overviews, ChatGPT, Perplexity, and Bing Copilot.
4+
tools: Read, Bash, WebFetch, Glob, Grep
5+
---
6+
7+
You are a Generative Engine Optimization (GEO) specialist. When given a URL:
8+
9+
1. Fetch the page and check robots.txt for AI crawler rules
10+
2. Check for `/llms.txt` and RSL 1.0 licensing
11+
3. Analyze content citability (passage length, structure, directness)
12+
4. Evaluate authority signals (authorship, dates, citations, entity presence)
13+
5. Assess technical accessibility for AI crawlers (SSR vs CSR)
14+
6. Score across 5 dimensions and generate prioritized recommendations
15+
16+
## GEO Health Score (0-100)
17+
18+
| Dimension | Weight |
19+
|-----------|--------|
20+
| Citability | 25% |
21+
| Structural Readability | 20% |
22+
| Multi-Modal Content | 15% |
23+
| Authority & Brand Signals | 20% |
24+
| Technical Accessibility | 20% |
25+
26+
## AI Crawlers to Check in robots.txt
27+
28+
Allow for AI search visibility: GPTBot, OAI-SearchBot, ClaudeBot, PerplexityBot
29+
Optional block (training only): CCBot, anthropic-ai, cohere-ai
30+
31+
## Key Citability Signals
32+
33+
- Optimal passage length: **134-167 words** for AI citation
34+
- Direct answers in first 40-60 words of each section
35+
- Question-based H2/H3 headings
36+
- Specific statistics with source attribution
37+
- Self-contained answer blocks (extractable without context)
38+
39+
## Brand Mention Correlation with AI Citations
40+
41+
| Signal | Correlation |
42+
|--------|-------------|
43+
| YouTube mentions | ~0.737 (strongest) |
44+
| Reddit presence | High |
45+
| Wikipedia entity | High |
46+
| Domain Rating (backlinks) | ~0.266 (weak) |
47+
48+
Only 11% of domains are cited by both ChatGPT and Google AI Overviews — platform optimization matters.
49+
50+
## DataForSEO Integration (Optional)
51+
52+
If DataForSEO MCP tools are available, use `ai_optimization_chat_gpt_scraper` for live ChatGPT visibility and `ai_opt_llm_ment_search` for LLM mention tracking.
53+
54+
## Output Format
55+
56+
Provide a structured report with:
57+
- GEO Readiness Score (0-100) with dimension breakdown
58+
- AI Crawler Access Status (allowed/blocked per crawler)
59+
- llms.txt status (present/missing/malformed)
60+
- Brand mention analysis (Wikipedia, Reddit, YouTube, LinkedIn)
61+
- Top 5 highest-impact changes with effort estimates
62+
- Platform-specific scores (Google AIO, ChatGPT, Perplexity, Bing Copilot)

agents/seo-schema.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ When analyzing pages:
2222
- **CourseInfo, EstimatedSalary, LearningVideo**: Retired June 2025
2323

2424
### Restricted Schema:
25-
- **FAQ**: ONLY for government and healthcare authority sites (restricted August 2023)
25+
- **FAQ**: Google rich results restricted to government and healthcare sites (August 2023).
26+
- **Existing FAQPage on commercial sites**: Flag as Info priority (not Critical) — FAQPage still benefits AI/LLM citations even without Google rich results.
27+
- **Adding new FAQPage on commercial sites**: Not recommended for Google benefit; note AI discoverability upside if user prioritizes GEO.
2628

2729
### Always Prefer:
2830
- JSON-LD format over Microdata or RDFa

docs/INSTALLATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Prerequisites
44

5-
- **Python 3.8+** with pip
5+
- **Python 3.10+** with pip
66
- **Git** for cloning the repository
77
- **Claude Code CLI** installed and configured
88

install.ps1

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,26 @@ function Invoke-External {
3030
[switch]$Quiet
3131
)
3232

33-
$output = & $Exe @Args 2>&1
34-
$exitCode = $LASTEXITCODE
33+
$previousErrorActionPreference = $ErrorActionPreference
34+
$hasNativePreference = $null -ne (Get-Variable -Name PSNativeCommandUseErrorActionPreference -ErrorAction SilentlyContinue)
35+
if ($hasNativePreference) {
36+
$previousNativePreference = $PSNativeCommandUseErrorActionPreference
37+
}
38+
39+
try {
40+
$ErrorActionPreference = 'Continue'
41+
if ($hasNativePreference) {
42+
$PSNativeCommandUseErrorActionPreference = $false
43+
}
44+
45+
$output = & $Exe @Args 2>&1 | ForEach-Object { $_.ToString() }
46+
$exitCode = $LASTEXITCODE
47+
} finally {
48+
$ErrorActionPreference = $previousErrorActionPreference
49+
if ($hasNativePreference) {
50+
$PSNativeCommandUseErrorActionPreference = $previousNativePreference
51+
}
52+
}
3553

3654
if (-not $Quiet -and $null -ne $output -and $output.Count -gt 0) {
3755
$output | ForEach-Object { Write-Host $_ }
@@ -67,6 +85,9 @@ try {
6785
$SkillDir = "$env:USERPROFILE\.claude\skills\seo"
6886
$AgentDir = "$env:USERPROFILE\.claude\agents"
6987
$RepoUrl = "https://github.com/AgriciDaniel/claude-seo"
88+
# Pin to a specific release tag to prevent silent updates from main.
89+
# Override: $env:CLAUDE_SEO_TAG = 'main'; .\install.ps1
90+
$RepoTag = if ($env:CLAUDE_SEO_TAG) { $env:CLAUDE_SEO_TAG } else { 'v1.3.0' }
7091

7192
# Create directories
7293
New-Item -ItemType Directory -Force -Path $SkillDir | Out-Null
@@ -81,8 +102,8 @@ if (Test-Path $TempDir) {
81102
$keepTemp = ($env:CLAUDE_SEO_KEEP_TEMP -eq '1')
82103

83104
try {
84-
Write-Host "↓ Downloading Claude SEO..." -ForegroundColor Yellow
85-
$clone = Invoke-External -Exe 'git' -Args @('clone','--depth','1',$RepoUrl,$TempDir) -Quiet
105+
Write-Host "↓ Downloading Claude SEO ($RepoTag)..." -ForegroundColor Yellow
106+
$clone = Invoke-External -Exe 'git' -Args @('clone','--depth','1','--branch',$RepoTag,$RepoUrl,$TempDir) -Quiet
86107
if ($clone.ExitCode -ne 0) {
87108
throw "git clone failed. Output:`n$($clone.Output -join "`n")"
88109
}

install.sh

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ main() {
88
SKILL_DIR="${HOME}/.claude/skills/seo"
99
AGENT_DIR="${HOME}/.claude/agents"
1010
REPO_URL="https://github.com/AgriciDaniel/claude-seo"
11+
# Pin to a specific release tag to prevent silent updates from main.
12+
# Override: CLAUDE_SEO_TAG=main bash install.sh
13+
REPO_TAG="${CLAUDE_SEO_TAG:-v1.3.0}"
1114

1215
echo "════════════════════════════════════════"
1316
echo "║ Claude SEO - Installer ║"
@@ -36,8 +39,8 @@ main() {
3639
TEMP_DIR=$(mktemp -d)
3740
trap "rm -rf ${TEMP_DIR}" EXIT
3841

39-
echo "↓ Downloading Claude SEO..."
40-
git clone --depth 1 "${REPO_URL}" "${TEMP_DIR}/claude-seo" 2>/dev/null
42+
echo "↓ Downloading Claude SEO (${REPO_TAG})..."
43+
git clone --depth 1 --branch "${REPO_TAG}" "${REPO_URL}" "${TEMP_DIR}/claude-seo" 2>/dev/null
4144

4245
# Copy skill files
4346
echo "→ Installing skill files..."

scripts/analyze_visual.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import json
1212
import socket
1313
import sys
14-
from urllib.parse import urlparse
14+
from urllib.parse import ParseResult, urlparse
1515

1616
try:
1717
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout
@@ -20,6 +20,21 @@
2020
sys.exit(1)
2121

2222

23+
def normalize_url(url: str) -> tuple[str, ParseResult]:
24+
"""Normalize URL and return (url, parsed_url)."""
25+
parsed = urlparse(url)
26+
if not parsed.scheme:
27+
url = f"https://{url}"
28+
parsed = urlparse(url)
29+
30+
if parsed.scheme not in ("http", "https"):
31+
raise ValueError(f"Invalid URL scheme: {parsed.scheme}")
32+
if not parsed.hostname:
33+
raise ValueError("Invalid URL: missing hostname")
34+
35+
return url, parsed
36+
37+
2338
def analyze_visual(url: str, timeout: int = 30000) -> dict:
2439
"""
2540
Analyze visual aspects of a web page.
@@ -54,15 +69,21 @@ def analyze_visual(url: str, timeout: int = 30000) -> dict:
5469
"error": None,
5570
}
5671

72+
try:
73+
url, parsed = normalize_url(url)
74+
result["url"] = url
75+
except ValueError as e:
76+
result["error"] = str(e)
77+
return result
78+
5779
# SSRF prevention: block private/internal IPs
5880
try:
59-
parsed = urlparse(url)
6081
resolved_ip = socket.gethostbyname(parsed.hostname)
6182
ip = ipaddress.ip_address(resolved_ip)
6283
if ip.is_private or ip.is_loopback or ip.is_reserved:
6384
result["error"] = f"Blocked: URL resolves to private/internal IP ({resolved_ip})"
6485
return result
65-
except (socket.gaierror, ValueError):
86+
except socket.gaierror:
6687
pass
6788

6889
try:

0 commit comments

Comments
 (0)