---
name: "SEO drift"
repo: https://github.com/agricidaniel/claude-seo
source: https://github.com/agricidaniel/claude-seo/blob/HEAD/skills/seo-drift/SKILL.md
license: "MIT"
install: "npx skills add agricidaniel/claude-seo --skill seo-drift"
page: https://100xlab.com/resources/agent-skills/agricidaniel-claude-seo--seo-drift--u2z9n1
---

# SEO Drift Monitor (April 2026)

Git for your SEO. Capture baselines, detect regressions, track changes over time.

---

## Commands

| Command | Purpose |
|---------|---------|
| `/seo drift baseline <url>` | Capture current SEO state as a "known good" snapshot |
| `/seo drift compare <url>` | Compare current page state to stored baseline |
| `/seo drift history <url>` | Show change history and past comparisons |

---

## What It Captures

Every baseline records these SEO-critical elements:

| Element | Field | Source |
|---------|-------|--------|
| Title tag | `title` | `parse_html.py` |
| Meta description | `meta_description` | `parse_html.py` |
| Canonical URL | `canonical` | `parse_html.py` |
| Robots directives | `meta_robots` | `parse_html.py` |
| H1 headings | `h1` (array) | `parse_html.py` |
| H2 headings | `h2` (array) | `parse_html.py` |
| H3 headings | `h3` (array) | `parse_html.py` |
| JSON-LD schema | `schema` (array) | `parse_html.py` |
| Open Graph tags | `open_graph` (dict) | `parse_html.py` |
| Core Web Vitals | `cwv` (dict) | `pagespeed_check.py` |
| HTTP status code | `status_code` | `fetch_page.py` |
| HTML content hash | `html_hash` (SHA-256) | Computed |
| Schema content hash | `schema_hash` (SHA-256) | Computed |

---

## How Comparison Works

The comparison engine applies **17 rules across 3 severity levels**. Load
`references/comparison-rules.md` for the full rule set with thresholds,
recommended actions, and cross-skill references.

### Severity Levels

| Level | Meaning | Response Time |
|-------|---------|---------------|
| **CRITICAL** | SEO-breaking change, likely traffic loss | Immediate |
| **WARNING** | Potential impact, needs investigation | Within 1 week |
| **INFO** | Awareness only, may be intentional | Review at convenience |

---

## Storage

All data is stored locally in SQLite:

```
~/.cache/claude-seo/drift/baselines.db
```

### Tables

- **baselines**: Captured snapshots with all SEO elements
- **comparisons**: Diff results with triggered rules and severities

URL normalization ensures consistent matching: lowercase scheme/host, strip
default ports (80/443), sort query parameters, remove UTM parameters, strip
trailing slashes.

---

## Command: `baseline`

Captures the current state of a page and stores it.

**Steps:**
1. Validate URL (SSRF protection via `google_auth.validate_url()`)
2. Fetch page via `scripts/fetch_page.py`
3. Parse HTML via `scripts/parse_html.py`
4. Optionally fetch CWV via `scripts/pagespeed_check.py` (use `--skip-cwv` to skip)
5. Hash HTML body and schema content (SHA-256)
6. Store snapshot in SQLite

**Execution:**
```bash
python3 scripts/drift_baseline.py <url>
python3 scripts/drift_baseline.py <url> --skip-cwv
```

**Output:** JSON with baseline ID, timestamp, URL, and summary of captured elements.

---

## Command: `compare`

Fetches the current page state and diffs it against the most recent baseline.

**Steps:**
1. Validate URL
2. Load most recent baseline from SQLite (or specific `--baseline-id`)
3. Fetch and parse current page state
4. Run all 17 comparison rules
5. Classify findings by severity
6. Store comparison result
7. Output JSON diff report

**Execution:**
```bash
python3 scripts/drift_compare.py <url>
python3 scripts/drift_compare.py <url> --baseline-id 5
python3 scripts/drift_compare.py <url> --skip-cwv
```

**Output:** JSON with all triggered rules, old/new values, severity, and actions.

After comparison, offer to generate an HTML report:
```bash
python3 scripts/drift_report.py <comparison_json_file> --output drift-report.html
```

---

## Command: `history`

Shows all baselines and comparisons for a URL.

**Execution:**
```bash
python3 scripts/drift_history.py <url>
python3 scripts/drift_history.py <url> --limit 10
```

**Output:** JSON array of baselines (newest first) with timestamps and comparison summaries.

---

## Cross-Skill Integration

When drift is detected, recommend the appropriate specialized skill:

| Finding | Recommendation |
|---------|----------------|
| Schema removed or modified | Run `/seo schema <url>` for full validation |
| CWV regression | Run `/seo technical <url>` for performance audit |
| Title or meta description changed | Run `/seo page <url>` for content analysis |
| Canonical changed or removed | Run `/seo technical <url>` for indexability check |
| Noindex added | Run `/seo technical <url>` for crawlability audit |
| H1/heading structure changed | Run `/seo content <url>` for E-E-A-T review |
| OG tags removed | Run `/seo page <url>` for social sharing analysis |
| Status code changed to error | Run `/seo technical <url>` for full diagnostics |

---

## Error Handling

| Scenario | Action |
|----------|--------|
| URL unreachable | Report error from `fetch_page.py`. Do not guess state. Suggest user verify URL. |
| No baseline exists for URL | Inform user and suggest running `baseline` first. |
| SSRF blocked (private IP) | Report `validate_url()` rejection. Never bypass. |
| SQLite database missing | Auto-create on first use. No error. |
| CWV fetch fails (no API key) | Store `null` for CWV fields. Skip CWV rules during comparison. |
| Page returns 4xx/5xx | Still capture as baseline (status code IS a tracked field). |
| Multiple baselines exist | Use most recent unless `--baseline-id` specified. |

---

## Security

- **All URL fetching** goes through `scripts/fetch_page.py` which enforces SSRF protection
  (blocks private IPs, loopback, reserved ranges, GCP metadata endpoints)
- **No curl, no subprocess HTTP calls** -- only the project's validated fetch pipeline
- **All SQLite queries** use parameterized placeholders (`?`), never string interpolation
- **TLS always verified** -- no `verify=False` anywhere in the pipeline

---

## Typical Workflows

### Pre/Post Deployment Check
```
/seo drift baseline https://example.com     # Before deploy
# ... deploy happens ...
/seo drift compare https://example.com      # After deploy
```

### Ongoing Monitoring
```
/seo drift baseline https://example.com     # Initial capture
# ... weeks later ...
/seo drift compare https://example.com      # Check for drift
/seo drift history https://example.com      # Review all changes
```

### Investigating a Traffic Drop
```
/seo drift compare https://example.com      # What changed?
/seo drift history https://example.com      # When did it change?
```

---

## Supporting file: references/comparison-rules.md

# SEO Drift Comparison Rules

17 rules across 3 severity levels. Each rule compares a specific SEO element
between the stored baseline and the current page state.

---

## CRITICAL (Immediate Action Required)

These changes typically cause measurable traffic loss within days.

### Rule 1: Schema/JSON-LD Completely Removed
- **Compare**: Baseline `schema` array has items, current is empty
- **Threshold**: Any schema present before, none now
- **Action**: Restore structured data immediately. Rich results will be lost within hours.
- **Cross-ref**: `/seo schema <url>`

### Rule 2: Canonical URL Changed
- **Compare**: Baseline `canonical` vs current `canonical`
- **Threshold**: Different non-null values (after normalization)
- **Action**: Verify the new canonical is intentional. Incorrect canonicals redirect ranking signals to wrong page.
- **Cross-ref**: `/seo technical <url>`

### Rule 3: Canonical URL Removed
- **Compare**: Baseline `canonical` was set, current is `null`
- **Threshold**: Had value, now missing
- **Action**: Restore canonical tag. Google will guess, often incorrectly for pages with query parameters.
- **Cross-ref**: `/seo technical <url>`

### Rule 4: Noindex Directive Added
- **Compare**: Baseline `meta_robots` did not contain "noindex", current does
- **Threshold**: "noindex" substring now present (case-insensitive)
- **Action**: If unintentional, remove immediately. Page will be dropped from index within days.
- **Cross-ref**: `/seo technical <url>`

### Rule 5: H1 Tag Removed Entirely
- **Compare**: Baseline `h1` had entries, current is empty
- **Threshold**: One or more H1s before, zero now
- **Action**: Restore H1 heading. Primary page topic signal for search engines.
- **Cross-ref**: `/seo content <url>`

### Rule 6: H1 Text Changed Significantly
- **Compare**: First H1 in baseline vs first H1 in current, SequenceMatcher ratio
- **Threshold**: Similarity ratio < 0.5 (>50% different)
- **Action**: Verify the H1 change aligns with target keyword strategy.
- **Cross-ref**: `/seo content <url>`

### Rule 7: Title Tag Removed Entirely
- **Compare**: Baseline `title` was set, current is `null` or empty
- **Threshold**: Had value, now missing
- **Action**: Restore title tag immediately. Google will auto-generate one, often poorly.
- **Cross-ref**: `/seo page <url>`

### Rule 8: HTTP Status Code Changed to Error
- **Compare**: Baseline `status_code` was 2xx, current is 4xx or 5xx
- **Threshold**: Status code class changed from success to client/server error
- **Action**: Investigate server error or missing page. Rankings will drop within days.
- **Cross-ref**: `/seo technical <url>`

---

## WARNING (Investigate Within 1 Week)

These changes may impact rankings or CTR but are sometimes intentional.

### Rule 9: Title Text Changed
- **Compare**: Baseline `title` vs current `title` (trimmed)
- **Threshold**: Strings differ (case-sensitive, whitespace-normalized)
- **Action**: Verify new title includes target keywords. Monitor CTR in GSC over 2 weeks.
- **Cross-ref**: `/seo page <url>`

### Rule 10: Meta Description Changed
- **Compare**: Baseline `meta_description` vs current `meta_description`
- **Threshold**: Strings differ (trimmed)
- **Action**: Verify new description includes call-to-action and target keywords. Monitor CTR.
- **Cross-ref**: `/seo page <url>`

### Rule 11: Core Web Vitals Metric Regressed >20%
- **Compare**: Each CWV metric p75 value (LCP, INP, CLS) baseline vs current
- **Threshold**: Current value is >20% worse than baseline (higher for LCP/INP, higher for CLS)
- **Action**: Investigate performance regression. Check recent code changes or third-party scripts.
- **Cross-ref**: `/seo technical <url>`

### Rule 12: CWV Performance Score Dropped 10+ Points
- **Compare**: Lighthouse performance score baseline vs current
- **Threshold**: Drop of 10 or more points (e.g., 85 to 74)
- **Action**: Run full PageSpeed analysis to identify new bottlenecks.
- **Cross-ref**: `/seo google psi <url>`

### Rule 13: OG Tags Removed
- **Compare**: Baseline `open_graph` had entries, current is empty
- **Threshold**: One or more OG tags before, none now
- **Action**: Restore OG tags. Social sharing will show generic/missing previews.
- **Cross-ref**: `/seo page <url>`

### Rule 14: Schema/JSON-LD Content Modified
- **Compare**: Baseline `schema_hash` vs current `schema_hash`
- **Threshold**: Hash differs AND schema still exists (removal is Rule 1)
- **Action**: Validate modified schema. Check for type changes, removed properties, or new validation errors.
- **Cross-ref**: `/seo schema <url>`

---

## INFO (Awareness Only)

These are tracked for completeness. Often positive or neutral changes.

### Rule 15: New Schema/JSON-LD Added
- **Compare**: Baseline `schema` was empty, current has items
- **Threshold**: No schema before, schema now present
- **Action**: Positive change. Validate the new schema with `/seo schema <url>`.
- **Cross-ref**: `/seo schema <url>`

### Rule 16: H2 Structure Changed
- **Compare**: Baseline `h2` array vs current `h2` array
- **Threshold**: Different number of H2s, or different H2 text values
- **Action**: Review heading hierarchy. Ensure content sections still align with target topics.
- **Cross-ref**: `/seo content <url>`

### Rule 17: Content Hash Changed
- **Compare**: Baseline `html_hash` vs current `html_hash`
- **Threshold**: Hash differs (catch-all for any body content change)
- **Action**: General content change detected. Review if no other rules triggered to understand what changed.
- **Cross-ref**: `/seo page <url>`
