
Seo Audit
- 601 installs
- 23.5k repo stars
- Updated July 17, 2026
- alirezarezvani/claude-skills
seo-audit is a Claude agent skill at version 1.0.0 that audits site SEO with Core Web Vitals scoring and weighted health reports for developers who need actionable ranking fixes on landing and marketing pages.
About
seo-audit is an alirezarezvani/claude-skills marketing skill at version 1.0.0 that audits landing and marketing pages across technical SEO, on-page elements, and content quality. Bundled scripts seo_checker.py and seo_health_scorer.py score individual pages 0–100 and produce weighted site health across seven categories with industry presets for saas, ecommerce, local, and publisher sites. Core Web Vitals thresholds encode 2026 CrUX rules: LCP ≤ 2.5s, INP ≤ 200ms, and CLS ≤ 0.1 at the 75th percentile with pass, warn, and fail mapping. Four reference files cover the full audit framework, E-E-A-T checklist, schema types, and CWV triage. Developers reach for seo-audit when diagnosing ranking drops, meta tag gaps, or performance-related SEO before launch. Output is a prioritized action plan with executive summary and scored findings per issue.
- Documents three Core Web Vitals: LCP, CLS, and INP with Good / Needs Improvement / Poor bands (2026 CrUX reference)
- Maps each metric to pass, warn, and fail states for automated seo_health_scorer-style workflows
- Lists concrete fixes: image preload/WebP, TTFB, render-blocking JS, explicit dimensions, font-display, long-task splitti
- Distinguishes field data (28-day CrUX p75) vs lab data (Lighthouse, PageSpeed)
- Performance category reference aligned with Google’s published CWV methodology
Seo Audit by the numbers
- 601 all-time installs (skills.sh)
- Ranked #685 of 1,879 Marketing & SEO skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
npx skills add https://github.com/alirezarezvani/claude-skills --skill seo-auditAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 601 |
|---|---|
| repo stars | ★ 23.5k |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 17, 2026 |
| Repository | alirezarezvani/claude-skills ↗ |
How do you audit landing pages for SEO health?
Audit landing and marketing pages against 2026 Core Web Vitals thresholds with pass, warn, and fail scoring for SEO health checks.
Who is it for?
Developers and growth engineers auditing SaaS or marketing sites who need scored technical, on-page, and CWV findings with remediation steps.
Skip if: Developers building programmatic SEO page farms at scale without an existing site to audit—use programmatic-seo instead.
When should I use this skill?
User mentions SEO audit, technical SEO, Core Web Vitals review, meta tag checks, or asks why pages are not ranking.
What you get
SEO audit report, 0–100 page and site health scores, prioritized fix list, and Core Web Vitals pass/warn/fail ratings.
- SEO audit report
- Weighted health score
- Prioritized action plan
By the numbers
- Version 1.0.0 with 2 bundled Python scripts
- Scores site health across 7 weighted categories
- Defines 3 Core Web Vitals metrics: LCP, INP, CLS
Files
SEO Audit
You are an expert in search engine optimization. Your goal is to identify SEO issues and provide actionable recommendations to improve organic search performance.
Initial Assessment
Check for product marketing context first: If .claude/product-marketing-context.md exists, read it before asking questions. Use that context and only ask for information not already covered or specific to this task.
Before auditing, understand:
1. Site Context
- What type of site? (SaaS, e-commerce, blog, etc.)
- What's the primary business goal for SEO?
- What keywords/topics are priorities?
2. Current State
- Any known issues or concerns?
- Current organic traffic level?
- Recent changes or migrations?
3. Scope
- Full site audit or specific pages?
- Technical + on-page, or one focus area?
- Access to Search Console / analytics?
---
Audit Framework
The audit walks three layers — technical (crawl/indexation/speed), on-page (titles, headings, internal links, keyword targeting), content (intent match, E-E-A-T, thin/duplicate pages). Full framework: references/seo-audit-reference.md.
Core Web Vitals pass/fail thresholds (75th percentile of real-user data; full triage in references/cwv-thresholds.md):
| Metric | Good | Needs improvement | Poor |
|---|---|---|---|
| LCP (Largest Contentful Paint) | ≤ 2.5s | 2.5-4.0s | > 4.0s |
| INP (Interaction to Next Paint) | ≤ 200ms | 200-500ms | > 500ms |
| CLS (Cumulative Layout Shift) | ≤ 0.1 | 0.1-0.25 | > 0.25 |
Tools
| Tool | Invocation | Output |
|---|---|---|
| On-page checker | python3 scripts/seo_checker.py --file page.html (or --url https://...; --json) | Scores a single page 0-100: title/meta/headings/links/images |
| Health scorer | python3 scripts/seo_health_scorer.py --checks checks.json --industry saas (no arg = --demo; industries: saas/ecommerce/local/publisher; --json) | Weighted 0-100 site health score across 7 categories |
Run seo_checker.py on the key templates/pages during the on-page layer, and seo_health_scorer.py on the completed check matrix to produce the audit's headline score.
Output Format
Audit Report Structure
Executive Summary
- Overall health assessment — lead with the
seo_health_scorer.pyscore and its weakest categories - Top 3-5 priority issues
- Quick wins identified
Technical SEO Findings For each issue:
- Issue: What's wrong
- Impact: SEO impact (High/Medium/Low)
- Evidence: How you found it
- Fix: Specific recommendation
- Priority: 1-5 or High/Medium/Low
On-Page SEO Findings Same format as above
Content Findings Same format as above
Prioritized Action Plan 1. Critical fixes (blocking indexation/ranking) 2. High-impact improvements 3. Quick wins (easy, immediate benefit) 4. Long-term recommendations
---
References
- SEO Audit Reference: Full audit framework, scoring, and remediation patterns
- Core Web Vitals Thresholds: LCP/INP/CLS targets and triage rules
- E-E-A-T Framework: Experience, Expertise, Authoritativeness, Trustworthiness checklist
- Schema Types: Structured data patterns by content type
---
Tools Referenced
Free Tools
- Google Search Console (essential)
- Google PageSpeed Insights
- Bing Webmaster Tools
- Rich Results Test
- Mobile-Friendly Test
- Schema Validator
Paid Tools (if available)
- Screaming Frog
- Ahrefs / Semrush
- Sitebulb
- ContentKing
---
Task-Specific Questions
1. What pages/keywords matter most? 2. Do you have Search Console access? 3. Any recent changes or migrations? 4. Who are your top organic competitors? 5. What's your current organic traffic baseline?
---
Related Skills
- programmatic-seo — WHEN: user wants to build SEO pages at scale after the audit identifies keyword gaps. WHEN NOT: don't use for diagnosing existing issues; stay in seo-audit mode.
- aeo — WHEN: user wants to optimize for AI answer engines (SGE, Perplexity, ChatGPT) in addition to traditional search. WHEN NOT: don't use for purely technical crawl/indexation issues.
- schema-markup — WHEN: audit reveals missing structured data opportunities (FAQ, HowTo, Product, Review schemas). WHEN NOT: don't use as a standalone fix when core technical SEO is broken.
- site-architecture — WHEN: audit uncovers poor internal linking, orphan pages, or crawl depth issues that need a structural redesign. WHEN NOT: don't involve when the audit scope is limited to on-page or content issues.
- content-strategy — WHEN: audit reveals thin content, keyword gaps, or lack of topical authority requiring a content plan. WHEN NOT: don't use when the problem is purely technical (robots.txt, redirects, speed).
- marketing-context — WHEN: always read first if
.claude/product-marketing-context.mdexists to avoid redundant questions. WHEN NOT: skip if no context file exists and user has provided all necessary product info directly.
---
Communication
All audit output follows the SEO Audit Quality Standard:
- Lead with the executive summary (3-5 bullets max)
- Findings use the Issue / Impact / Evidence / Fix / Priority format consistently
- Prioritized Action Plan is always the final deliverable section
- Avoid jargon without explanation; write for a technically-aware but non-SEO-specialist reader
- Quick wins are called out explicitly and kept separate from high-effort recommendations
- Never present recommendations without evidence or rationale
---
Proactive Triggers
Automatically surface seo-audit recommendations when:
1. Traffic drop mentioned — User says organic traffic dropped or rankings fell; immediately frame an audit scope. 2. Site migration or redesign — User mentions a planned or recent URL change, platform switch, or redesign; flag pre/post-migration audit needs. 3. "Why isn't my page ranking?" — Any ranking frustration triggers the on-page + intent checklist before external factors. 4. Content strategy discussion — When content-strategy skill is active and keyword gaps appear, proactively suggest an SEO audit to validate opportunity. 5. New site or product launch — User preparing a launch; proactively recommend a technical SEO pre-launch checklist from the audit framework.
---
Output Artifacts
| Artifact | Format | Description |
|---|---|---|
| Executive Summary | Markdown bullets | 3-5 top issues + quick wins, suitable for sharing with stakeholders |
| Technical SEO Findings | Structured table | Issue / Impact / Evidence / Fix / Priority per finding |
| On-Page SEO Findings | Structured table | Same format, focused on content and metadata |
| Prioritized Action Plan | Numbered list | Ordered by impact × effort, grouped into Critical / High / Quick Wins |
| Keyword Cannibalization Map | Table | Pages competing for same keyword with recommended canonical or redirect actions |
Core Web Vitals Thresholds (2026)
Reference for seo_health_scorer.py and the Performance category. Updated per Google's CrUX methodology.
The three metrics
| Metric | Good | Needs Improvement | Poor | What it measures |
|---|---|---|---|---|
| LCP (Largest Contentful Paint) | ≤ 2.5s | 2.5s – 4.0s | > 4.0s | Loading speed — when the main content is visible |
| CLS (Cumulative Layout Shift) | ≤ 0.1 | 0.1 – 0.25 | > 0.25 | Visual stability — unexpected layout jumps |
| INP (Interaction to Next Paint) | ≤ 200ms | 200ms – 500ms | > 500ms | Responsiveness — delay after user interaction |
Scoring mapping
pass = metric in "Good" range
warn = metric in "Needs Improvement" range
fail = metric in "Poor" rangeCommon fixes by metric
LCP > 2.5s
- Optimize largest image (WebP/AVIF, proper sizing, preload)
- Reduce server response time (TTFB < 800ms)
- Remove render-blocking CSS/JS
- Use CDN for static assets
CLS > 0.1
- Set explicit width/height on images and video
- Reserve space for ads/embeds before load
- Avoid inserting content above the fold after page load
- Use
font-display: swapfor web fonts
INP > 200ms
- Break up long tasks (> 50ms) into smaller chunks
- Defer non-critical JavaScript
- Use
requestIdleCallbackfor low-priority work - Minimize main-thread blocking from third-party scripts
Measurement sources
- Field data: CrUX (Chrome User Experience Report) — 28-day rolling p75
- Lab data: Lighthouse, PageSpeed Insights — synthetic, not real users
- Monitoring: web-vitals.js library for RUM (Real User Monitoring)
Field data (CrUX) is what Google uses for ranking signals. Lab data helps debug but doesn't directly affect rankings.
E-E-A-T Framework Reference
Experience, Expertise, Authoritativeness, Trustworthiness — Google's quality rater guidelines framework for evaluating content quality. Updated per September 2025 Quality Rater Guidelines.
The four signals
| Signal | What it means | How to demonstrate |
|---|---|---|
| Experience | First-hand experience with the topic | Personal anecdotes, original photos, usage details, "I tested this" |
| Expertise | Demonstrable knowledge or skill | Credentials, detailed technical depth, accurate terminology |
| Authoritativeness | Recognition by others in the field | Backlinks from authoritative sites, citations, brand mentions |
| Trustworthiness | Accuracy, transparency, safety | HTTPS, clear authorship, sources cited, contact info, privacy policy |
Trust is the most important — it encompasses the other three. A page can have expertise but lose trust through misleading claims.
Audit checklist
Experience signals
- [ ] Author has demonstrable experience with the topic
- [ ] Content includes first-hand observations (not just research summaries)
- [ ] Original images or screenshots (not stock photos)
- [ ] Specific details that only come from experience
Expertise signals
- [ ] Author bio with relevant credentials
- [ ] Content depth matches topic complexity (YMYL topics need more)
- [ ] Accurate, up-to-date information
- [ ] Proper use of domain terminology
Authoritativeness signals
- [ ] Site is recognized in its niche (backlink profile)
- [ ] Author has published elsewhere on this topic
- [ ] Content is cited by other authoritative sources
- [ ] Organization has relevant credentials/certifications
Trustworthiness signals
- [ ] HTTPS enforced
- [ ] Clear author attribution (name, bio, photo)
- [ ] Sources cited with links
- [ ] Contact information accessible
- [ ] Privacy policy present
- [ ] No misleading claims or clickbait
- [ ] Content reviewed or updated regularly (dateModified)
YMYL topics (Your Money or Your Life)
Topics that could impact health, safety, financial stability, or well-being require higher E-E-A-T standards:
- Health and medical information
- Financial advice and transactions
- Legal information
- News and current events
- Safety-related information
- Shopping (when significant money is involved)
For YMYL: authors MUST have verifiable credentials. Self-reported expertise is insufficient.
Scoring for seo_health_scorer.py
Content category checks → E-E-A-T subchecks:
Author bio present and linked → pass/fail (high severity)
Sources cited with URLs → pass/fail (medium severity)
dateModified within 12 months → pass/warn/fail (medium severity)
YMYL topic + qualified author → pass/fail (critical severity for YMYL)
Contact information accessible → pass/fail (medium severity)Schema Markup Types Reference
JSON-LD structured data types, their status, and validation guidance. Updated for 2026.
Active schema types (use these)
| Type | Use case | Rich result? | Priority |
|---|---|---|---|
Organization | Company/brand identity | Knowledge panel | High — every site |
WebSite | Site-level info + sitelinks search | Sitelinks searchbox | High — every site |
BreadcrumbList | Navigation path | Breadcrumbs in SERPs | High — multi-level sites |
Article / NewsArticle | Blog posts, news | Article cards, Top Stories | High — publishers |
Product | Product pages | Price, availability, reviews | Critical — ecommerce |
FAQPage | FAQ sections | Expandable FAQ in SERPs | Medium |
HowTo | Step-by-step guides | Step-by-step cards | Medium |
LocalBusiness | Physical businesses | Maps, hours, reviews | Critical — local |
Review / AggregateRating | Reviews and ratings | Star ratings in SERPs | High — review sites |
Event | Events with dates | Event cards | Medium — event sites |
VideoObject | Video content | Video cards | Medium — video pages |
SoftwareApplication | Apps/tools | App info in SERPs | Medium — SaaS |
Course | Educational content | Course cards | Medium — edu sites |
Recipe | Recipes | Recipe cards | High — food sites |
JobPosting | Job listings | Job search integration | High — job sites |
Deprecated / reduced (avoid or migrate)
| Type | Status | Since | Notes |
|---|---|---|---|
HowTo | Reduced visibility | Aug 2023 | Still valid but rarely shown as rich result |
SpecialAnnouncement | Deprecated | 2024 | Was for COVID; no longer processed |
QAPage | Reduced | 2024 | Showing less frequently; prefer FAQPage |
Validation checklist
For each schema type on the page:
- [ ] Valid JSON-LD (not Microdata or RDFa — JSON-LD preferred)
- [ ] Passes Google Rich Results Test
- [ ] No warnings in Search Console
- [ ] Required properties present (per schema.org spec)
- [ ] No fabricated reviews or ratings
- [ ] @type matches actual page content (no schema spam)
- [ ] Nested schemas properly connected (@id references)
Common mistakes
- Schema spam: Adding Review schema to non-review pages
- Self-serving reviews: AggregateRating on your own product page without genuine reviews
- Missing required fields: Product without
offers, Article withoutauthor - Stale data: Event schema with past dates still active
- Duplicate schemas: Multiple Organization schemas on one page
seo-audit reference
Audit Framework
Priority Order
1. Crawlability & Indexation (can Google find and index it?) 2. Technical Foundations (is the site fast and functional?) 3. On-Page Optimization (is content optimized?) 4. Content Quality (does it deserve to rank?) 5. Authority & Links (does it have credibility?)
---
Technical SEO Audit
Crawlability
Robots.txt
- Check for unintentional blocks
- Verify important pages allowed
- Check sitemap reference
XML Sitemap
- Exists and accessible
- Submitted to Search Console
- Contains only canonical, indexable URLs
- Updated regularly
- Proper formatting
Site Architecture
- Important pages within 3 clicks of homepage
- Logical hierarchy
- Internal linking structure
- No orphan pages
Crawl Budget Issues (for large sites)
- Parameterized URLs under control
- Faceted navigation handled properly
- Infinite scroll with pagination fallback
- Session IDs not in URLs
Indexation
Index Status
- site:domain.com check
- Search Console coverage report
- Compare indexed vs. expected
Indexation Issues
- Noindex tags on important pages
- Canonicals pointing wrong direction
- Redirect chains/loops
- Soft 404s
- Duplicate content without canonicals
Canonicalization
- All pages have canonical tags
- Self-referencing canonicals on unique pages
- HTTP → HTTPS canonicals
- www vs. non-www consistency
- Trailing slash consistency
Site Speed & Core Web Vitals
Core Web Vitals
- LCP (Largest Contentful Paint): < 2.5s
- INP (Interaction to Next Paint): < 200ms
- CLS (Cumulative Layout Shift): < 0.1
Speed Factors
- Server response time (TTFB)
- Image optimization
- JavaScript execution
- CSS delivery
- Caching headers
- CDN usage
- Font loading
Tools
- PageSpeed Insights
- WebPageTest
- Chrome DevTools
- Search Console Core Web Vitals report
Mobile-Friendliness
- Responsive design (not separate m. site)
- Tap target sizes
- Viewport configured
- No horizontal scroll
- Same content as desktop
- Mobile-first indexing readiness
Security & HTTPS
- HTTPS across entire site
- Valid SSL certificate
- No mixed content
- HTTP → HTTPS redirects
- HSTS header (bonus)
URL Structure
- Readable, descriptive URLs
- Keywords in URLs where natural
- Consistent structure
- No unnecessary parameters
- Lowercase and hyphen-separated
---
On-Page SEO Audit
Title Tags
Check for:
- Unique titles for each page
- Primary keyword near beginning
- 50-60 characters (visible in SERP)
- Compelling and click-worthy
- Brand name placement (end, usually)
Common issues:
- Duplicate titles
- Too long (truncated)
- Too short (wasted opportunity)
- Keyword stuffing
- Missing entirely
Meta Descriptions
Check for:
- Unique descriptions per page
- 150-160 characters
- Includes primary keyword
- Clear value proposition
- Call to action
Common issues:
- Duplicate descriptions
- Auto-generated garbage
- Too long/short
- No compelling reason to click
Heading Structure
Check for:
- One H1 per page
- H1 contains primary keyword
- Logical hierarchy (H1 → H2 → H3)
- Headings describe content
- Not just for styling
Common issues:
- Multiple H1s
- Skip levels (H1 → H3)
- Headings used for styling only
- No H1 on page
Content Optimization
Primary Page Content
- Keyword in first 100 words
- Related keywords naturally used
- Sufficient depth/length for topic
- Answers search intent
- Better than competitors
Thin Content Issues
- Pages with little unique content
- Tag/category pages with no value
- Doorway pages
- Duplicate or near-duplicate content
Image Optimization
Check for:
- Descriptive file names
- Alt text on all images
- Alt text describes image
- Compressed file sizes
- Modern formats (WebP)
- Lazy loading implemented
- Responsive images
Internal Linking
Check for:
- Important pages well-linked
- Descriptive anchor text
- Logical link relationships
- No broken internal links
- Reasonable link count per page
Common issues:
- Orphan pages (no internal links)
- Over-optimized anchor text
- Important pages buried
- Excessive footer/sidebar links
Keyword Targeting
Per Page
- Clear primary keyword target
- Title, H1, URL aligned
- Content satisfies search intent
- Not competing with other pages (cannibalization)
Site-Wide
- Keyword mapping document
- No major gaps in coverage
- No keyword cannibalization
- Logical topical clusters
---
Content Quality Assessment
E-E-A-T Signals
Experience
- First-hand experience demonstrated
- Original insights/data
- Real examples and case studies
Expertise
- Author credentials visible
- Accurate, detailed information
- Properly sourced claims
Authoritativeness
- Recognized in the space
- Cited by others
- Industry credentials
Trustworthiness
- Accurate information
- Transparent about business
- Contact information available
- Privacy policy, terms
- Secure site (HTTPS)
Content Depth
- Comprehensive coverage of topic
- Answers follow-up questions
- Better than top-ranking competitors
- Updated and current
User Engagement Signals
- Time on page
- Bounce rate in context
- Pages per session
- Return visits
---
Common Issues by Site Type
SaaS/Product Sites
- Product pages lack content depth
- Blog not integrated with product pages
- Missing comparison/alternative pages
- Feature pages thin on content
- No glossary/educational content
E-commerce
- Thin category pages
- Duplicate product descriptions
- Missing product schema
- Faceted navigation creating duplicates
- Out-of-stock pages mishandled
Content/Blog Sites
- Outdated content not refreshed
- Keyword cannibalization
- No topical clustering
- Poor internal linking
- Missing author pages
Local Business
- Inconsistent NAP
- Missing local schema
- No Google Business Profile optimization
- Missing location pages
- No local content
---
#!/usr/bin/env python3
"""
seo_checker.py — On-page SEO analyzer
Usage:
python3 seo_checker.py [--file page.html] [--url https://...] [--json]
python3 seo_checker.py # demo mode with embedded sample HTML
"""
import argparse
import json
import math
import re
import sys
import urllib.request
from html.parser import HTMLParser
# ---------------------------------------------------------------------------
# HTML Parser
# ---------------------------------------------------------------------------
class SEOParser(HTMLParser):
def __init__(self):
super().__init__()
self.title = ""
self._in_title = False
self.meta_description = ""
self.h_tags = [] # list of (level, text)
self._current_h = None
self._current_h_text = []
self.images = [] # list of {"src": ..., "alt": ...}
self._in_body = False
self.links = [] # list of {"href": ..., "text": ...}
self._current_link_text = []
self._current_link_href = ""
self._in_link = False
self.body_text_parts = []
self._in_script = False
self._in_style = False
self.viewport_meta = False
def handle_starttag(self, tag, attrs):
attrs_dict = dict(attrs)
tag = tag.lower()
if tag == "title":
self._in_title = True
elif tag == "meta":
name = attrs_dict.get("name", "").lower()
prop = attrs_dict.get("property", "").lower()
if name == "description":
self.meta_description = attrs_dict.get("content", "")
if name == "viewport":
self.viewport_meta = True
if prop == "og:description" and not self.meta_description:
self.meta_description = attrs_dict.get("content", "")
elif tag in ("h1", "h2", "h3", "h4", "h5", "h6"):
self._current_h = int(tag[1])
self._current_h_text = []
elif tag == "img":
self.images.append({
"src": attrs_dict.get("src", ""),
"alt": attrs_dict.get("alt", None),
})
elif tag == "a":
self._in_link = True
self._current_link_href = attrs_dict.get("href", "")
self._current_link_text = []
elif tag == "body":
self._in_body = True
elif tag == "script":
self._in_script = True
elif tag == "style":
self._in_style = True
def handle_endtag(self, tag):
tag = tag.lower()
if tag == "title":
self._in_title = False
elif tag in ("h1", "h2", "h3", "h4", "h5", "h6"):
if self._current_h is not None:
self.h_tags.append((self._current_h, " ".join(self._current_h_text).strip()))
self._current_h = None
self._current_h_text = []
elif tag == "a":
if self._in_link:
self.links.append({
"href": self._current_link_href,
"text": " ".join(self._current_link_text).strip(),
})
self._in_link = False
self._current_link_text = []
self._current_link_href = ""
elif tag == "script":
self._in_script = False
elif tag == "style":
self._in_style = False
def handle_data(self, data):
if self._in_title:
self.title += data
if self._current_h is not None:
self._current_h_text.append(data)
if self._in_link:
self._current_link_text.append(data)
if self._in_body and not self._in_script and not self._in_style:
self.body_text_parts.append(data)
# ---------------------------------------------------------------------------
# Analysis helpers
# ---------------------------------------------------------------------------
def _is_external(href, base_domain=""):
if not href:
return False
return href.startswith("http://") or href.startswith("https://")
def analyze_html(html: str, base_domain: str = "") -> dict:
parser = SEOParser()
parser.feed(html)
results = {}
# --- Title ---
title = parser.title.strip()
title_len = len(title)
title_ok = 50 <= title_len <= 60
results["title"] = {
"value": title,
"length": title_len,
"optimal_range": "50-60 chars",
"pass": title_ok,
"score": 100 if title_ok else (50 if title else 0),
"note": "Good length" if title_ok else (
f"Too {'short' if title_len < 50 else 'long'} ({title_len} chars)" if title else "Missing title tag"
),
}
# --- Meta description ---
desc = parser.meta_description.strip()
desc_len = len(desc)
desc_ok = 150 <= desc_len <= 160
results["meta_description"] = {
"value": desc[:80] + ("..." if len(desc) > 80 else ""),
"length": desc_len,
"optimal_range": "150-160 chars",
"pass": desc_ok,
"score": 100 if desc_ok else (50 if 100 <= desc_len < 150 or 160 < desc_len <= 200 else (30 if desc else 0)),
"note": "Good length" if desc_ok else (
f"Too {'short' if desc_len < 150 else 'long'} ({desc_len} chars)" if desc else "Missing meta description"
),
}
# --- H1 ---
h1s = [t for lvl, t in parser.h_tags if lvl == 1]
h1_count = len(h1s)
h1_ok = h1_count == 1
results["h1"] = {
"count": h1_count,
"values": h1s,
"pass": h1_ok,
"score": 100 if h1_ok else (50 if h1_count > 1 else 0),
"note": "Exactly one H1 ✓" if h1_ok else (
f"Multiple H1s ({h1_count})" if h1_count > 1 else "No H1 found"
),
}
# --- Heading hierarchy ---
heading_issues = []
prev_level = 0
for lvl, _ in parser.h_tags:
if prev_level and lvl > prev_level + 1:
heading_issues.append(f"H{prev_level} → H{lvl} skips a level")
prev_level = lvl
hierarchy_ok = len(heading_issues) == 0
results["heading_hierarchy"] = {
"headings": [(f"H{l}", t[:60]) for l, t in parser.h_tags],
"issues": heading_issues,
"pass": hierarchy_ok,
"score": max(0, 100 - len(heading_issues) * 25),
"note": "Hierarchy OK" if hierarchy_ok else f"{len(heading_issues)} level-skip issue(s)",
}
# --- Image alt text ---
total_imgs = len(parser.images)
imgs_with_alt = sum(1 for img in parser.images if img["alt"] is not None and img["alt"].strip())
alt_pct = (imgs_with_alt / total_imgs * 100) if total_imgs else 100
alt_ok = alt_pct == 100
results["image_alt_text"] = {
"total_images": total_imgs,
"with_alt": imgs_with_alt,
"coverage_pct": round(alt_pct, 1),
"pass": alt_ok,
"score": round(alt_pct),
"note": "All images have alt text" if alt_ok else f"{total_imgs - imgs_with_alt} image(s) missing alt",
}
# --- Link ratio ---
total_links = len(parser.links)
ext_links = sum(1 for l in parser.links if _is_external(l["href"], base_domain))
int_links = total_links - ext_links
ratio = (int_links / total_links) if total_links else 0
ratio_ok = ratio >= 0.5 or total_links == 0
results["link_ratio"] = {
"total_links": total_links,
"internal": int_links,
"external": ext_links,
"internal_pct": round(ratio * 100, 1),
"pass": ratio_ok,
"score": 100 if ratio_ok else round(ratio * 100),
"note": "Good internal/external balance" if ratio_ok else "More external than internal links",
}
# --- Word count ---
body_text = " ".join(parser.body_text_parts)
words = re.findall(r"\b\w+\b", body_text)
word_count = len(words)
wc_ok = word_count >= 300
results["word_count"] = {
"count": word_count,
"minimum": 300,
"pass": wc_ok,
"score": min(100, round(word_count / 300 * 100)) if not wc_ok else 100,
"note": f"{word_count} words (good)" if wc_ok else f"Only {word_count} words — need 300+",
}
# --- Viewport meta ---
results["viewport_meta"] = {
"present": parser.viewport_meta,
"pass": parser.viewport_meta,
"score": 100 if parser.viewport_meta else 0,
"note": "Mobile viewport tag present" if parser.viewport_meta else "Missing viewport meta tag",
}
return results
def compute_overall_score(results: dict) -> int:
weights = {
"title": 20,
"meta_description": 15,
"h1": 15,
"heading_hierarchy": 10,
"image_alt_text": 10,
"link_ratio": 10,
"word_count": 15,
"viewport_meta": 5,
}
total_w = sum(weights.values())
score = sum(results[k]["score"] * w for k, w in weights.items() if k in results)
return round(score / total_w)
# ---------------------------------------------------------------------------
# Demo HTML
# ---------------------------------------------------------------------------
DEMO_HTML = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>10 Ways to Boost Your Marketing ROI in 2024</title>
<meta name="description" content="Discover ten proven strategies to maximize your marketing return on investment, reduce wasted ad spend, and grow revenue faster with data-driven techniques.">
</head>
<body>
<h1>10 Ways to Boost Your Marketing ROI in 2024</h1>
<p>Marketing budgets are tight. Every dollar counts. Here is how to make yours work harder.</p>
<h2>1. Audit Your Current Spend</h2>
<p>Before adding channels, understand where money goes. Most companies waste 30% of budget on low-ROI tactics.</p>
<img src="audit-chart.png" alt="Marketing spend audit chart showing channel breakdown">
<h2>2. Double Down on SEO</h2>
<p>Organic traffic compounds. Paid stops the moment you stop spending. Invest in content that ranks.</p>
<img src="seo-graph.png" alt="SEO traffic growth over 12 months">
<h3>On-Page Optimization</h3>
<p>Start with title tags, meta descriptions, and heading structure before anything else.</p>
<h2>3. Improve Email Open Rates</h2>
<p>Subject lines determine 80% of open rates. Test at least three variants per campaign.</p>
<a href="/email-templates">Email templates library</a>
<a href="https://mailchimp.com">Mailchimp</a>
<h2>4. Use Retargeting Wisely</h2>
<p>Retargeting works best with frequency caps. Show the same ad more than 7 times and you hurt brand perception.</p>
<h2>5. Build Landing Pages That Convert</h2>
<p>A single focused landing page beats a homepage for paid traffic every time. Remove navigation. Add a clear CTA.</p>
<a href="/landing-page-guide">Landing page guide</a>
<a href="/cro-checklist">CRO checklist</a>
<a href="https://unbounce.com">Unbounce</a>
<p>With these strategies you should see measurable improvement within 90 days. Start with the audit — it reveals the quickest wins.</p>
</body>
</html>"""
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="On-page SEO checker — scores an HTML page 0-100."
)
parser.add_argument("--file", help="Path to HTML file")
parser.add_argument("--url", help="URL to fetch and analyze")
parser.add_argument("--domain", default="", help="Base domain for internal link detection")
parser.add_argument("--json", action="store_true", help="Output as JSON")
args = parser.parse_args()
if args.file:
with open(args.file, "r", encoding="utf-8", errors="replace") as f:
html = f.read()
elif args.url:
with urllib.request.urlopen(args.url, timeout=10) as resp:
html = resp.read().decode("utf-8", errors="replace")
else:
html = DEMO_HTML
if not args.json:
print("No input provided — running in demo mode.\n")
results = analyze_html(html, base_domain=args.domain)
overall = compute_overall_score(results)
if args.json:
output = {"overall_score": overall, "checks": results}
print(json.dumps(output, indent=2))
return
# Human-readable output
ICONS = {True: "✅", False: "❌"}
print("=" * 60)
print(f" SEO AUDIT RESULTS Overall Score: {overall}/100")
print("=" * 60)
checks = [
("Title Tag", "title"),
("Meta Description", "meta_description"),
("H1 Tag", "h1"),
("Heading Hierarchy", "heading_hierarchy"),
("Image Alt Text", "image_alt_text"),
("Link Ratio", "link_ratio"),
("Word Count", "word_count"),
("Viewport Meta", "viewport_meta"),
]
for label, key in checks:
r = results[key]
icon = ICONS[r["pass"]]
score = r["score"]
note = r["note"]
print(f" {icon} {label:<22} [{score:>3}/100] {note}")
print("=" * 60)
# Grade
grade = "A" if overall >= 90 else "B" if overall >= 75 else "C" if overall >= 60 else "D" if overall >= 40 else "F"
print(f" Grade: {grade} Score: {overall}/100")
print("=" * 60)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
seo_health_scorer.py — Weighted 0-100 SEO health score across 7 categories.
Inspired by the scoring methodology used in top SEO audit tools. Replaces
binary pass/fail with a quantified health score that enables trend tracking,
stakeholder reporting, and prioritized action plans.
Categories and default weights:
Technical SEO: 22% (crawlability, indexation, redirects, sitemaps, robots.txt)
Content Quality: 23% (thin content, duplicate, keyword stuffing, readability)
On-Page SEO: 20% (titles, metas, headings, internal links, alt text)
Schema Markup: 10% (JSON-LD, breadcrumbs, FAQ, product, article schemas)
Performance: 10% (Core Web Vitals thresholds: LCP, CLS, INP)
AI Readiness: 10% (answer-first format, citability, entity clarity)
Images: 5% (alt text, compression, lazy loading, format)
Each check within a category returns pass/warn/fail. Severity multipliers:
pass = 1.0, warn = 0.5, fail = 0.0
Industry profiles adjust weights:
SaaS → Technical +5%, Content +5%, Schema -5%, Images -5%
Ecommerce → Schema +5%, Images +5%, AI Readiness -5%, Content -5%
Local → On-Page +5%, Schema +5%, Technical -5%, AI Readiness -5%
Publisher → Content +5%, AI Readiness +5%, Technical -5%, Schema -5%
Usage:
python seo_health_scorer.py --checks checks.json
python seo_health_scorer.py --checks checks.json --industry saas
python seo_health_scorer.py --checks checks.json --json
python seo_health_scorer.py --demo
Input format (checks.json):
[
{"category": "technical", "check": "robots.txt exists", "result": "pass", "severity": "critical"},
{"category": "content", "check": "thin content pages", "result": "fail", "severity": "high", "detail": "12 pages < 300 words"},
...
]
Severity levels for priority ordering:
critical — blocks indexation or causes penalties (fix immediately)
high — significantly impacts rankings (fix within 1 week)
medium — optimization opportunity (fix within 1 month)
low — backlog polish item
"""
from __future__ import annotations
import argparse
import json
import sys
from collections import defaultdict
from pathlib import Path
DEFAULT_WEIGHTS = {
"technical": 0.22,
"content": 0.23,
"on_page": 0.20,
"schema": 0.10,
"performance": 0.10,
"ai_readiness": 0.10,
"images": 0.05,
}
INDUSTRY_ADJUSTMENTS = {
"saas": {"technical": 0.05, "content": 0.05, "schema": -0.05, "images": -0.05},
"ecommerce": {"schema": 0.05, "images": 0.05, "ai_readiness": -0.05, "content": -0.05},
"local": {"on_page": 0.05, "schema": 0.05, "technical": -0.05, "ai_readiness": -0.05},
"publisher": {"content": 0.05, "ai_readiness": 0.05, "technical": -0.05, "schema": -0.05},
}
RESULT_SCORES = {"pass": 1.0, "warn": 0.5, "fail": 0.0}
SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3}
DEMO_CHECKS = [
{"category": "technical", "check": "robots.txt exists", "result": "pass", "severity": "critical"},
{"category": "technical", "check": "sitemap.xml valid", "result": "pass", "severity": "critical"},
{"category": "technical", "check": "no redirect chains", "result": "warn", "severity": "high", "detail": "2 chains found (3-hop)"},
{"category": "technical", "check": "canonical tags present", "result": "pass", "severity": "high"},
{"category": "technical", "check": "mobile-friendly", "result": "pass", "severity": "critical"},
{"category": "content", "check": "no thin pages (<300 words)", "result": "fail", "severity": "high", "detail": "8 pages under 300 words"},
{"category": "content", "check": "no duplicate titles", "result": "pass", "severity": "medium"},
{"category": "content", "check": "no keyword stuffing", "result": "pass", "severity": "medium"},
{"category": "content", "check": "readability (Flesch 60-70)", "result": "warn", "severity": "low", "detail": "avg Flesch 52"},
{"category": "on_page", "check": "title tags 50-60 chars", "result": "warn", "severity": "high", "detail": "4 pages over 60 chars"},
{"category": "on_page", "check": "meta descriptions 150-160", "result": "fail", "severity": "medium", "detail": "12 pages missing meta description"},
{"category": "on_page", "check": "H1 tags present and unique", "result": "pass", "severity": "high"},
{"category": "on_page", "check": "internal linking (min 3 per page)", "result": "warn", "severity": "medium", "detail": "6 pages have <3 internal links"},
{"category": "on_page", "check": "all images have alt text", "result": "fail", "severity": "medium", "detail": "23 images missing alt"},
{"category": "schema", "check": "JSON-LD Organization schema", "result": "pass", "severity": "medium"},
{"category": "schema", "check": "breadcrumb schema", "result": "fail", "severity": "medium", "detail": "no breadcrumb markup found"},
{"category": "schema", "check": "article/product schema", "result": "pass", "severity": "medium"},
{"category": "performance", "check": "LCP < 2.5s", "result": "pass", "severity": "critical"},
{"category": "performance", "check": "CLS < 0.1", "result": "warn", "severity": "high", "detail": "CLS 0.14 on mobile"},
{"category": "performance", "check": "INP < 200ms", "result": "pass", "severity": "high"},
{"category": "ai_readiness", "check": "answer-first paragraphs", "result": "fail", "severity": "medium", "detail": "most H2s don't start with a direct answer"},
{"category": "ai_readiness", "check": "entity clarity", "result": "warn", "severity": "low", "detail": "ambiguous entity references on 3 pages"},
{"category": "images", "check": "WebP/AVIF format", "result": "warn", "severity": "low", "detail": "14 images still JPEG"},
{"category": "images", "check": "lazy loading", "result": "pass", "severity": "medium"},
]
def get_weights(industry=None):
weights = dict(DEFAULT_WEIGHTS)
if industry and industry in INDUSTRY_ADJUSTMENTS:
for cat, adj in INDUSTRY_ADJUSTMENTS[industry].items():
weights[cat] = weights.get(cat, 0) + adj
return weights
def score_checks(checks, industry=None):
weights = get_weights(industry)
by_category = defaultdict(list)
for c in checks:
cat = c.get("category", "other").lower().replace("-", "_").replace(" ", "_")
by_category[cat].append(c)
category_scores = {}
all_findings = []
for cat, cat_checks in by_category.items():
if not cat_checks:
continue
total = 0.0
for check in cat_checks:
result = check.get("result", "fail").lower()
total += RESULT_SCORES.get(result, 0.0)
if result != "pass":
all_findings.append({
"category": cat,
"check": check.get("check", ""),
"result": result,
"severity": check.get("severity", "medium"),
"detail": check.get("detail", ""),
})
cat_score = (total / len(cat_checks)) * 100 if cat_checks else 100
category_scores[cat] = round(cat_score, 1)
# Weighted overall score
overall = 0.0
total_weight = 0.0
for cat, weight in weights.items():
if cat in category_scores:
overall += category_scores[cat] * weight
total_weight += weight
if total_weight > 0:
overall = overall / total_weight
else:
overall = 0.0
# Sort findings by severity
all_findings.sort(key=lambda f: SEVERITY_ORDER.get(f["severity"], 99))
# Quick wins: high/critical severity + likely fast fix
quick_wins = [f for f in all_findings if f["severity"] in ("critical", "high") and f["result"] == "warn"]
# Grade
if overall >= 90:
grade = "A"
elif overall >= 75:
grade = "B"
elif overall >= 60:
grade = "C"
elif overall >= 40:
grade = "D"
else:
grade = "F"
return {
"overall_score": round(overall, 1),
"grade": grade,
"industry": industry or "general",
"weights_used": {k: round(v, 2) for k, v in weights.items()},
"category_scores": category_scores,
"total_checks": len(checks),
"passed": sum(1 for c in checks if c.get("result") == "pass"),
"warnings": sum(1 for c in checks if c.get("result") == "warn"),
"failures": sum(1 for c in checks if c.get("result") == "fail"),
"findings": all_findings,
"quick_wins": quick_wins,
"action_plan": {
"critical": [f for f in all_findings if f["severity"] == "critical"],
"high": [f for f in all_findings if f["severity"] == "high"],
"medium": [f for f in all_findings if f["severity"] == "medium"],
"low": [f for f in all_findings if f["severity"] == "low"],
},
}
def print_report(result):
print(f"SEO Health Score: {result['overall_score']}/100 (Grade: {result['grade']})")
print(f"Industry profile: {result['industry']}")
print(f"Checks: {result['total_checks']} total — {result['passed']} pass, {result['warnings']} warn, {result['failures']} fail")
print()
print("Category Breakdown:")
for cat, score in sorted(result["category_scores"].items()):
weight = result["weights_used"].get(cat, 0)
bar = "█" * int(score / 5) + "░" * (20 - int(score / 5))
print(f" {cat:15s} {bar} {score:5.1f}/100 (weight {weight:.0%})")
print()
if result["quick_wins"]:
print(f"Quick Wins ({len(result['quick_wins'])}):")
for f in result["quick_wins"]:
print(f" ⚡ [{f['severity'].upper()}] {f['check']}: {f['detail']}")
print()
for level in ("critical", "high", "medium", "low"):
items = result["action_plan"][level]
if items:
print(f"{level.upper()} ({len(items)}):")
for f in items:
detail = f" — {f['detail']}" if f["detail"] else ""
print(f" [{f['result'].upper()}] {f['check']}{detail}")
print()
def main():
p = argparse.ArgumentParser(
description="Compute a weighted 0-100 SEO health score across 7 categories.",
epilog="Run with --demo to see a sample report. Provide checks as JSON for real audits.",
)
p.add_argument("--checks", help="Path to checks JSON file (array of check objects)")
p.add_argument(
"--industry",
choices=["saas", "ecommerce", "local", "publisher"],
default=None,
help="Industry profile adjusts category weights",
)
p.add_argument("--json", action="store_true", help="JSON output")
p.add_argument("--demo", action="store_true", help="Run with demo data")
args = p.parse_args()
if args.demo:
checks = DEMO_CHECKS
elif args.checks:
path = Path(args.checks)
if not path.exists():
print(f"[error] {path} not found", file=sys.stderr)
sys.exit(1)
checks = json.loads(path.read_text(encoding="utf-8"))
else:
p.print_help()
sys.exit(0)
result = score_checks(checks, industry=args.industry)
if args.json:
print(json.dumps(result, indent=2))
else:
print_report(result)
if __name__ == "__main__":
main()
Related skills
FAQ
What Core Web Vitals thresholds does seo-audit use?
seo-audit uses 2026 CrUX thresholds at the 75th percentile: LCP ≤ 2.5s good, INP ≤ 200ms good, CLS ≤ 0.1 good, with warn and fail bands documented in references/cwv-thresholds.md.
Which scripts power seo-audit scoring?
seo-audit runs seo_checker.py for per-page 0–100 on-page scores and seo_health_scorer.py for weighted site health across 7 categories with saas, ecommerce, local, and publisher industry presets.
What version is the seo-audit skill?
seo-audit is version 1.0.0 in alirezarezvani/claude-skills marketing-skill, updated 2026-03-06, with 2 Python scripts and 4 reference markdown files.
Is Seo Audit safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.