
Seo Audit
- 861 installs
- 336 repo stars
- Updated July 20, 2026
- seo-skills/seo-audit-skill
seo-audit is a Claude agent skill that runs comprehensive SEO, technical, content, performance, security, and AI-readiness website audits using the SEOmator CLI for developers analyzing site health.
About
seo-audit is an MIT seomator skill at version 3.0 that audits websites via the SEOmator CLI, returning LLM-optimized reports with health scores across 251 rules and 20 categories. It covers SEO, technical issues, content quality, security, JavaScript rendering, Core Web Vitals when Chrome or Chromium is available, and AI readiness. The skill requires Node.js 18+ and npm, with optional Chrome for rendering and CWV measurements, and restricts shell access to seomator commands. Developers invoke seo-audit when debugging ranking drops, pre-launch checks, or producing structured audit artifacts for remediation planning.
- Audits against 251 rules across 20 distinct categories
- Delivers LLM-optimized reports with health scores, severity levels, affected URLs and fix suggestions
- Covers core SEO, performance (Core Web Vitals), content quality, security, JavaScript rendering and AI readiness
- Supports both headless and Chrome/Chromium modes for JS rendering analysis
- Returns structured data perfect for feeding directly into Claude or Cursor agents
Seo Audit by the numbers
- 861 all-time installs (skills.sh)
- +14 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #523 of 1,879 Marketing & SEO skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/seo-skills/seo-audit-skill --skill seo-auditAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 861 |
|---|---|
| repo stars | ★ 336 |
| Security audit | 1 / 3 scanners passed |
| Last updated | July 20, 2026 |
| Repository | seo-skills/seo-audit-skill ↗ |
How do you audit a website for SEO and technical health?
Run comprehensive SEO, technical, content, performance, security, and AI-readiness audits on any website using the SEOmator CLI.
Who is it for?
Developers and SEO engineers who need a CLI-driven, machine-readable site audit spanning technical SEO, performance, security, and AI readiness before launch or during debugging.
Skip if: Teams wanting manual content strategy or keyword research without automated multi-category technical site scanning.
When should I use this skill?
The user asks to audit website SEO, debug ranking or technical issues, check site health, or run SEOmator against a URL.
What you get
SEOmator LLM-optimized audit report with category health scores across 251 rules and 20 categories.
- LLM-optimized SEO audit report
- Per-category health scores
By the numbers
- Covers 251 audit rules across 20 categories
- SEOmator skill metadata version 3.0
Files
SEO Audit Skill
Audit websites for SEO, technical, content, performance, security, JavaScript rendering, and AI readiness using the SEOmator CLI.
SEOmator provides comprehensive website auditing by analyzing website structure and content against 251 rules across 20 categories.
It provides a list of issues with severity levels, affected URLs, and actionable fix suggestions.
Links
- SEOmator npm package: npmjs.com/package/@seomator/seo-audit
- GitHub repository: github.com/seo-skills/seo-audit-skill
- Web UI: seomator.com/free-seo-audit-tool
What This Skill Does
This skill enables AI agents to audit websites for 251 rules in 20 categories, including:
- Core SEO (19 rules): Canonical URLs, indexing directives, title uniqueness, canonical conflicts/loops
- Performance (22 rules): LCP, CLS, FCP, TTFB, INP, compression, caching, minification, HTTP/2
- Links (19 rules): Broken links, redirect chains, anchor text, orphan pages, localhost/fragment links
- Images (14 rules): Alt text, dimensions, lazy loading, modern formats, alt length, background images
- Security (16 rules): HTTPS, HSTS, CSP, external link safety, leaked secrets, SSL expiry/protocol
- Technical SEO (13 rules): robots.txt, sitemap.xml, URL structure, 404 pages, soft 404s, error codes
- Crawlability (18 rules): Sitemap conflicts, indexability signals, canonical chains, pagination issues
- Structured Data (13 rules): Schema.org markup, Article, Organization, FAQ, Product, Breadcrumb
- JavaScript Rendering (13 rules): Rendered DOM checks, raw vs rendered mismatches, SSR detection
- Accessibility (12 rules): ARIA labels, color contrast, form labels, landmarks, touch targets
- Content (17 rules): Word count, readability, keyword density, duplicate detection, pixel widths
- Social (9 rules): Open Graph tags, Twitter cards, share buttons, profile links
- E-E-A-T (14 rules): Author bylines, citations, trust signals, about/contact pages, YMYL detection
- URL Structure (14 rules): Keyword slugs, stop words, uppercase, underscores, session IDs, tracking params
- Redirects (8 rules): Redirect loops, types (301/302), meta refresh, JavaScript redirects, broken redirects
- Mobile (5 rules): Font sizes, horizontal scroll, intrusive interstitials, viewport issues
- Internationalization (10 rules): lang attribute, hreflang validation (return links, conflicts, mismatches)
- HTML Validation (9 rules): Doctype, charset, head structure, lorem ipsum, multiple titles/descriptions
- AI/GEO Readiness (5 rules): Semantic HTML, AI bot access, llms.txt, schema drift
- Legal Compliance (1 rule): Cookie consent
The audit crawls the website, analyzes each page against audit rules, and returns a comprehensive report with:
- Overall health score (0-100) with letter grade (A-F)
- Category breakdowns with pass/warn/fail counts
- Specific issues with affected URLs grouped by rule
- Actionable fix recommendations
When to Use
Use this skill when you need to:
- Analyze a website's SEO health
- Debug technical SEO issues
- Check for broken links and redirect chains
- Validate meta tags, canonical URLs, and structured data
- Audit security headers, SSL, and HTTPS
- Check accessibility compliance
- Analyze JavaScript rendering and SSR compatibility
- Evaluate AI/GEO readiness (semantic HTML, llms.txt, bot access)
- Detect duplicate content across pages
- Validate hreflang and internationalization setup
- Check HTML document structure and validation
- Generate site audit reports in multiple formats
- Compare site health before/after changes
Prerequisites
This skill requires the SEOmator CLI to be installed.
Installation
npm install -g @seomator/seo-auditVerify Installation
Check that seomator is installed and the system is ready:
seomator self doctorThis checks:
- Node.js version (18+ recommended)
- npm availability
- Chrome/Chromium for Core Web Vitals and JS rendering
- Write permissions for ~/.seomator
- Local config file presence
Setup
Running seomator init creates a seomator.toml config file in the current directory.
seomator init # Interactive setup
seomator init -y # Use defaults
seomator init --preset blog # Blog-optimized config
seomator init --preset ecommerce # E-commerce config
seomator init --preset ci # Minimal CI configIf there is no seomator.toml in the directory, CREATE ONE with seomator init before running audits.
Usage
AI Agent Best Practices
YOU SHOULD always prefer `--format llm` - it provides token-optimized XML output specifically designed for AI agents (50-70% smaller than JSON).
When auditing: 1. Prefer live websites over local dev servers for accurate performance and rendering data 2. Use `--no-cwv` for faster audits when Core Web Vitals and JS rendering checks aren't needed 3. Scope fixes as concurrent tasks when implementing multiple fixes 4. Run typechecking/formatting after implementing fixes (tsc, eslint, prettier, etc.)
Website Discovery
If the user doesn't provide a website to audit: 1. Check for local dev server configurations (package.json scripts, .env files) 2. Look for Vercel/Netlify project links 3. Check environment variables for deployment URLs 4. Ask the user which URL to audit
If you have both local and live websites available, suggest auditing the live site for accurate results.
Basic Workflow
# Quick single-page audit with LLM output
seomator audit https://example.com --format llm --no-cwv
# Multi-page crawl (up to 50 pages)
seomator audit https://example.com --crawl -m 50 --format llm --no-cwv
# Full audit with Core Web Vitals + JS rendering analysis
seomator audit https://example.com --crawl -m 20 --format llmAdvanced Options
Force fresh crawl (ignore cache):
seomator audit https://example.com --refresh --format llmResume interrupted crawl:
seomator audit https://example.com --resume --format llmAudit specific categories only:
seomator audit https://example.com -c core,security,js --format llm --no-cwvSave HTML report for sharing:
seomator audit https://example.com --format html -o report.htmlVerbose output for debugging:
seomator audit https://example.com --format llm -vCommand Reference
Audit Command Options
| Option | Alias | Description | Default |
|---|---|---|---|
--format <fmt> | -f | Output format: console, json, html, markdown, llm | console |
--max-pages <n> | -m | Maximum pages to crawl | 10 |
--crawl | Enable multi-page crawl | false | |
--categories <list> | -c | Comma-separated categories to audit | All |
--refresh | -r | Ignore cache, fetch fresh | false |
--resume | Resume interrupted crawl | false | |
--no-cwv | Skip Core Web Vitals + JS rendering | false | |
--verbose | -v | Show progress | false |
--output <path> | -o | Output file path | |
--config <path> | Config file path | ||
--save | Save to ~/.seomator | false |
Other Commands
seomator init # Create config file
seomator self doctor # Check system setup
seomator config --list # Show all config values
seomator report --list # List past reports
seomator db stats # Show database statisticsOutput Formats
| Format | Flag | Best For |
|---|---|---|
| console | --format console | Human terminal output (default) |
| json | --format json | CI/CD, programmatic processing |
| html | --format html | Standalone reports, sharing |
| markdown | --format markdown | Documentation, GitHub |
| llm | --format llm | AI agents (recommended) |
The --format llm output is a compact XML format optimized for token efficiency:
- 50-70% smaller than JSON output
- Issues sorted by severity (critical first)
- Fix suggestions included for each issue
- Clean stdout for piping to AI tools
Examples
Example 1: Quick Audit with LLM Output
# User asks: "Check example.com for SEO issues"
seomator audit https://example.com --format llm --no-cwvExample 2: Deep Crawl for Large Site
# User asks: "Do a thorough audit with up to 100 pages"
seomator audit https://example.com --crawl -m 100 --format llm --no-cwvExample 3: Fresh Audit After Changes
# User asks: "Re-audit the site, ignore cached results"
seomator audit https://example.com --refresh --format llm --no-cwvExample 4: Generate Shareable Report
# User asks: "Create an HTML report I can share"
seomator audit https://example.com --crawl -m 20 --format html -o seo-report.htmlExample 5: Focus on Specific Areas
# User asks: "Just check my JavaScript rendering and redirects"
seomator audit https://example.com -c js,redirect --format llmEvaluating Results
Score Ranges
| Score | Grade | Meaning |
|---|---|---|
| 90-100 | A | Excellent - Minor optimizations only |
| 80-89 | B | Good - Address warnings |
| 70-79 | C | Needs Work - Priority fixes required |
| 50-69 | D | Poor - Multiple critical issues |
| 0-49 | F | Critical - Major problems to resolve |
Priority Order (by category weight)
Fix issues in this order for maximum impact:
1. Core (12%) - Meta tags, canonical, H1, indexing 2. Performance (12%) - Core Web Vitals + optimization 3. Links (8%) - Internal linking structure 4. Images (8%) - Performance + accessibility 5. Security (8%) - Trust signals, SSL 6. Technical SEO (7%) - Crawling foundation 7. Crawlability (5%) - Indexability, pagination 8. Structured Data (5%) - Rich snippets 9. JavaScript Rendering (5%) - Rendered DOM, SSR 10. Content (5%) - Text quality + duplicates 11. Accessibility (4%) - WCAG compliance 12. Social (3%) - Social sharing 13. E-E-A-T (3%) - Trust, expertise 14. URL Structure (3%) - URL hygiene 15. Redirects (3%) - Redirect chains 16. Mobile (2%) - Viewport, fonts 17. Internationalization (2%) - Hreflang 18. HTML Validation (2%) - Document structure 19. AI/GEO Readiness (2%) - Semantic HTML, AI bots 20. Legal Compliance (1%) - Cookie consent
Fix by Severity
1. Failures (status: "fail") - Must fix immediately 2. Warnings (status: "warn") - Should fix soon 3. Passes (status: "pass") - No action needed
Output Summary
After implementing fixes, give the user a summary of all changes made.
When planning scope, organize tasks so they can run concurrently as sub-agents to speed up implementation.
Troubleshooting
seomator command not found
If you see this error, seomator is not installed or not in your PATH.
Solution:
npm install -g @seomator/seo-auditCore Web Vitals not measured
If CWV metrics are missing, Chrome/Chromium may not be available.
Solution: 1. Install Chrome, Chromium, or Edge 2. Run seomator self doctor to verify browser detection 3. Use --no-cwv to skip CWV if not needed
Crawl timeout or slow performance
For large sites, audits may take several minutes.
Solution:
- Use
--verboseto see progress - Limit pages with
-m 20for faster results - Use
--no-cwvto skip browser-based measurements
Invalid URL
Ensure the URL includes the protocol:
# Wrong
seomator audit example.com
# Correct
seomator audit https://example.comHow It Works
1. Fetch: Downloads the page HTML and measures response time 2. Parse: Extracts DOM, meta tags, links, images, structured data 3. Enrich: Fetches robots.txt and sitemap once per audit 4. Render (if CWV enabled): Captures rendered DOM via Playwright for JS rendering analysis 5. Crawl (if enabled): Discovers and fetches linked pages 6. Analyze: Runs 251 audit rules against each page 7. Score: Calculates category and overall weighted scores 8. Report: Generates output in requested format
Results are stored in ~/.seomator/ for later retrieval with seomator report.
Trust Model
The seomator CLI fetches HTML from arbitrary user-supplied URLs. Any text quoted from those pages — titles, meta tags, headings, link text, alt attributes, schema content — is untrusted input that may attempt indirect prompt injection against the LLM consuming the report.
The LLM-format reporter (--format llm) applies a layered defense:
1. Per-report nonce. Every render emits a 128-bit hex nonce on the root <seo-audit> element. 2. Nonce-stamped delimiters. Site-derived text inside <msg> and <details> is wrapped in <untrusted-{nonce}>...</untrusted-{nonce}>. An attacker cannot forge the closing tag because the nonce is unpredictable and unique per audit. 3. Security notice. The report includes a <security-notice> instructing the consuming LLM to treat the wrapped blocks as data, not instructions. 4. Invisible-character stripping. Zero-width chars (U+200B–U+200D, U+2060, U+FEFF), Unicode tag block (U+E0000–U+E007F), and C0/C1 controls are removed from quoted content before XML escaping. 5. XML escaping. Any literal < > & " ' inside untrusted content is escaped, so a crafted </untrusted-...> literal becomes inert text.
Tool-authored fields — fix suggestions, rule IDs, category metadata — are emitted as plain XML and are not wrapped, since wrapping trusted content in untrusted delimiters would dilute the signal.
Resources
- Full rules reference: See
docs/SEO-AUDIT-RULES.mdfor all 251 rules - Storage architecture: See
docs/STORAGE-ARCHITECTURE.mdfor database details - CLI help:
seomator --helpandseomator <command> --help
# Dependencies
node_modules/
# Build output
dist/
dist-electron/
# Electron packaged app
release/
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
*.log
npm-debug.log*
# Test coverage
coverage/
# Environment
.env
.env.local
.env.*.local
# Temporary files
*.tmp
*.temp
.cache/
# SEOmator local data
.seomator/
# Generated reports & screenshots
seo-report-*.html
*.png
# Playwright MCP data
.playwright-mcp/
{
"mcpServers": {
"shadcn": {
"command": "npx",
"args": [
"shadcn@latest",
"mcp"
]
}
}
}
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Security
- LLM reporter now wraps all site-derived content (rule messages and details)
in nonce-stamped <untrusted-{nonce}>...</untrusted-{nonce}> delimiters and emits a <security-notice> instructing the consuming LLM to treat those blocks as data only. Defends against indirect prompt injection from audited pages whose content reaches the report (e.g., a hostile <title> or meta description). The 128-bit per-report nonce prevents an attacker from forging a closing tag because they cannot predict it at audit time.
- LLM reporter now strips zero-width characters (U+200B–U+200D, U+2060,
U+FEFF) and Unicode tag block characters (U+E0000–U+E007F) from quoted site content before XML escaping. The Unicode tag block is the dominant invisible-prompt-injection vector — characters render as zero pixels but carry hidden ASCII instructions LLMs will read.
- Removed
context7.jsonfrom the repository. The file contained only a
Context7 documentation-service public identifier (analogous to a Stripe publishable key — designed to ship in source) but tripped a credential scanner via pk_<base62> regex matching.
Added
src/reporters/llm-reporter.test.ts— covers the new security envelope:
nonce uniqueness, security-notice presence, untrusted-block wrapping for messages and details, fix-suggestion exemption, zero-width and Unicode-tag character stripping, and closing-tag forgery defense via XML escaping.
- "Trust Model" section in
SKILL.mddocumenting the layered defense applied
to LLM-format output.
[3.0.1] - 2026-05-06
Fixed
- Republished
dist/index.jsso the programmatic entry point
(import { createAuditor } from '@seomator/seo-audit') works on npm. The source was correct, but the 3.0.0 tarball was missing the library build artifact, causing programmatic consumers to receive an undefined export.
Added
- Integration test suite for the programmatic API (
src/auditor.test.ts).
Covers createAuditor / Auditor exports, default-vs-filtered category selection, the AuditResult shape returned by audit(), lifecycle callback ordering (onCategoryStart → onRuleComplete* → onCategoryComplete), and the single-fetch invariant. Imports through ./index.js to mirror the npm consumer entry point and catch packaging regressions like the one reported against the published 3.0.0 artifact.
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
SEOmator is a comprehensive SEO audit tool (@seomator/seo-audit) with 251 rules across 20 categories. It ships as both a CLI tool (published to npm) and an Electron desktop app (local only). It fetches web pages, parses HTML with Cheerio, optionally measures Core Web Vitals via Playwright, and scores pages against SEO best practices.
Critical Rules (read before making changes)
package.json Dual-Purpose Constraints
The package.json serves both the npm CLI package and the Electron desktop app. These fields have strict requirements:
| Field | Value | Why |
|---|---|---|
main | ./dist-electron/main/index.js | Electron reads this to find the main process entry. DO NOT change to ./dist/cli.js or Electron will execute Commander CLI instead of launching the app window. |
exports | ./dist/cli.js | npm/Node.js consumers use this for programmatic imports. Takes priority over main in modern Node.js. |
bin | ./dist/cli.js | npm CLI users use this (seomator command). |
files | ["dist"] | Only `dist/` ships to npm. This is the firewall — electron/, dist-electron/, scripts/ never reach npm users. |
If you change `main` to anything other than the Electron entry, `npm run electron:dev` will break — Electron will print Commander help text and exit instead of opening the app window.
Dependency Split: CLI vs Electron
- `dependencies`: Only CLI packages (cheerio, commander, playwright, better-sqlite3, etc.). These are what npm users install.
- `devDependencies`: Electron-only packages (react, react-dom, react-router-dom, recharts, zustand, electron, electron-vite, tailwindcss, etc.).
- Never move react/zustand/recharts/electron packages into `dependencies` — npm users would download ~15MB of unused Electron UI code.
npm Publishing Checklist
1. Verify dependencies contains only CLI packages (no react, zustand, recharts, electron) 2. Verify files: ["dist"] — only CLI build ships 3. npm run build → builds CLI via tsup 4. npm pack --dry-run → confirm only dist/ files + README + package.json 5. npm publish --access public 6. The prepublishOnly script auto-runs npm run build before publish
Published as @seomator/seo-audit on npm. Current version: 3.0.0.
better-sqlite3 Native Module ABI
better-sqlite3 is a C++ addon compiled against a specific Node.js ABI. Electron and CLI/Node.js use different ABIs:
npx electron-rebuild -f -w better-sqlite3 # Before running Electron
npm rebuild better-sqlite3 # Before running CLI testsYou must recompile when switching between Electron and CLI. Failure produces a cryptic NODE_MODULE_VERSION mismatch error.
Zero Modifications to src/
The Electron app is purely additive — all Electron code lives in electron/. The src/ directory is shared by both CLI and Electron through direct imports (via the @core alias in electron-vite config). Never put Electron-specific code in src/.
Build & Development Commands
CLI
npm run build # Build with tsup (ESM, single entry: src/cli.ts)
npm run dev # Build in watch mode
npm run test:run # Run all tests once (vitest)
npm test # Run tests in watch modeRun locally after building:
./dist/cli.js audit https://example.com --no-cwvRun a single test file:
npx vitest run src/rules/core/core.test.tsElectron Desktop App
npx electron-rebuild -f -w better-sqlite3 # Required before first run
npm run electron:dev # Dev mode with Vite HMR + Electron hot reload
npm run electron:build # Production build (main + preload + renderer)
npm run electron:pack # Build + package into distributableArchitecture
Rule System (the core abstraction)
The entire audit engine is built on a self-registering rule pattern:
1. `defineRule()` (src/rules/define-rule.ts) - Creates and validates an AuditRule object with id, name, description, category, weight, and run(context) function.
2. `registerRule()` (src/rules/registry.ts) - Stores rules in a global Map<string, AuditRule>. Throws on duplicate IDs.
3. Category `index.ts` files (e.g., src/rules/core/index.ts) - Import individual rule files and call registerRule() for each. This is the registration point.
4. `src/rules/loader.ts` - Static-imports all 20 category index.ts files. The act of importing triggers side-effect registration. loadAllRules() exists for API compat but rules load at import time.
5. Result helpers: pass(ruleId, message, details?), warn(...), fail(...) return RuleResult with scores 100/50/0 respectively.
Adding a New Rule
1. Create src/rules/<category>/<rule-name>.ts exporting a const created via defineRule() 2. Rule ID convention: <category>-<descriptive-name> (e.g., core-canonical-conflicting) 3. Import and registerRule() in src/rules/<category>/index.ts 4. The run function receives AuditContext and returns RuleResult (or Promise)
AuditContext
Defined in src/types.ts. Every rule receives the same context object containing:
- Always available:
url,html,$(CheerioAPI),headers,statusCode,responseTime,cwv,links,images,invalidLinks,specialLinks,figures,inlineSvgs,pictureElements - Tier 2 (network-fetched, optional):
robotsTxtContent,sitemapContent,sitemapUrls,redirectChain - Tier 4 (Playwright, optional):
renderedHtml,rendered$(CheerioAPI of rendered DOM)
Scoring Model
- Rule level:
pass=100,warn=50,fail=0 (src/scoring.ts) - Category score: Weighted average of rule scores within the category
- Overall score: Weighted average of category scores using category weights
- Category weights must sum to exactly 100 (validated by
validateCategoryWeights()insrc/categories/index.ts)
20 Categories & Weights
core(12%), perf(12%), links(8%), images(8%), security(8%), technical(7%), crawl(5%), schema(5%), content(5%), js(5%), a11y(4%), social(3%), eeat(3%), url(3%), redirect(3%), mobile(2%), i18n(2%), htmlval(2%), geo(2%), legal(1%)
Audit Flow
Auditor class (src/auditor.ts) orchestrates: 1. loadAllRules() → triggers static imports 2. fetchPage() → HTTP fetch + Cheerio parse → AuditContext 3. (Optional) fetchPageWithPlaywright() → CWV metrics + rendered DOM 4. enrichContext() → fetches robots.txt + sitemap once per audit 5. runAllCategories() → iterates categories → getRulesByCategory() → runs each rule 6. buildAuditResult() → weighted scoring
Key Directories
src/rules/- 251 audit rules in 20 category subdirectoriessrc/categories/- Category definitions with weightssrc/commands/- CLI command handlers (audit, crawl, init, config, db, etc.)src/crawler/- HTTP fetcher, queue-based crawler, URL normalizationsrc/reporters/- Output formatters (console, json, html, markdown, llm)src/storage/- SQLite persistence (project-db, audits-db, link-cache)src/config/- TOML config loading, validation, presetselectron/- Electron desktop app (does NOT modifysrc/)electron/main/- Main process: BrowserWindow, IPC handlers, audit/db bridgeselectron/preload/- contextBridge exposing typedelectronAPIelectron/renderer/- React UI: pages, components, hooks, storeselectron/shared/- IPC type definitions shared between main and renderer
Electron Desktop App Architecture
The desktop app wraps the existing audit engine without modifying src/:
- IPC Bridge pattern:
audit-bridge.tswrapsAuditorcallbacks →webContents.send()for streaming progress.db-bridge.tswrapsAuditsDatabase→ipcMain.handle()for queries. - Preload security:
contextIsolation: true,nodeIntegration: false. The preload script exposes a typedElectronAPIviacontextBridge.exposeInMainWorld(). - State management: Zustand store (
audit-store.ts) with state machine:idle → running → complete | error. IPC events drive state transitions. - Rule metadata: The rule registry only exists in the main process. After an audit completes,
audit-bridge.tsbuilds aruleId → { name, description }map viagetRuleById()and sends it alongside theAuditResultso the renderer can display rule names and descriptions. - Design tokens: CSS variables in
globals.cssextracted fromsrc/reporters/html-reporter.tsfor visual consistency. Light/dark theme support via[data-theme="dark"]. - Build: electron-vite handles the triple build (main → CJS, preload → ESM, renderer → Vite/React). Config:
electron/electron-vite.config.ts. - Path aliases:
@core→../src(access CLI engine from Electron main),@renderer→./renderer. - CSS: Tailwind v4 uses
@theme {}block for custom properties.@layer baseis critical — unlayered CSS overrides Tailwind v4 utilities. - `getAPI()` returns
nulloutside Electron — all renderer hooks must null-guard.
Testing Conventions
- Test files:
src/rules/<category>/<category>.test.ts - Tests create a minimal
AuditContextusingcheerio.load(html)for$, with stub values for other fields - Use
null as anyfor context fields not relevant to the rule under test - Import rules directly from their individual files, not via the category index
- Run
npm rebuild better-sqlite3before running CLI tests if you were previously running Electron
Tech Stack
CLI (src/)
- TypeScript (ES2022 target, ESM modules, bundler resolution)
- tsup for building (single ESM entry,
#!/usr/bin/env nodebanner) - vitest for testing
- Cheerio for HTML parsing
- Playwright for CWV measurement and rendered DOM capture
- better-sqlite3 for storage
- Commander for CLI parsing
- chalk/ora/cli-table3/log-update for terminal UI
Desktop App (electron/)
- Electron + electron-vite (triple build: main/preload/renderer)
- React with Tailwind CSS v4 (uses
@theme {}block for custom properties) - Zustand for state management
- Recharts for score trend charts
- electron-builder for packaging
AI Agent Integration
Use SEOmator with Claude Code, Cursor, and other AI coding assistants
SEOmator is built for autonomous AI workflows. This guide shows you how to integrate SEOmator with AI coding agents to audit websites and implement fixes automatically.
Three Ways to Use SEOmator
| Method | Description |
|---|---|
| CLI for Humans | Run audits directly from your terminal with human-readable output |
| Pipe to Agent | Pipe audit reports to Claude or other AI assistants using --format llm |
| Skill Integration | Install the SEOmator skill so agents can run audits autonomously |
Install the Skill
The SEOmator skill enables AI agents to run audits, analyze results, and implement fixes without manual intervention.
Installation
npx skills add seo-skills/seo-audit-skillThis installs the seo-audit skill for:
- Claude Code - Desktop and CLI
- Cursor - AI-first code editor
- Any agent supporting Claude Code skills
The skill is a thin wrapper that calls the SEOmator CLI. Install both the CLI and skill for full functionality.
For best results, use the skill in plan mode if your agent supports it. This lets the agent analyze all issues and create a comprehensive implementation plan before making changes to your codebase.
Verify Installation
After installing, verify the skill is available:
npx skills listYou should see seo-audit in the output.
Using with Claude Code
Basic Audit Workflow
The easiest way to run an audit is with a slash command:
/seo-auditThis triggers the skill directly. Claude will detect your project's website (from config, environment, or code) and run an audit.
You can also specify a URL explicitly:
/seo-audit https://example.comOr use natural language:
Use the seo-audit skill to audit example.comClaude will:
1. Run the audit using SEOmator CLI 2. Parse the results 3. Summarize issues by severity 4. Suggest next steps
Example Prompts
Audit and summarize issues:
/seo-audit example.com and summarize the top 5 most critical issuesAudit with specific focus:
/seo-audit mysite.com focusing on accessibility and performance issuesAudit and fix all issues:
/seo-audit this site and fix all errors and warningsAudit local development site:
/seo-audit http://localhost:3000 and create a prioritized fix listPlan Mode for Comprehensive Fixes
For larger fix efforts, use Claude's plan mode to create an implementation strategy:
Step 1: Trigger plan mode
Ask Claude to enter plan mode before starting work:
Enter plan mode. Use the seo-audit skill to audit example.com,
then create a comprehensive plan to fix all high and medium severity issues.Step 2: Review the plan
Claude will:
- Run the audit
- Analyze all issues
- Group fixes by category
- Create an ordered implementation plan
- Identify dependencies between fixes
Review and approve the plan.
Step 3: Execute the plan
Once approved, Claude will implement fixes systematically, checking off completed items.
Using Subagents for Parallel Fixes
For complex sites with many issues, prompt Claude to use subagents:
Use the seo-audit skill to audit example.com.
Then spawn subagents to fix issues in parallel:
- Subagent 1: Fix all accessibility issues
- Subagent 2: Fix all SEO meta tag issues
- Subagent 3: Fix all performance issuesThis parallelizes work across independent issue categories.
Piping to Claude (Alternative Method)
If you prefer not to use skills, pipe audit output directly to Claude:
Using Report Formats
Pipe audit results directly to Claude in LLM-optimized format:
seomator audit https://example.com --format llm | claudeOr run the audit and export later:
# Run audit (stores results)
seomator audit https://example.com --save
# Export and pipe to Claude
seomator report --latest --format llm | claudeLLM Format Benefits:
- Compact structured output (50-70% smaller than JSON)
- Token-optimized for API costs and context limits
- Includes actionable fix suggestions
- Works with any LLM (Claude, GPT, etc.)
Example Workflows
# Audit and ask Claude to prioritize fixes
seomator audit https://example.com --format llm | claude "Prioritize these issues and create a fix plan"
# Audit and implement high-severity fixes
seomator audit https://example.com --format llm | claude "Fix all high-severity issues"
# Audit and explain issues to non-technical stakeholder
seomator audit https://example.com --format markdown | claude "Explain these issues in simple terms"Output Formats for AI
| Format | Flag | Best For |
|---|---|---|
llm | --format llm | Compact XML for AI agents (50-70% smaller, token-optimized) |
json | --format json | Custom AI processing scripts |
markdown | --format markdown | AI agents that prefer markdown |
console | (default) | Human-readable terminal output |
Using with Other AI Coding Assistants
Cursor
Cursor supports Claude Code skills natively:
1. Install the skill:
npx skills add seo-skills/seo-audit-skill2. Run with slash command:
/seo-audit3. Or use composer mode for multi-file fixes:
/seo-audit then fix all issues across the codebaseWindsurf / Aider / Other Agents
For agents without skill support, use piping:
# Windsurf (Cascade)
seomator audit https://example.com --format llm | windsurf
# Aider
seomator audit https://example.com --format llm | aider
# Generic LLM API - save to file then send
seomator audit https://example.com --format llm > audit.xml
# Then send audit.xml content to your LLMAdvanced Agent Patterns
Pre-Deploy Audits
In your deployment workflow:
Before I deploy, use seo-audit skill to audit
http://localhost:3000 and ensure there are no high-severity
issues introduced since the last deployment.Automated Regression Detection
After making changes:
I just updated the homepage. Use seo-audit skill to audit
the site and verify I didn't introduce any SEO or accessibility
regressions.Continuous Monitoring
Set up regular audits:
Every week, use seo-audit skill to audit production and compare
against last week's scores. Alert me if any category drops more
than 5 points.Configuration for Agents
Project-Scoped Config
Create seomator.toml in your project so agents use consistent settings:
[project]
name = "my-website"
domains = ["example.com"]
[crawler]
max_pages = 50
respect_robots = true
exclude = ["/admin/**", "/api/**"]
[rules]
disable = ["perf-inp"]
[output]
format = "llm"Now when agents run audits, they'll use these settings automatically.
Limiting Crawl Scope
For large sites, configure agents to audit specific sections:
[crawler]
max_pages = 20
include = ["/blog/**"]
exclude = ["/admin/**", "/api/**"]Skill vs Piping: Which to Use?
Use the skill when:
- Working in Claude Code, Cursor, or skill-compatible editors
- You want agents to discover and use SEOmator autonomously
- Building multi-step workflows where the agent decides when to audit
- The agent needs to run audits as part of a larger task
Use piping when:
- Working with agents that don't support skills
- You want explicit control over when audits run
- Integrating into shell scripts or automation
- Using SEOmator with non-coding LLMs
Troubleshooting
Skill not found
Verify installation:
npx skills list | grep seo-auditReinstall if missing:
npx skills add seo-skills/seo-audit-skill --forceAgent can't run audits
Ensure SEOmator CLI is installed:
seomator --versionIf not installed:
npm install -g @seomator/seo-auditCore Web Vitals failing
SEOmator uses your system browser for Core Web Vitals. Check browser availability:
seomator self doctorOr skip CWV for faster audits:
seomator audit https://example.com --no-cwvPiping produces no output
Check the output format:
seomator audit https://example.com --format llmEnsure you're using --format llm for LLM-optimized output.
Next Steps
- Quickstart - Installation and first audit
- Introduction - Overview and features
- GitHub - Source code and issues
Configuration
Customize SEOmator behavior with config files and CLI options
SEOmator uses a layered configuration system. Settings can come from config files, CLI arguments, or built-in defaults.
Quick Start
Create a config file in your project:
seomator init # Interactive setup
seomator init -y # Use defaults
seomator init --preset blog # Use blog presetThis creates seomator.toml in your current directory.
Configuration File
Full Example
[project]
name = "my-website"
domains = ["example.com", "www.example.com"]
[crawler]
max_pages = 100
concurrency = 3
timeout_ms = 30000
respect_robots = true
delay_ms = 100
user_agent = "" # Empty = random browser UA per crawl
# URL filtering (glob patterns)
include = [] # Empty = crawl all
exclude = ["/admin/**", "/api/**", "/wp-json/**"]
# Query param handling
drop_query_prefixes = ["utm_", "gclid", "fbclid", "ref"]
allow_query_params = [] # Empty = keep all except dropped
# Crawl distribution
max_prefix_budget = 0.25 # Prevent over-crawling single paths (0-1)
[rules]
enable = ["*"] # Enable all rules by default
disable = ["perf-inp"] # Disable specific rules (supports wildcards)
[external_links]
enabled = true
cache_ttl_days = 7
timeout_ms = 10000
concurrency = 5
[output]
format = "console" # console, json, html, markdown, llm
path = "" # Output file path (optional)Configuration Sections
[project]
Project identification and domain configuration.
| Key | Type | Default | Description |
|---|---|---|---|
name | string | - | Project name for reports |
domains | string[] | - | Allowed domains (crawl stays within these) |
[crawler]
Controls how SEOmator crawls websites.
| Key | Type | Default | Description |
|---|---|---|---|
max_pages | number | 10 | Maximum pages to crawl |
concurrency | number | 3 | Concurrent requests |
timeout_ms | number | 30000 | Request timeout in milliseconds |
respect_robots | boolean | true | Honor robots.txt directives |
delay_ms | number | 100 | Delay between requests |
user_agent | string | "" | Custom user agent (empty = random browser UA) |
include | string[] | [] | URL patterns to include (glob) |
exclude | string[] | [] | URL patterns to exclude (glob) |
drop_query_prefixes | string[] | ["utm_", "gclid", "fbclid"] | Query params to strip |
allow_query_params | string[] | [] | Query params to keep (empty = all except dropped) |
max_prefix_budget | number | 0.25 | Max fraction of crawl for single path prefix |
[rules]
Enable or disable specific audit rules.
| Key | Type | Default | Description |
|---|---|---|---|
enable | string[] | ["*"] | Rules to enable (supports wildcards) |
disable | string[] | [] | Rules to disable (supports wildcards) |
Wildcard Examples:
[rules]
enable = ["*"] # Enable all rules
disable = [
"perf-*", # Disable all performance rules
"a11y-color-contrast", # Disable specific rule
"content-word-count",
][external_links]
External link checking configuration.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Check external links |
cache_ttl_days | number | 7 | Days to cache link check results |
timeout_ms | number | 10000 | External link timeout |
concurrency | number | 5 | Concurrent external link checks |
[output]
Default output configuration.
| Key | Type | Default | Description |
|---|---|---|---|
format | string | "console" | Output format: console, json, html, markdown, llm |
path | string | "" | Default output file path |
Config Priority
Configuration is merged from multiple sources (highest to lowest priority):
1. CLI arguments (--max-pages 50) 2. Local config (./seomator.toml) 3. Parent directory configs (searches up the tree) 4. Global config (~/.seomator/config.toml) 5. Built-in defaults
Presets
SEOmator includes presets for common use cases:
seomator init --preset blog # Content sites
seomator init --preset ecommerce # E-commerce sites
seomator init --preset ci # Minimal CI/CD config| Preset | Description |
|---|---|
default | Standard configuration |
blog | Optimized for content sites - focuses on content, E-E-A-T |
ecommerce | Optimized for e-commerce - focuses on structured data, performance |
ci | Minimal config for CI/CD - fast, essential rules only |
Config Commands
View configuration
seomator config --list # Show all config values
seomator config show # Show merged config with sources
seomator config path # Show config file pathsGet/set values
seomator config crawler.max_pages # Get value
seomator config crawler.max_pages 50 # Set value
seomator config --global # Modify global configValidate configuration
seomator config validate # Check for errors/warningsCLI Options
CLI options override config file settings:
| Option | Config Equivalent | Description |
|---|---|---|
-m, --max-pages <n> | crawler.max_pages | Max pages to crawl |
--concurrency <n> | crawler.concurrency | Concurrent requests |
--timeout <ms> | crawler.timeout_ms | Request timeout |
-c, --categories <list> | - | Filter to specific categories |
-f, --format <type> | output.format | Output format |
-o, --output <path> | output.path | Output file path |
--no-cwv | - | Skip Core Web Vitals |
-r, --refresh | - | Ignore cache |
--resume | - | Resume interrupted crawl |
URL Filtering
Include patterns
Only crawl URLs matching these patterns:
[crawler]
include = [
"/blog/**", # All blog pages
"/products/**", # All product pages
]Exclude patterns
Skip URLs matching these patterns:
[crawler]
exclude = [
"/admin/**", # Admin pages
"/api/**", # API endpoints
"/wp-json/**", # WordPress REST API
"/**?*", # URLs with query strings
"/tag/**", # Tag archives
"/author/**", # Author archives
]Glob pattern syntax
| Pattern | Matches |
|---|---|
* | Single path segment |
** | Multiple path segments |
? | Single character |
[abc] | Character class |
Query Parameter Handling
Drop tracking parameters
Remove common tracking parameters:
[crawler]
drop_query_prefixes = [
"utm_", # Google Analytics
"gclid", # Google Ads
"fbclid", # Facebook
"ref", # Referral tracking
"source",
"medium",
"campaign",
]Allow specific parameters
Keep only specific query parameters:
[crawler]
allow_query_params = [
"page", # Pagination
"sort", # Sorting
"category", # Filtering
]Environment-Specific Config
Development
[project]
name = "my-site-dev"
[crawler]
max_pages = 10
timeout_ms = 60000 # Longer timeout for slow dev server
[rules]
disable = ["security-*"] # Skip security rules in devCI/CD
[project]
name = "my-site-ci"
[crawler]
max_pages = 50
concurrency = 5
[output]
format = "json"
[rules]
disable = ["perf-inp", "perf-cls"] # Skip flaky CWV rules in CIProduction
[project]
name = "my-site"
[crawler]
max_pages = 500
respect_robots = true
[external_links]
enabled = true
cache_ttl_days = 1 # More frequent link checksStorage Locations
SEOmator stores data in these locations:
~/.seomator/ # Global directory
├── projects/ # Per-domain project databases
│ └── example.com/
│ └── project.db # Crawls, pages, links, images
├── audits.db # Centralized audit results
├── link-cache.db # External link check cache
└── config.toml # Global configurationSee Storage Architecture for details.
Next Steps
- Quickstart - Run your first audit
- AI Agent Integration - Use with Claude Code
- Rules Reference - All 148 rules explained
SEOmator Desktop App — Architecture Guide
The Electron desktop app wraps the existing Node.js audit engine in a native macOS/Windows application with a React UI. Zero changes were made to `src/` — the entire Electron layer is additive, living in the electron/ directory.
Table of Contents
- How It Works
- Directory Structure
- The Three-Process Model
- IPC Bridge Architecture
- State Management
- Design System
- Build System
- Distribution & Packaging
- Development Workflow
- Key Design Decisions
---
How It Works
The desktop app reuses the CLI's Auditor class directly — no HTTP APIs, no child processes, no re-implementation. The connection works through Electron's IPC (Inter-Process Communication):
┌───────────────────────────────────────────────────────────────┐
│ Renderer Process (React UI) │
│ │
│ useAudit() hook ──► window.electronAPI.runAudit(url) │
│ │ │
│ │ ipcRenderer.send('audit:run') │
└─────────────────────────┼─────────────────────────────────────┘
│
┌───────────┼───────────┐
│ Preload Script │
│ contextBridge │
└───────────┼───────────┘
│
┌─────────────────────────┼─────────────────────────────────────┐
│ Main Process (Node.js) │ │
│ ▼ │
│ audit-bridge.ts ──► new Auditor({ │
│ onCategoryStart: (id, name) => { │
│ win.webContents.send( │
│ 'audit:progress:category-start', │
│ { categoryId: id, ... } │
│ ); │
│ } │
│ }) │
│ │
│ Auditor class (from src/auditor.ts) │
│ └── fetchPage() → Cheerio parse → runAllCategories() │
│ └── 251 rules across 20 categories │
│ │
│ db-bridge.ts ──► AuditsDatabase (from src/storage/) │
│ └── SQLite queries: listAudits, getScoreTrend, etc. │
└───────────────────────────────────────────────────────────────┘The key insight: The CLI's Auditor class already exposes a callback interface (onCategoryStart, onCategoryComplete, onRuleComplete, onPageComplete). The audit bridge simply wires those callbacks to BrowserWindow.webContents.send() calls, turning the CLI's synchronous callbacks into IPC event streams.
---
Directory Structure
electron/
├── electron-vite.config.ts # Triple-target Vite build (main + preload + renderer)
├── tsconfig.json # TypeScript config with @core/* and @renderer/* aliases
│
├── main/ # Main process (Node.js context — full system access)
│ ├── index.ts # BrowserWindow creation, IPC handler registration
│ ├── audit-bridge.ts # Wraps Auditor class → IPC events
│ └── db-bridge.ts # Wraps AuditsDatabase → IPC invoke/handle
│
├── preload/ # Preload script (security boundary)
│ └── index.ts # contextBridge — exposes typed electronAPI
│
├── renderer/ # Renderer process (browser context — React app)
│ ├── index.html # HTML shell
│ ├── main.tsx # React entry point
│ ├── App.tsx # Root component — header + page routing
│ ├── components/ # 15 React components
│ │ ├── Header.tsx # App header with navigation tabs
│ │ ├── AuditRunner.tsx # URL input + run/cancel controls
│ │ ├── ProgressStream.tsx # Live category-by-category progress
│ │ ├── ScoreCircle.tsx # Animated circular score gauge
│ │ ├── ScoreStats.tsx # Pass/warn/fail summary cards
│ │ ├── CategoryGrid.tsx # Grid of category score cards
│ │ ├── CategorySection.tsx # Expandable category with rules
│ │ ├── CategoryBar.tsx # Horizontal score bar
│ │ ├── RuleCard.tsx # Individual rule result display
│ │ ├── IssuesTable.tsx # Filterable table of all issues
│ │ ├── FilterTabs.tsx # Pass/warn/fail tab filters
│ │ ├── Sidebar.tsx # Navigation sidebar
│ │ ├── AuditList.tsx # History list of past audits
│ │ ├── DomainPicker.tsx # Domain selector for history
│ │ └── ScoreTrend.tsx # Recharts line chart of score over time
│ ├── pages/
│ │ ├── AuditPage.tsx # Main audit view (runner + results)
│ │ └── HistoryPage.tsx # Audit history with trend charts
│ ├── hooks/
│ │ ├── useAudit.ts # Audit lifecycle via IPC (subscribe/run/cancel)
│ │ ├── useAuditHistory.ts # Database queries via IPC
│ │ └── useTheme.ts # Light/dark theme with localStorage
│ ├── stores/
│ │ └── audit-store.ts # Zustand — audit state machine
│ ├── lib/
│ │ ├── ipc-client.ts # Typed accessor for window.electronAPI
│ │ ├── format.ts # Score color/label helpers
│ │ └── fix-suggestions.ts # Rule fix suggestion registry
│ └── styles/
│ ├── tailwind.css # Tailwind v4 entry
│ └── globals.css # Design tokens (light + dark themes)
│
├── shared/ # Shared between main and renderer
│ └── ipc-types.ts # IPC channel names + payload types
│
└── resources/ # Build assets
└── icon.png # App icon (1024x1024, auto-converts to .icns/.ico)---
The Three-Process Model
Electron applications run three isolated processes. This is fundamental to understanding the architecture:
1. Main Process (electron/main/)
- Runs Node.js — full access to file system, native modules,
src/code. - Creates the
BrowserWindowand manages the app lifecycle. - Hosts the two IPC bridges:
- `audit-bridge.ts` — instantiates the
Auditorclass fromsrc/auditor.ts, streams progress events to the renderer. - `db-bridge.ts` — wraps
AuditsDatabasefromsrc/storage/audits-db/, handles SQLite queries. - Configured in
electron/main/index.ts: - Window: 1280x820, macOS hidden inset title bar, traffic lights at (16, 16).
sandbox: falseis required becausebetter-sqlite3is a native C++ addon.- Dev mode loads from Vite dev server (
ELECTRON_RENDERER_URL); production loadsrenderer/index.html.
2. Preload Script (electron/preload/)
- Security boundary between main and renderer processes.
- Uses Electron's
contextBridge.exposeInMainWorld()to createwindow.electronAPI. - Provides a fully typed interface (
ElectronAPI) — the renderer never touchesipcRendererdirectly. - The preload script is the only code that can access both Node.js APIs and the DOM.
3. Renderer Process (electron/renderer/)
- Standard browser context — runs the React app. No Node.js access.
- Communicates exclusively through
window.electronAPI(exposed by preload). - Built with Vite + React + Tailwind CSS v4.
---
IPC Bridge Architecture
IPC channels are defined in electron/shared/ipc-types.ts and follow a namespace:action naming convention.
Audit Channels (streaming events)
These use the send/on pattern — fire-and-forget messages that stream in real time.
| Direction | Channel | Payload | Purpose |
|---|---|---|---|
| Renderer → Main | audit:run | AuditRunArgs | Start an audit |
| Renderer → Main | audit:cancel | — | Abort running audit |
| Main → Renderer | audit:progress:category-start | { categoryId, categoryName } | Category began |
| Main → Renderer | audit:progress:category-complete | { categoryId, categoryName, result } | Category finished |
| Main → Renderer | audit:progress:rule-complete | { ruleId, ruleName, result } | Single rule finished |
| Main → Renderer | audit:progress:page-complete | { url, pageNumber, totalPages } | Crawled page done |
| Main → Renderer | audit:complete | AuditCompletePayload | Entire audit done |
| Main → Renderer | audit:error | string | Error message |
The AuditCompletePayload includes both the full AuditResult and a ruleMetadata map (ruleId → { name, description }) looked up from the rule registry.
Database Channels (request/response)
These use the invoke/handle pattern — async request-response, like an RPC call.
| Channel | Args | Returns | Purpose |
|---|---|---|---|
db:list-audits | { domain?, limit?, offset? } | AuditSummaryIpc[] | List past audits |
db:get-score-trend | { domain, limit? } | ScoreTrendPoint[] | Score history for charts |
db:get-audited-domains | — | string[] | Unique domains audited |
How the Audit Bridge Connects to src/
The bridge works because Auditor already has a callback-based interface:
// In audit-bridge.ts — this is the entire connection
currentAuditor = new Auditor({
measureCwv: args.options.measureCwv ?? false,
onCategoryStart: (categoryId, categoryName) => {
win.webContents.send('audit:progress:category-start', { categoryId, categoryName });
},
onCategoryComplete: (categoryId, categoryName, result) => {
win.webContents.send('audit:progress:category-complete', { categoryId, categoryName, result });
},
// ... same pattern for onRuleComplete, onPageComplete
});
// Single vs multi-page audit
if (args.options.crawl) {
result = await currentAuditor.auditWithCrawl(args.url, maxPages, concurrency);
} else {
result = await currentAuditor.audit(args.url);
}No adapters, no serialization layers — the existing Auditor callbacks map 1:1 to IPC events.
---
State Management
Zustand Store (audit-store.ts)
The audit runs as a state machine:
idle ──[startAudit]──► running ──[setComplete]──► complete
│
└──[setError]──► errorState shape:
status:'idle' | 'running' | 'complete' | 'error'url: Target URL being auditedprogress: Live streaming data (completed categories, current category, rule count)result: FullAuditResult(same TypeScript type fromsrc/types.ts)ruleMetadata: Rule names and descriptions from the registryerror: Error message string
Data Flow
User clicks "Run"
└─► useAudit().run(url)
├─► store.startAudit(url) // status = 'running'
└─► electronAPI.runAudit({ url }) // IPC to main process
└─► Auditor runs...
├─► onCategoryStart ──► store.setCategoryStart()
├─► onRuleComplete ──► store.addRuleComplete()
├─► onCategoryComplete ──► store.setCategoryComplete()
└─► audit:complete ──► store.setComplete(result) // status = 'complete'Hooks
| Hook | Purpose |
|---|---|
useAudit() | Subscribes to IPC events, updates Zustand store, returns { run, cancel, status, result, progress } |
useAuditHistory() | Fetches audit history + score trends from SQLite via IPC |
useTheme() | Light/dark toggle persisted in localStorage |
---
Design System
The desktop app shares the same visual language as the CLI's HTML report output.
Design Tokens (globals.css)
Extracted from src/reporters/html-reporter.ts into CSS custom properties:
- Colors:
--color-pass(#10b981),--color-warn(#f59e0b),--color-fail(#ef4444) - Backgrounds:
--color-bg,--color-bg-elevated,--color-bg-hover - Typography: IBM Plex Sans (body), IBM Plex Mono (code)
- Layout:
--header-height: 64px,--sidebar-width: 260px - Dark mode:
[data-theme='dark']overrides all token values
Tailwind CSS v4
Uses the new Tailwind v4 syntax with @theme {} blocks for custom properties. The @tailwindcss/vite plugin handles the build integration.
Electron-Specific CSS
.drag-region { -webkit-app-region: drag; } /* Header is draggable */
.no-drag { -webkit-app-region: no-drag; } /* Buttons inside header are clickable */The macOS window uses titleBarStyle: 'hiddenInset' with traffic lights positioned at (16, 16), which gives the native frameless look while keeping window controls.
---
Build System
The app uses electron-vite, which runs three parallel Vite builds:
electron-vite build
├── main/index.ts → dist-electron/main/index.js (Node.js, ESM)
├── preload/index.ts → dist-electron/preload/index.mjs (Node.js, ESM)
└── renderer/index.html → dist-electron/renderer/ (Browser, React+CSS)Configuration in electron/electron-vite.config.ts:
- Main: Uses
externalizeDepsPlugin()— allnode_modulesare left as externalrequire()calls, not bundled. This is critical for native modules likebetter-sqlite3. - Preload: Same externalization strategy.
- Renderer: Standard Vite build with React and Tailwind plugins.
- Path aliases:
@core→../src,@renderer→./renderer
---
Distribution & Packaging
Configuration
Packaging is handled by electron-builder (v26.7.0), configured in electron-builder.yml:
| Setting | Value |
|---|---|
appId | com.seomator.desktop |
productName | SEOmator |
buildResources | electron/resources/ |
output | release/ |
asar | true (with .node files unpacked) |
Platform Targets
| Platform | Format | Notes |
|---|---|---|
| macOS | .dmg + .zip | Universal binary (Intel + Apple Silicon) |
| Windows | .exe (NSIS) | User-selectable install directory |
| Linux | .AppImage | Portable, no install needed |
Scripts
npm run electron:icon # Generate icon.png from scripts/generate-icon.mjs
npm run electron:pack # Quick test build (unpacked .app directory)
npm run electron:dist # Full distributable for current platform
npm run electron:dist:mac # macOS .dmg + .zip
npm run electron:dist:win # Windows .exe installerIcon Pipeline
1. scripts/generate-icon.mjs renders an HTML-designed icon using Playwright → electron/resources/icon.png (1024x1024) 2. electron-builder auto-converts the PNG to .icns (macOS) and .ico (Windows) at build time 3. To use a custom icon, replace electron/resources/icon.png with any 1024x1024+ PNG
Native Module Handling
better-sqlite3 is a C++ addon that must be compiled for the target Electron version:
1. electron-builder install-app-deps rebuilds native modules against Electron's Node.js headers 2. asarUnpack: ["**/*.node"] extracts compiled .node files from the asar archive (they can't be loaded from within an asar)
Code Signing (Production)
For distribution outside your team:
- macOS: Requires an Apple Developer ID certificate + notarization. Add
CSC_LINKandCSC_KEY_PASSWORDenv vars, plusnotarizeconfig inelectron-builder.yml. - Windows: Requires an Authenticode code signing certificate. Add
CSC_LINKandCSC_KEY_PASSWORDenv vars. - Without signing: Users see "unidentified developer" warnings. On macOS, users can bypass via right-click → Open.
---
Development Workflow
# Start dev mode (Vite HMR + Electron hot reload)
npm run electron:dev
# In dev mode:
# - Renderer loads from Vite dev server (localhost:5173)
# - Main process restarts on file changes
# - DevTools open automaticallyAdding New IPC Channels
1. Define the channel name and payload types in electron/shared/ipc-types.ts 2. Add the handler in the relevant bridge (audit-bridge.ts or db-bridge.ts) 3. Expose it through the preload script (electron/preload/index.ts) 4. Call it from the renderer via getAPI() from electron/renderer/lib/ipc-client.ts
Adding New Pages/Components
1. Create the component in electron/renderer/components/ 2. If it needs IPC data, create a hook in electron/renderer/hooks/ 3. If it needs shared state, extend the Zustand store in electron/renderer/stores/audit-store.ts 4. Add routing in electron/renderer/App.tsx
---
Key Design Decisions
Why No Changes to src/?
The Electron app is purely additive. The CLI and desktop app share the same Auditor class, rule registry, and scoring engine through direct imports (via the @core alias). This means:
- Bug fixes to rules automatically apply to both CLI and desktop.
- No sync issues between two codebases.
- The CLI remains independently publishable to npm.
Why send/on Instead of invoke/handle for Audits?
Audits are long-running operations (5-30 seconds) with streaming progress updates. The invoke/handle pattern returns a single promise — it can't stream intermediate results. Using send/on lets us push 20+ category progress events to the renderer in real time, showing the user a live progress stream instead of a spinner.
Database queries use invoke/handle because they're simple request-response operations (query → rows).
Why contextBridge Instead of nodeIntegration: true?
Security. With nodeIntegration: true, any XSS vulnerability in the renderer gives an attacker full Node.js access (file system, network, child processes). The contextBridge pattern ensures the renderer can only call the specific functions we expose — nothing more.
Why sandbox: false?
better-sqlite3 is a native C++ addon that uses Node.js N-API. Electron's sandbox restricts native module loading. Since the preload script needs to bridge to the main process (which loads better-sqlite3), the sandbox is disabled. The contextBridge still provides the security boundary.
Why Zustand Over Redux/Context?
Zustand is 1KB, has zero boilerplate, and works perfectly for the audit state machine pattern. The store is a single flat object with actions — no reducers, no action types, no providers. It also supports subscriptions outside React (useful for future IPC patterns).
Why Design Tokens from HTML Reporter?
The HTML report (src/reporters/html-reporter.ts) established the visual language — score colors, typography, spacing. Extracting those values into CSS custom properties in globals.css ensures the desktop app and HTML report look identical. Users see consistent branding across all output formats.
Introduction
CLI Website Audits for Humans, Agents & LLMs
A comprehensive SEO audit tool with 148 rules across 16 categories that fits into your AI workflow. Built with Node.js, works with your system browser.
Quick Links
- npm: https://www.npmjs.com/package/@seomator/seo-audit
- GitHub: https://github.com/seo-skills/seo-audit-skill
- Web UI: https://seomator.com/free-seo-audit-tool
- Skills:
npx skills add seo-skills/seo-audit-skill
Three Ways to Use SEOmator
CLI for Humans
Run audits directly in your terminal with beautiful, human-readable output:
seomator audit https://example.comPerfect for:
- Manual audits during development
- Quick site health checks
- Terminal-first workflows
Pipe to AI
Pipe clean, LLM-optimized output to any AI assistant:
seomator audit https://example.com --format llm | claudePerfect for:
- Ad-hoc AI assistance with audits
- Custom AI workflows and scripts
- Agents without skill support
AI Agent Skills
Install the skill for fully autonomous AI workflows:
npx skills add seo-skills/seo-audit-skillThen prompt your AI agent:
Use the seo-audit skill to audit this site and fix all issuesPerfect for:
- Autonomous fixing of SEO/accessibility issues
- Multi-step AI workflows with plan mode
- Continuous monitoring and regression detection
Why SEOmator?
AI-Native Design
Built for coding agents. LLM-optimized output works seamlessly with Claude Code, Cursor, and any AI assistant.
Developer-First CLI
npm package with zero config needed. Works with your system Chrome/Chromium for Core Web Vitals.
148 Rules, 16 Categories
Comprehensive coverage across SEO, accessibility, performance, security, and E-E-A-T signals.
Smart Incremental Crawling
SQLite-based storage with content hashing. Skip unchanged pages. Resume interrupted crawls.
E-E-A-T Auditing
Dedicated rules for Experience, Expertise, Authority, and Trust—Google's top ranking signals.
Multiple Output Formats
Console, JSON, HTML reports, Markdown, LLM-friendly output. Export exactly what you need.
Works Where You Work
| Environment | Integration |
|---|---|
| Terminal | Run anywhere with a single command |
| Claude Code | Install the seo-audit skill for autonomous workflows |
| Cursor | Native skill integration with composer mode |
| Any AI Agent | Pipe text/JSON/markdown/llm to any LLM |
| CI/CD | Fail pipelines on audit errors with exit codes |
| Shell Scripts | Integrate into your automation workflows |
Rule Categories
SEOmator runs 148 rules across 16 categories:
| Category | Weight | Rules | Description |
|---|---|---|---|
| Core | 14% | 14 | Meta tags, canonical, H1, indexing |
| Performance | 14% | 12 | Core Web Vitals + performance hints |
| Links | 9% | 13 | Internal/external links |
| Images | 9% | 12 | Image optimization |
| Security | 9% | 12 | HTTPS, headers, mixed content |
| Technical SEO | 8% | 8 | Robots.txt, sitemap, SSL |
| Crawlability | 6% | 6 | Sitemap, indexability signals |
| Structured Data | 5% | 13 | JSON-LD, Schema.org |
| Accessibility | 5% | 12 | WCAG, ARIA compliance |
| Content | 5% | 11 | Text quality, readability, headings |
| Social | 4% | 9 | Open Graph, Twitter Cards |
| E-E-A-T | 4% | 14 | Trust signals, expertise |
| URL Structure | 3% | 2 | Slug keywords, stop words |
| Mobile | 3% | 3 | Font size, horizontal scroll |
| Internationalization | 1% | 2 | Language, hreflang |
| Legal Compliance | 1% | 1 | Cookie consent |
Resources
- GitHub: https://github.com/seo-skills/seo-audit-skill - View source, report issues, contribute
- npm: https://www.npmjs.com/package/@seomator/seo-audit - Package details and versions
- Website: https://seomator.com - Learn more about SEOmator
Quickstart
Install SEOmator and run your first SEO audit
Installation
npm install -g @seomator/seo-auditThis installs the seomator command globally.
Requirements:
- Node.js 18+
- Chrome, Chromium, or Edge (for Core Web Vitals - auto-detected)
After installation, run seomator self doctor to verify everything is set up correctly.
Three Ways to Use SEOmator
CLI for Humans
Run audits directly with human-readable terminal output:
seomator audit https://example.comAI Agent Skill
Install the skill for autonomous AI workflows:
npx skills add seo-skills/seo-audit-skillThen run /seo-audit or prompt your AI agent:
Use the seo-audit skill to audit this site and fix all issuesFor best results, use the skill in plan mode if your agent supports it. This lets the agent analyze issues and create an implementation plan before making changes.
Pipe to AI
Export reports to Claude or other AI assistants:
seomator audit https://example.com --format llm | claudeSee AI Agent Integration for detailed guidance on using SEOmator with Claude Code, Cursor, and other AI coding assistants.
Run Your First Audit
Audit a website with a single command:
seomator audit https://example.comYou'll see output like:
╔═══════════════════════════════════════════════════════════════════════╗
║ ║
║ ███████╗███████╗ ██████╗ ███╗ ███╗ █████╗ ████████╗ ██████╗ ██████╗
║ ██╔════╝██╔════╝██╔═══██╗████╗ ████║██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗
║ ███████╗█████╗ ██║ ██║██╔████╔██║███████║ ██║ ██║ ██║██████╔╝
║ ╚════██║██╔══╝ ██║ ██║██║╚██╔╝██║██╔══██║ ██║ ██║ ██║██╔══██╗
║ ███████║███████╗╚██████╔╝██║ ╚═╝ ██║██║ ██║ ██║ ╚██████╔╝██║ ██║
║ ╚══════╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝
║ ║
║ v2.2.0 • 148 rules • 16 categories ║
║ https://seomator.com ║
║ ║
╚═══════════════════════════════════════════════════════════════════════╝
Auditing: https://example.com
Max pages: 10
✓ Audited 5 pages in 3.2s
──────────────────────────────────────────────────
SEOMATOR REPORT
https://example.com • 5 pages • 85/100 (B)
──────────────────────────────────────────────────
Category Breakdown:
--------------------------------------------------
Core SEO ████████░░ 82%
Performance █████████░ 91%
Links █████████░ 88%
Images ███████░░░ 75%
Security ██████████ 100%
...
ISSUES
Core SEO (1 error, 2 warnings)
core/meta-description (error)
✗ Missing meta description
→ /about
→ /contact
core/og-tags (warning)
⚠ Missing og:image
→ /
Images (3 warnings)
images/alt-text (warning)
⚠ Missing alt attribute
→ /products (3 images)
──────────────────────────────────────────────────
125 passed • 15 warnings • 8 failed
──────────────────────────────────────────────────Common Options
1. Multi-page crawl
Crawl multiple pages instead of just the homepage:
seomator audit https://example.com --crawl -m 202. Skip Core Web Vitals
For faster audits without browser-based metrics:
seomator audit https://example.com --no-cwv3. Export to JSON
Output machine-readable JSON for CI/CD or AI processing:
seomator audit https://example.com --format json -o report.json4. Generate HTML report
Create a visual HTML report with interactive features:
seomator audit https://example.com --format html -o report.htmlThe HTML report includes:
- Category progress bars - Visual overview of all category scores at a glance
- Rule names & descriptions - Human-readable titles instead of just rule IDs
- Collapsible pages lists - "N pages affected" toggles for multi-page issues
- Fix suggestions - Prominent "How to Fix" guidance for each issue
- Dark mode toggle - Switch between light and dark themes
- Status filtering - Filter by All/Failures/Warnings/Passed
- URL filtering - Filter issues by specific page (multi-page crawls)
- Sidebar navigation - Quick jump to any category section
5. Fresh crawl
Ignore cache and fetch all pages fresh:
seomator audit https://example.com --refresh6. Filter categories
Audit specific categories only:
seomator audit https://example.com -c core,perf,securityOutput Formats
| Format | Flag | Use Case |
|---|---|---|
console | (default) | Human-readable terminal output |
json | --format json | CI/CD pipelines, programmatic processing |
html | --format html | Interactive visual reports with dark mode, filtering, progress bars |
markdown | --format markdown | Documentation, GitHub |
llm | --format llm | Compact AI-optimized (50-70% smaller) |
Configuration
Create a seomator.toml config file for consistent settings:
seomator init # Interactive setup
seomator init -y # Use defaults
seomator init --preset blog # Use blog presetExample configuration:
[project]
name = "my-website"
domains = ["example.com", "www.example.com"]
[crawler]
max_pages = 100
concurrency = 3
timeout_ms = 30000
exclude = ["/admin/**", "/api/**"]
[rules]
enable = ["*"]
disable = ["perf-inp"]
[output]
format = "console"Exit Codes
SEOmator uses exit codes for CI/CD integration:
| Code | Meaning |
|---|---|
0 | Passed (score >= 70) |
1 | Failed (score < 70) |
2 | Error |
Using with AI Agents
SEOmator is designed for AI workflows. Two primary methods:
1. Install the Skill (Recommended)
npx skills add seo-skills/seo-audit-skillThen use with Claude Code, Cursor, or any skill-compatible agent:
Use the seo-audit skill to audit example.com and fix all issues2. Pipe Output to AI
# Audit and pipe directly to Claude
seomator audit https://example.com --format llm | claude "analyze and prioritize fixes"The --format llm provides compact, token-optimized output designed specifically for AI agents.
See AI Agent Integration for advanced workflows including plan mode, subagents, and continuous monitoring.
Next Steps
- Run
seomator --helpfor all available commands - See the main README for full CLI reference
- Check the GitHub repo for updates
SEOmator Documentation
Comprehensive SEO audit CLI tool with 148 rules across 16 categories
Getting Started
| Document | Description |
|---|---|
| Introduction | Overview, features, and why SEOmator |
| Quickstart | Installation and first audit |
| Configuration | Config files, presets, and options |
Integration
| Document | Description |
|---|---|
| AI Agent Integration | Claude Code, Cursor, and LLM workflows |
Reference
| Document | Description |
|---|---|
| Technical Architecture | How SEOmator works under the hood |
| SEO Audit Rules | All 148 rules with examples and fixes |
| Storage Architecture | SQLite database schema and internals |
Quick Links
# Install
npm install -g @seomator/seo-audit
# Run audit
seomator audit https://example.com
# Multi-page crawl
seomator audit https://example.com --crawl -m 20
# Create config
seomator init
# AI-optimized output
seomator audit https://example.com --format llm | claudeExternal Resources
- npm: https://www.npmjs.com/package/@seomator/seo-audit
- GitHub: https://github.com/seo-skills/seo-audit-skill
- Web UI: https://seomator.com/free-seo-audit-tool
- Skills:
npx skills add seo-skills/seo-audit-skill
Storage Architecture
Technical documentation for SEOmator's SQLite-based storage system.
Overview
SEOmator uses a split-database architecture:
- Per-project databases for crawl data (isolates projects, enables easy cleanup)
- Centralized audits database for audit results (enables cross-project analytics)
This design balances data isolation with analytical capabilities.
Database Locations
~/.seomator/
├── projects/
│ └── <domain>/
│ └── project.db # Per-domain crawl data
├── audits.db # Centralized audit results
└── link-cache.db # External link validation cacheTechnology Stack
- better-sqlite3: Synchronous SQLite bindings for Node.js
- zlib: Native Node.js compression for HTML storage
- crypto: SHA-256 hashing for URL deduplication
Project Database Schema
Located at ~/.seomator/projects/<domain>/project.db
Tables
projects
Represents a single domain/website.
CREATE TABLE projects (
id INTEGER PRIMARY KEY,
domain TEXT NOT NULL UNIQUE,
name TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
config_json TEXT
);crawls
Represents a single crawl session.
CREATE TABLE crawls (
id INTEGER PRIMARY KEY,
crawl_id TEXT NOT NULL UNIQUE, -- e.g., "2024-01-23-abc123"
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
start_url TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'running', -- running|completed|failed|cancelled
started_at TEXT DEFAULT (datetime('now')),
completed_at TEXT,
config_json TEXT,
stats_json TEXT, -- {totalPages, duration, errorCount}
error_message TEXT
);pages
Stores crawled page data with optional compressed HTML.
CREATE TABLE pages (
id INTEGER PRIMARY KEY,
crawl_id INTEGER NOT NULL REFERENCES crawls(id) ON DELETE CASCADE,
url TEXT NOT NULL,
url_hash TEXT NOT NULL, -- SHA-256 truncated to 16 chars
status_code INTEGER DEFAULT 0,
depth INTEGER DEFAULT 0,
content_type TEXT,
html BLOB, -- Compressed if > 10KB
html_compressed INTEGER DEFAULT 0, -- 1 if compressed
html_size INTEGER, -- Original size in bytes
headers_json TEXT,
load_time_ms INTEGER,
ttfb_ms INTEGER,
cwv_lcp REAL, -- Core Web Vitals
cwv_cls REAL,
cwv_inp REAL,
cwv_fcp REAL,
cwv_ttfb REAL,
error_message TEXT,
crawled_at TEXT DEFAULT (datetime('now')),
UNIQUE(crawl_id, url_hash)
);links
Stores links found on pages.
CREATE TABLE links (
id INTEGER PRIMARY KEY,
page_id INTEGER NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
href TEXT NOT NULL,
href_hash TEXT NOT NULL,
anchor_text TEXT,
is_internal INTEGER DEFAULT 0,
is_nofollow INTEGER DEFAULT 0,
rel_value TEXT,
target_status_code INTEGER, -- Populated after link checking
target_error TEXT
);images
Stores images found on pages.
CREATE TABLE images (
id INTEGER PRIMARY KEY,
page_id INTEGER NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
src TEXT NOT NULL,
src_hash TEXT NOT NULL,
alt TEXT,
has_alt INTEGER DEFAULT 0,
width TEXT,
height TEXT,
is_lazy_loaded INTEGER DEFAULT 0,
loading_attr TEXT,
srcset TEXT,
file_size INTEGER,
format TEXT -- jpg, png, webp, etc.
);frontier
Queue for resumable crawling (future use).
CREATE TABLE frontier (
id INTEGER PRIMARY KEY,
crawl_id INTEGER NOT NULL REFERENCES crawls(id) ON DELETE CASCADE,
url TEXT NOT NULL,
url_hash TEXT NOT NULL,
depth INTEGER DEFAULT 0,
priority INTEGER DEFAULT 0,
status TEXT DEFAULT 'pending', -- pending|processing|completed|skipped
discovered_at TEXT DEFAULT (datetime('now')),
UNIQUE(crawl_id, url_hash)
);Audits Database Schema
Located at ~/.seomator/audits.db
Tables
audits
Main audit records.
CREATE TABLE audits (
id INTEGER PRIMARY KEY,
audit_id TEXT NOT NULL UNIQUE,
domain TEXT NOT NULL,
project_name TEXT,
crawl_id TEXT, -- Links to project DB crawl
start_url TEXT NOT NULL,
overall_score INTEGER NOT NULL,
total_rules INTEGER DEFAULT 0,
passed_count INTEGER DEFAULT 0,
warning_count INTEGER DEFAULT 0,
failed_count INTEGER DEFAULT 0,
pages_audited INTEGER DEFAULT 1,
config_json TEXT,
started_at TEXT DEFAULT (datetime('now')),
completed_at TEXT,
status TEXT DEFAULT 'running' -- running|completed|failed
);audit_categories
Category-level results.
CREATE TABLE audit_categories (
id INTEGER PRIMARY KEY,
audit_id INTEGER NOT NULL REFERENCES audits(id) ON DELETE CASCADE,
category_id TEXT NOT NULL,
category_name TEXT NOT NULL,
score INTEGER NOT NULL,
weight INTEGER NOT NULL,
pass_count INTEGER DEFAULT 0,
warn_count INTEGER DEFAULT 0,
fail_count INTEGER DEFAULT 0,
UNIQUE(audit_id, category_id)
);audit_results
Per-rule, per-page audit results.
CREATE TABLE audit_results (
id INTEGER PRIMARY KEY,
audit_id INTEGER NOT NULL REFERENCES audits(id) ON DELETE CASCADE,
category_id TEXT NOT NULL,
rule_id TEXT NOT NULL,
rule_name TEXT NOT NULL,
page_url TEXT NOT NULL,
page_url_hash TEXT NOT NULL,
status TEXT NOT NULL, -- pass|warn|fail
score INTEGER NOT NULL,
message TEXT NOT NULL,
details_json TEXT,
executed_at TEXT DEFAULT (datetime('now'))
);issues
Aggregated issues for reporting.
CREATE TABLE issues (
id INTEGER PRIMARY KEY,
audit_id INTEGER NOT NULL REFERENCES audits(id) ON DELETE CASCADE,
rule_id TEXT NOT NULL,
category_id TEXT NOT NULL,
severity TEXT NOT NULL, -- critical|warning|info
title TEXT NOT NULL,
description TEXT,
affected_pages_count INTEGER DEFAULT 1,
affected_pages_json TEXT, -- JSON array of URLs
fix_suggestion TEXT,
priority_score INTEGER DEFAULT 0 -- Calculated score for sorting
);audit_comparisons
Stores audit-to-audit comparisons.
CREATE TABLE audit_comparisons (
id INTEGER PRIMARY KEY,
current_audit_id INTEGER REFERENCES audits(id) ON DELETE CASCADE,
previous_audit_id INTEGER REFERENCES audits(id) ON DELETE CASCADE,
domain TEXT NOT NULL,
score_delta INTEGER NOT NULL,
category_deltas_json TEXT, -- Array of {categoryId, delta}
new_issues_count INTEGER DEFAULT 0,
fixed_issues_count INTEGER DEFAULT 0,
compared_at TEXT DEFAULT (datetime('now'))
);Key Algorithms
URL Hashing
Uses SHA-256 truncated to 16 characters (64 bits) for compact, deterministic hashing.
import * as crypto from 'crypto';
function hashUrl(url: string): string {
return crypto.createHash('sha256').update(url).digest('hex').slice(0, 16);
}HTML Compression
Compresses HTML pages larger than 10KB using zlib deflate (level 6).
import * as zlib from 'zlib';
const THRESHOLD = 10 * 1024; // 10KB
function compressHtml(html: string): { data: Buffer; compressed: boolean } {
const buf = Buffer.from(html, 'utf-8');
if (buf.length < THRESHOLD) {
return { data: buf, compressed: false };
}
const compressed = zlib.deflateSync(buf, { level: 6 });
// Only use compressed if smaller
if (compressed.length >= buf.length) {
return { data: buf, compressed: false };
}
return { data: compressed, compressed: true };
}Typical compression ratios: 70-80% reduction for HTML.
Issue Priority Scoring
Prioritizes issues based on severity and affected page count.
function calculatePriorityScore(severity: string, affectedPages: number): number {
const severityScore = { critical: 100, warning: 50, info: 10 }[severity] || 0;
return severityScore * Math.log10(affectedPages + 1);
}Usage Examples
Opening a Project Database
import { ProjectDatabase } from '../storage/project-db/index.js';
// Open/create database for a domain
const db = new ProjectDatabase('example.com');
// Or from a full URL (domain is extracted)
const db2 = new ProjectDatabase('https://www.example.com/path');
// Get or create project
const project = db.getOrCreateProject('My Website');
// Create a crawl
const crawl = db.createCrawl({
crawlId: '2024-01-23-abc123',
startUrl: 'https://example.com',
config: { /* ... */ },
});
// Insert pages with automatic HTML compression
db.insertPage(crawl.id, {
url: 'https://example.com/page',
statusCode: 200,
depth: 1,
html: '<html>...</html>', // Auto-compressed if > 10KB
headers: { 'content-type': 'text/html' },
loadTimeMs: 250,
});
// Complete the crawl
db.completeCrawl(crawl.crawlId, {
totalPages: 10,
duration: 5000,
errorCount: 0,
});
// Close when done
db.close();Using the Audits Database
import { getAuditsDatabase, closeAuditsDatabase } from '../storage/audits-db/index.js';
// Get singleton instance
const auditsDb = getAuditsDatabase();
// Create an audit
const audit = auditsDb.createAudit({
auditId: '2024-01-23-xyz789',
domain: 'example.com',
projectName: 'My Website',
startUrl: 'https://example.com',
});
// Insert results
auditsDb.insertResults(audit.id, [
{
categoryId: 'meta-tags',
ruleId: 'title-present',
ruleName: 'Page Title Present',
pageUrl: 'https://example.com',
status: 'pass',
score: 100,
message: 'Title tag found',
},
// ... more results
]);
// Complete audit
auditsDb.completeAudit(audit.auditId, {
overallScore: 85,
totalRules: 55,
passedCount: 45,
warningCount: 7,
failedCount: 3,
pagesAudited: 10,
});
// Generate issues from failed results
auditsDb.generateIssuesFromResults(audit.id);
// Get top priority issues
const issues = auditsDb.getTopPriorityIssues(audit.id, 10);
// Compare with previous audit
const previousAudit = auditsDb.getPreviousAudit('example.com', audit.auditId);
if (previousAudit) {
const comparison = auditsDb.compareAudits(audit.id, previousAudit.id);
console.log(`Score delta: ${comparison.scoreDelta}`);
}
// Close when app exits
closeAuditsDatabase();Querying Results
// Get all failed results
const failures = auditsDb.getResultsByStatus(audit.id, 'fail');
// Get results for a specific rule
const titleResults = auditsDb.getResultsByRule(audit.id, 'title-present');
// Get results for a specific page
const pageResults = auditsDb.getResultsByPage(audit.id, 'https://example.com/contact');
// Get issues by severity
const criticalIssues = auditsDb.getIssuesBySeverity(audit.id, 'critical');
// Get score trend for a domain
const trend = auditsDb.getScoreTrend('example.com', 10);Performance Considerations
WAL Mode
All databases use WAL (Write-Ahead Logging) mode for:
- Concurrent reads during writes
- Better performance for write-heavy workloads
- Crash recovery
PRAGMA journal_mode = WAL;Foreign Keys
Foreign keys are enabled with cascade deletes for data integrity:
PRAGMA foreign_keys = ON;Deleting a crawl automatically deletes all associated pages, links, and images.
Indexes
Strategic indexes are created for common query patterns:
-- Project database
CREATE INDEX idx_pages_crawl ON pages(crawl_id);
CREATE INDEX idx_pages_url_hash ON pages(url_hash);
CREATE INDEX idx_links_page ON links(page_id);
-- Audits database
CREATE INDEX idx_audits_domain ON audits(domain);
CREATE INDEX idx_results_rule ON audit_results(audit_id, rule_id);
CREATE INDEX idx_issues_priority ON issues(audit_id, priority_score DESC);Batch Operations
Use transactions for bulk inserts:
// Insert multiple pages efficiently
db.insertPages(crawlId, pagesArray); // Uses single transaction
// Insert multiple results
auditsDb.insertResults(auditId, resultsArray); // Uses single transactionMigration from JSON
Existing JSON files can be migrated using the CLI:
# Preview migration
seomator db migrate --dry-run
# Run migration (creates backups)
seomator db migrate
# Rollback if needed
seomator db restoreThe migration process: 1. Detects .seomator/crawls/*.json and .seomator/reports/*.json 2. Creates appropriate project databases and audits entries 3. Backs up original files to .bak directories 4. Reports statistics on migrated files
Type System
All database records have corresponding TypeScript types in src/storage/types.ts:
- Db* prefix: Raw database records (match SQLite column names)
- Hydrated*: Enriched records with parsed JSON fields and Date objects
- *Options: Query/filter options for list operations
- *InsertInput**: Input types for creating records
Example:
// Raw database record
interface DbPage {
id: number;
crawl_id: number;
url: string;
html_compressed: number; // 0 or 1
// ...
}
// Hydrated for application use
interface HydratedPage {
id: number;
crawlId: number; // camelCase
url: string;
htmlCompressed: boolean; // boolean, not 0/1
crawledAt: Date; // Date, not string
// ...
}Future Enhancements
Planned improvements:
1. Resumable Crawling: Use frontier table to resume interrupted crawls 2. Full-Text Search: Add FTS5 virtual tables for content search 3. Scheduled Audits: Store scheduling metadata for recurring audits 4. Data Retention: Automatic cleanup of old crawls/audits based on policy 5. Export/Import: Database backup and restoration utilities
Technical Architecture
How SEOmator works under the hood
This document explains the internal architecture of SEOmator, covering data flow, components, and how different parts interact.
High-Level Overview
┌─────────────────────────────────────────────────────────────────────────┐
│ CLI Entry │
│ (src/cli.ts) │
└─────────────────────────────────────────────────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────┐ ┌───────────┐ ┌──────────┐
│ audit │ │ crawl │ │ report │
│ command │ │ command │ │ command │
└────┬────┘ └─────┬─────┘ └────┬─────┘
│ │ │
▼ ▼ │
┌─────────────────────────────────┐ │
│ Crawler │ │
│ (src/crawler/crawler.ts) │ │
└─────────────┬───────────────────┘ │
│ │
▼ │
┌─────────────────────────────────┐ │
│ Fetcher │ │
│ (src/crawler/fetcher.ts) │ │
│ + Playwright (for CWV) │ │
└─────────────┬───────────────────┘ │
│ │
▼ │
┌─────────────────────────────────┐ │
│ Project DB │ │
│ (SQLite per domain) │ │
│ - Pages (HTML compressed) │ │
│ - Links │ │
│ - Images │ │
└─────────────┬───────────────────┘ │
│ │
▼ │
┌─────────────────────────────────┐ │
│ Auditor │ │
│ (src/auditor.ts) │ │
│ 148 Rules × N Pages │ │
└─────────────┬───────────────────┘ │
│ │
▼ │
┌─────────────────────────────────┐ │
│ Audits DB │◄─────┘
│ (Centralized SQLite) │
│ - Audit results │
│ - Per-rule scores │
│ - Issues aggregation │
└─────────────┬───────────────────┘
│
▼
┌─────────────────────────────────┐
│ Reporters │
│ console │ json │ html │ │
│ markdown │ llm │
└─────────────────────────────────┘Component Details
1. CLI Entry (src/cli.ts)
The main entry point using Commander.js. Registers all subcommands:
seomator
├── audit <url> # Run SEO audit
├── crawl <url> # Crawl without analysis
├── analyze [id] # Analyze stored crawl
├── report [query] # View past reports
├── init # Create config file
├── config # View/modify config
├── db # Database management
└── self doctor # System health checkKey responsibilities:
- Parse CLI arguments
- Load and merge configuration
- Route to appropriate command handler
- Handle exit codes (0=pass, 1=fail, 2=error)
2. Configuration System (src/config/)
src/config/
├── schema.ts # TypeScript type definitions
├── defaults.ts # Default values & presets
├── loader.ts # Config file discovery & loading
├── writer.ts # Config file generation (init)
└── validator.ts # Config validationConfig resolution order: 1. CLI arguments (highest priority) 2. ./seomator.toml (project config) 3. Parent directories (searches up tree) 4. ~/.seomator/config.toml (global config) 5. Built-in defaults (lowest priority)
Config format: TOML (human-readable, supports comments)
3. Crawler (src/crawler/)
src/crawler/
├── crawler.ts # Queue-based multi-page crawler
├── fetcher.ts # HTTP fetching + Playwright for CWV
└── url-filter.ts # URL normalization, include/excludeCrawler Flow
┌──────────────┐
│ Start URL │
└──────┬───────┘
│
▼
┌──────────────┐
│ URL Queue │◄────────────────┐
└──────┬───────┘ │
│ │
▼ │
┌──────────────┐ │
│ URL Filter │ │
│ (normalize, │ │
│ dedupe) │ │
└──────┬───────┘ │
│ │
▼ │
┌────────────────────────┐ │
│ Fetcher │ │
│ ┌──────────────────┐ │ │
│ │ HTTP Request │ │ │
│ │ (Node fetch) │ │ │
│ └────────┬─────────┘ │ │
│ │ │ │
│ ┌────────▼─────────┐ │ │
│ │ Playwright │ │ │
│ │ (CWV metrics) │ │ │
│ └────────┬─────────┘ │ │
└───────────┼────────────┘ │
│ │
▼ │
┌────────────────────────┐ │
│ Parse HTML │ │
│ (Cheerio) │ │
│ - Extract links ────┼─────────────┘
│ - Extract images │
│ - Store in DB │
└────────────────────────┘URL Filtering (url-filter.ts)
- Normalization: Strips tracking params (utm_*, gclid, fbclid)
- Deduplication: Hash-based URL tracking
- Include/Exclude: Glob pattern matching
- Domain restriction: Stays within configured domains
- Budget control:
max_prefix_budgetprevents over-crawling single paths
Fetcher (fetcher.ts)
Two modes: 1. HTTP-only (fast): Node.js fetch for HTML 2. Playwright (CWV): Launches browser for Core Web Vitals
// Playwright metrics collected:
interface CoreWebVitals {
lcp: number; // Largest Contentful Paint
fcp: number; // First Contentful Paint
cls: number; // Cumulative Layout Shift
ttfb: number; // Time to First Byte
inp: number; // Interaction to Next Paint
}4. Storage System (src/storage/)
src/storage/
├── index.ts # Main exports
├── types.ts # Database record types
├── paths.ts # Directory & path utilities
├── utils/
│ ├── hash.ts # URL hashing (SHA-256)
│ └── compression.ts # HTML compression (zlib)
├── project-db/ # Per-domain database
│ ├── index.ts # ProjectDatabase class
│ ├── schema.ts # Table definitions
│ ├── projects.ts # Project CRUD
│ ├── crawls.ts # Crawl operations
│ ├── pages.ts # Page storage
│ ├── links.ts # Link operations
│ └── images.ts # Image operations
├── audits-db/ # Centralized audits
│ ├── index.ts # AuditsDatabase singleton
│ ├── schema.ts # Table definitions
│ ├── audits.ts # Audit CRUD
│ ├── results.ts # Per-rule results
│ ├── issues.ts # Issue aggregation
│ └── comparisons.ts # Audit comparisons
└── link-cache.ts # External link cacheDatabase Architecture
~/.seomator/
├── projects/
│ ├── example.com/
│ │ └── project.db # Per-domain SQLite
│ └── mysite.org/
│ └── project.db
├── audits.db # Centralized audits
└── link-cache.db # External link cacheProject Database Schema
-- Crawls table
CREATE TABLE crawls (
id TEXT PRIMARY KEY,
project_id TEXT,
started_at TEXT,
completed_at TEXT,
status TEXT,
pages_crawled INTEGER,
config TEXT -- JSON
);
-- Pages table (HTML compressed with zlib)
CREATE TABLE pages (
id TEXT PRIMARY KEY,
crawl_id TEXT,
url TEXT,
url_hash TEXT,
status_code INTEGER,
content_type TEXT,
html BLOB, -- Compressed if >10KB
html_compressed INTEGER,
title TEXT,
meta_description TEXT,
h1 TEXT,
cwv TEXT, -- JSON: {lcp, fcp, cls, ttfb, inp}
fetched_at TEXT
);
-- Links table
CREATE TABLE links (
id INTEGER PRIMARY KEY,
page_id TEXT,
href TEXT,
text TEXT,
rel TEXT,
is_internal INTEGER,
status_code INTEGER
);
-- Images table
CREATE TABLE images (
id INTEGER PRIMARY KEY,
page_id TEXT,
src TEXT,
alt TEXT,
width INTEGER,
height INTEGER,
loading TEXT
);Audits Database Schema
-- Audits table
CREATE TABLE audits (
id TEXT PRIMARY KEY,
crawl_id TEXT,
domain TEXT,
started_at TEXT,
completed_at TEXT,
overall_score REAL,
grade TEXT,
pages_audited INTEGER,
config TEXT -- JSON
);
-- Results table (per-rule, per-page)
CREATE TABLE results (
id INTEGER PRIMARY KEY,
audit_id TEXT,
page_url TEXT,
rule_id TEXT,
category TEXT,
status TEXT, -- pass, warn, fail
score REAL,
message TEXT,
details TEXT -- JSON
);
-- Issues table (aggregated)
CREATE TABLE issues (
id INTEGER PRIMARY KEY,
audit_id TEXT,
rule_id TEXT,
category TEXT,
severity TEXT,
count INTEGER,
affected_urls TEXT, -- JSON array
recommendation TEXT
);5. Rules Engine (src/rules/)
src/rules/
├── define-rule.ts # Rule definition helper
├── pattern-matcher.ts # Wildcard rule matching
├── core/ # 14 rules
├── perf/ # 12 rules
├── links/ # 13 rules
├── images/ # 12 rules
├── security/ # 12 rules
├── technical/ # 8 rules
├── crawl/ # 6 rules
├── schema/ # 13 rules
├── a11y/ # 12 rules
├── content/ # 11 rules
├── social/ # 9 rules
├── eeat/ # 14 rules
├── url/ # 2 rules
├── mobile/ # 3 rules
├── i18n/ # 2 rules
└── legal/ # 1 ruleRule Structure
interface Rule {
id: string; // e.g., 'core-meta-title'
name: string; // e.g., 'Meta Title'
description: string;
category: string; // e.g., 'core'
weight: number; // 1-10, affects category score
run(context: AuditContext): RuleResult;
}
interface AuditContext {
url: string;
html: string;
$: CheerioAPI; // Parsed DOM
headers: Headers;
statusCode: number;
cwv?: CoreWebVitals;
config: Config;
}
interface RuleResult {
status: 'pass' | 'warn' | 'fail';
score: number; // 0-100
message: string;
details?: Record<string, unknown>;
}Rule Execution Flow
┌─────────────────┐
│ Page Data │
│ (from crawl) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Parse HTML │
│ (Cheerio) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Build Context │
│ {url, $, cwv} │
└────────┬────────┘
│
▼
┌─────────────────────────────────────┐
│ For each enabled rule: │
│ ┌─────────────────────────────┐ │
│ │ rule.run(context) │ │
│ │ → RuleResult │ │
│ └─────────────────────────────┘ │
└────────────────┬────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Store Results │
│ - Per-rule scores │
│ - Aggregate by category │
│ - Calculate overall score │
└─────────────────────────────────────┘6. Auditor (src/auditor.ts)
Orchestrates the audit process:
class Auditor {
async audit(url: string, options: AuditOptions): Promise<AuditReport> {
// 1. Load or run crawl
const crawl = await this.getCrawl(url, options);
// 2. Load rules (filtered by config)
const rules = this.loadRules(options.config);
// 3. Run rules against each page
const results = [];
for (const page of crawl.pages) {
const context = this.buildContext(page);
for (const rule of rules) {
results.push(await rule.run(context));
}
}
// 4. Calculate scores
const scores = this.calculateScores(results);
// 5. Store in audits DB
await this.storeAudit(scores);
return scores;
}
}7. Scoring System (src/scoring.ts)
// Status to score mapping
const STATUS_SCORES = {
pass: 100,
warn: 50,
fail: 0,
};
// Category weights (total = 100%)
const CATEGORY_WEIGHTS = {
core: 14,
perf: 14,
links: 9,
images: 9,
security: 9,
technical: 8,
crawl: 6,
schema: 5,
a11y: 5,
content: 5,
social: 4,
eeat: 4,
url: 3,
mobile: 3,
i18n: 1,
legal: 1,
};Scoring algorithm:
1. Rule score: Based on status (pass=100, warn=50, fail=0) 2. Category score: Weighted average of rule scores within category 3. Overall score: Weighted average of category scores 4. Grade: A (90+), B (80+), C (70+), D (60+), F (<60)
8. Reporters (src/reporters/)
src/reporters/
├── banner.ts # ASCII art, grades, progress bars
├── terminal.ts # Console output with colors
├── progress.ts # Real-time progress indicators
├── json.ts # JSON output
├── html-reporter.ts # Self-contained HTML report
├── markdown-reporter.ts # GitHub-flavored Markdown
└── llm-reporter.ts # Token-optimized LLM outputOutput Format Comparison
| Format | Size | Use Case |
|---|---|---|
console | - | Human terminal output |
json | 100% | CI/CD, programmatic |
html | 150% | Visual reports |
markdown | 80% | Documentation |
llm | 30-50% | AI agents (token-optimized) |
LLM Reporter
Designed for minimal token usage:
<audit url="example.com" score="85" grade="B" pages="5">
<issues>
<issue rule="core-meta-description" severity="error" count="2">
<msg>Missing meta description</msg>
<fix>Add meta description tag</fix>
<urls>/about,/contact</urls>
</issue>
</issues>
</audit>Data Flow: Complete Audit
User runs: seomator audit https://example.com --crawl -m 10
1. CLI PARSING
├── Parse arguments
├── Load config (seomator.toml + defaults)
└── Merge options
2. CRAWLING
├── Initialize URL queue with start URL
├── For each URL (up to max_pages):
│ ├── Check URL filter (include/exclude)
│ ├── Fetch HTML (HTTP request)
│ ├── Optionally run Playwright for CWV
│ ├── Parse with Cheerio
│ ├── Extract links → add to queue
│ ├── Extract images
│ └── Store in project.db
└── Mark crawl complete
3. AUDITING
├── Load enabled rules (148 total, filtered by config)
├── For each page:
│ ├── Build AuditContext {url, $, headers, cwv}
│ ├── Run each rule → RuleResult
│ └── Store results in audits.db
└── Calculate scores
4. SCORING
├── Aggregate rule results by category
├── Calculate weighted category scores
├── Calculate overall score
└── Determine grade (A-F)
5. REPORTING
├── Select reporter based on --format
├── Generate output
└── Write to file or stdout
6. EXIT
└── Return exit code (0=pass, 1=fail, 2=error)External Dependencies
| Dependency | Purpose |
|---|---|
commander | CLI argument parsing |
cheerio | HTML parsing (jQuery-like) |
playwright | Browser automation for CWV |
better-sqlite3 | SQLite database |
chalk | Terminal colors |
cli-progress | Progress bars |
ora | Spinners |
@iarna/toml | TOML config parsing |
Performance Considerations
Concurrency
- Crawler: Configurable concurrent requests (default: 3)
- External links: Separate concurrency (default: 5)
- Rules: Run sequentially per page (DOM access)
Caching
- Pages: Stored in SQLite with content hashing
- External links: Cached with configurable TTL
- Crawls: Can be resumed if interrupted
Memory
- HTML compression: zlib for pages >10KB
- Streaming: Large crawls process pages incrementally
- SQLite WAL: Enables concurrent reads during writes
Extending SEOmator
Adding a New Rule
// src/rules/core/my-rule.ts
import { defineRule } from '../define-rule.js';
export const myRule = defineRule({
id: 'core-my-rule',
name: 'My Rule',
description: 'Checks for something important',
category: 'core',
weight: 5,
run(context) {
const { $ } = context;
// Check something
const hasFeature = $('meta[name="feature"]').length > 0;
if (hasFeature) {
return {
status: 'pass',
score: 100,
message: 'Feature found',
};
}
return {
status: 'fail',
score: 0,
message: 'Feature missing',
details: {
recommendation: 'Add <meta name="feature"> tag',
},
};
},
});Adding a New Reporter
// src/reporters/my-reporter.ts
import type { AuditReport } from '../types.js';
export function generateMyReport(report: AuditReport): string {
// Transform report to your format
return myFormat;
}Next Steps
- Configuration - Config options reference
- SEO Audit Rules - All 148 rules
- Storage Architecture - Database details
# electron-builder configuration
# Docs: https://www.electron.build/configuration
appId: com.seomator.desktop
productName: SEOmator
copyright: Copyright © 2026 SEOmator
directories:
buildResources: electron/resources
output: release
# Include compiled Electron code + package.json (for main entry resolution).
# Production node_modules are included automatically by electron-builder.
files:
- dist-electron/**/*
- package.json
- "!src/**"
- "!electron/**"
- "!scripts/**"
- "!docs/**"
- "!dist/**"
- "!*.md"
- "!tsconfig*"
- "!tsup.config*"
- "!vitest*"
- "!.git*"
# Pack everything into asar, but unpack native .node addons
# (they can't be loaded from inside an asar archive)
asar: true
asarUnpack:
- "**/*.node"
# ──────────────── macOS ────────────────
mac:
category: public.app-category.developer-tools
target:
- target: dmg
arch: [universal]
- target: zip
arch: [universal]
darkModeSupport: true
icon: electron/resources/icon.png
dmg:
artifactName: SEOmator-${version}-mac.${ext}
contents:
- x: 130
y: 220
- x: 410
y: 220
type: link
path: /Applications
# ──────────────── Windows ────────────────
win:
target:
- target: nsis
arch: [x64]
icon: electron/resources/icon.png
artifactName: SEOmator-${version}-win-${arch}.${ext}
nsis:
oneClick: false
perMachine: false
allowToChangeInstallationDirectory: true
createDesktopShortcut: true
createStartMenuShortcut: true
# ──────────────── Linux ────────────────
linux:
target:
- AppImage
category: Development
icon: electron/resources/icon.png
artifactName: SEOmator-${version}-linux-${arch}.${ext}
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
import { resolve } from 'path';
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()],
build: {
outDir: 'dist-electron/main',
rollupOptions: {
input: resolve(__dirname, 'main/index.ts'),
},
},
resolve: {
alias: {
'@core': resolve(__dirname, '../src'),
},
},
},
preload: {
plugins: [externalizeDepsPlugin()],
build: {
outDir: 'dist-electron/preload',
rollupOptions: {
input: resolve(__dirname, 'preload/index.ts'),
},
},
},
renderer: {
root: resolve(__dirname, 'renderer'),
build: {
outDir: resolve(__dirname, '../dist-electron/renderer'),
rollupOptions: {
input: resolve(__dirname, 'renderer/index.html'),
},
},
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@renderer': resolve(__dirname, 'renderer'),
},
},
},
});
/**
* Audit Bridge — connects the Auditor class to Electron IPC.
*
* The existing Auditor class already uses a callback interface
* (onCategoryStart, onCategoryComplete, etc.), so we simply wire
* those callbacks to BrowserWindow.webContents.send() calls.
*/
import { BrowserWindow, ipcMain } from 'electron';
import { Auditor } from '@core/auditor.js';
import type { AuditResult } from '@core/types.js';
import { getRuleById } from '@core/rules/registry.js';
import { IPC_CHANNELS, type AuditRunArgs, type AuditCompletePayload, type RuleMetadataIpc } from '../shared/ipc-types.js';
import { fetchPageWithBrowserWindow } from './electron-fetcher.js';
/** Build a map of ruleId -> { name, description } from the rule registry */
function buildRuleMetadata(result: AuditResult): Record<string, RuleMetadataIpc> {
const metadata: Record<string, RuleMetadataIpc> = {};
for (const cat of result.categoryResults) {
for (const r of cat.results) {
if (!metadata[r.ruleId]) {
const rule = getRuleById(r.ruleId);
metadata[r.ruleId] = {
name: rule?.name ?? r.ruleId.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
description: rule?.description ?? '',
};
}
}
}
return metadata;
}
let currentAuditor: Auditor | null = null;
let abortController: AbortController | null = null;
export function registerAuditHandlers(getWindow: () => BrowserWindow | null): void {
ipcMain.on(IPC_CHANNELS.AUDIT_RUN, async (_event, args: AuditRunArgs) => {
const win = getWindow();
if (!win) return;
// Prevent concurrent audits
if (currentAuditor) {
win.webContents.send(IPC_CHANNELS.AUDIT_ERROR, 'An audit is already running');
return;
}
abortController = new AbortController();
try {
currentAuditor = new Auditor({
measureCwv: args.options.measureCwv ?? false,
categories: args.options.categories ?? [],
browserFetcher: fetchPageWithBrowserWindow,
onCategoryStart: (categoryId, categoryName) => {
if (!abortController?.signal.aborted) {
win.webContents.send(IPC_CHANNELS.AUDIT_CATEGORY_START, {
categoryId,
categoryName,
});
}
},
onCategoryComplete: (categoryId, categoryName, result) => {
if (!abortController?.signal.aborted) {
win.webContents.send(IPC_CHANNELS.AUDIT_CATEGORY_COMPLETE, {
categoryId,
categoryName,
result,
});
}
},
onRuleComplete: (ruleId, ruleName, result) => {
if (!abortController?.signal.aborted) {
win.webContents.send(IPC_CHANNELS.AUDIT_RULE_COMPLETE, {
ruleId,
ruleName,
result,
});
}
},
onPageComplete: (url, pageNumber, totalPages) => {
if (!abortController?.signal.aborted) {
win.webContents.send(IPC_CHANNELS.AUDIT_PAGE_COMPLETE, {
url,
pageNumber,
totalPages,
});
}
},
});
let result: AuditResult;
if (args.options.crawl) {
result = await currentAuditor.auditWithCrawl(
args.url,
args.options.maxPages ?? 10,
args.options.concurrency ?? 3,
);
} else {
result = await currentAuditor.audit(args.url);
}
if (!abortController?.signal.aborted) {
const payload: AuditCompletePayload = {
result,
ruleMetadata: buildRuleMetadata(result),
};
win.webContents.send(IPC_CHANNELS.AUDIT_COMPLETE, payload);
}
} catch (error) {
if (!abortController?.signal.aborted) {
const message = error instanceof Error ? error.message : 'Unknown error';
win.webContents.send(IPC_CHANNELS.AUDIT_ERROR, message);
}
} finally {
currentAuditor = null;
abortController = null;
}
});
ipcMain.on(IPC_CHANNELS.AUDIT_CANCEL, () => {
if (abortController) {
abortController.abort();
currentAuditor = null;
abortController = null;
}
});
}
/**
* Database Bridge — exposes AuditsDatabase queries over IPC.
*
* Uses the invoke/handle pattern for request-response queries.
* The AuditsDatabase is a singleton that stores data in ~/.seomator/audits.db.
*/
import { ipcMain } from 'electron';
import { AuditsDatabase } from '@core/storage/audits-db/index.js';
import {
IPC_CHANNELS,
type DbListAuditsArgs,
type DbScoreTrendArgs,
type AuditSummaryIpc,
type ScoreTrendPoint,
type AuditDetailIpc,
type RuleMetadataIpc,
} from '../shared/ipc-types.js';
import { getRuleById } from '@core/rules/registry.js';
import type { AuditResult, CategoryResult, RuleResult } from '@core/types.js';
export function registerDbHandlers(): void {
ipcMain.handle(
IPC_CHANNELS.DB_LIST_AUDITS,
(_event, args?: DbListAuditsArgs): AuditSummaryIpc[] => {
const db = AuditsDatabase.getInstance();
const summaries = db.listAudits({
domain: args?.domain,
limit: args?.limit ?? 50,
offset: args?.offset ?? 0,
});
// Serialize Date objects for IPC transport
return summaries.map((s) => ({
id: s.id,
auditId: s.auditId,
domain: s.domain,
projectName: s.projectName,
startUrl: s.startUrl,
overallScore: s.overallScore,
pagesAudited: s.pagesAudited,
passedCount: s.passedCount,
warningCount: s.warningCount,
failedCount: s.failedCount,
startedAt: s.startedAt instanceof Date ? s.startedAt.toISOString() : String(s.startedAt),
completedAt: s.completedAt instanceof Date ? s.completedAt.toISOString() : s.completedAt ? String(s.completedAt) : null,
status: s.status,
}));
},
);
ipcMain.handle(
IPC_CHANNELS.DB_GET_SCORE_TREND,
(_event, args: DbScoreTrendArgs): ScoreTrendPoint[] => {
const db = AuditsDatabase.getInstance();
const trend = db.getScoreTrend(args.domain, args.limit);
return trend.map((t) => ({
auditId: t.auditId,
score: t.score,
date: t.date instanceof Date ? t.date.toISOString() : String(t.date),
}));
},
);
ipcMain.handle(
IPC_CHANNELS.DB_GET_AUDITED_DOMAINS,
(): string[] => {
const db = AuditsDatabase.getInstance();
return db.getAuditedDomains();
},
);
ipcMain.handle(
IPC_CHANNELS.DB_GET_AUDIT_DETAIL,
(_event, auditId: string): AuditDetailIpc | null => {
const db = AuditsDatabase.getInstance();
const audit = db.getAudit(auditId);
if (!audit) return null;
// Fetch stored categories and results
const dbCategories = db.getCategories(audit.id);
const dbResults = db.getResults(audit.id);
// Group results by categoryId
const resultsByCategory = new Map<string, RuleResult[]>();
for (const r of dbResults) {
const list = resultsByCategory.get(r.categoryId) ?? [];
list.push({
ruleId: r.ruleId,
status: r.status as RuleResult['status'],
message: r.message,
score: r.score,
details: (r.details as Record<string, unknown>) ?? undefined,
});
resultsByCategory.set(r.categoryId, list);
}
// Reconstruct CategoryResult[] in the same order as stored categories
const categoryResults: CategoryResult[] = dbCategories.map((cat) => ({
categoryId: cat.categoryId,
score: cat.score,
passCount: cat.passCount,
warnCount: cat.warnCount,
failCount: cat.failCount,
results: resultsByCategory.get(cat.categoryId) ?? [],
}));
const result: AuditResult = {
url: audit.startUrl,
overallScore: audit.overallScore,
categoryResults,
timestamp: audit.startedAt instanceof Date
? audit.startedAt.toISOString()
: String(audit.startedAt),
crawledPages: audit.pagesAudited,
};
// Build rule metadata from results + registry
const ruleMetadata: Record<string, RuleMetadataIpc> = {};
for (const r of dbResults) {
if (!ruleMetadata[r.ruleId]) {
const rule = getRuleById(r.ruleId);
ruleMetadata[r.ruleId] = {
name: rule?.name ?? r.ruleName,
description: rule?.description ?? '',
};
}
}
return { result, ruleMetadata };
},
);
}
/**
* Electron Main Process Entry Point
*
* Creates the BrowserWindow and registers IPC handlers that bridge
* the existing Node.js audit engine to the React renderer.
*/
import { app, BrowserWindow } from 'electron';
import { join } from 'path';
import { registerAuditHandlers } from './audit-bridge.js';
import { registerDbHandlers } from './db-bridge.js';
let mainWindow: BrowserWindow | null = null;
function createWindow(): void {
mainWindow = new BrowserWindow({
width: 1280,
height: 820,
minWidth: 900,
minHeight: 600,
title: 'SEOmator',
titleBarStyle: 'hiddenInset',
trafficLightPosition: { x: 16, y: 16 },
webPreferences: {
preload: join(__dirname, '../preload/index.mjs'),
contextIsolation: true,
nodeIntegration: false,
sandbox: false, // Required for better-sqlite3 in preload chain
},
});
// In dev, load the Vite dev server; in production, load the built files
if (process.env.ELECTRON_RENDERER_URL) {
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL);
mainWindow.webContents.openDevTools();
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
}
mainWindow.on('closed', () => {
mainWindow = null;
});
}
// Register IPC handlers before window creation
app.whenReady().then(() => {
registerAuditHandlers(() => mainWindow);
registerDbHandlers();
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
/**
* Root application component — layout shell with header and page routing.
*/
import { useState } from 'react';
import { Header } from './components/Header.js';
import { AuditPage } from './pages/AuditPage.js';
import { HistoryPage } from './pages/HistoryPage.js';
import { useAuditStore } from './stores/audit-store.js';
export function App() {
const [activeView, setActiveView] = useState<'audit' | 'history'>('audit');
const { url, result } = useAuditStore();
return (
<div>
<Header
url={url}
crawledPages={result?.crawledPages}
activeView={activeView}
onViewChange={setActiveView}
/>
{activeView === 'audit' ? <AuditPage /> : <HistoryPage onNavigateToAudit={() => setActiveView('audit')} />}
</div>
);
}
/**
* Dropdown for selecting an audited domain from history.
*/
interface DomainPickerProps {
domains: string[];
selected: string | null;
onChange: (domain: string | null) => void;
}
export function DomainPicker({ domains, selected, onChange }: DomainPickerProps) {
return (
<select
value={selected ?? ''}
onChange={(e) => onChange(e.target.value || null)}
className="px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg-elevated)] text-sm focus:outline-none focus:border-[var(--color-accent)]"
style={{ color: 'var(--color-text)' }}
>
<option value="">All domains</option>
{domains.map((domain) => (
<option key={domain} value={domain}>
{domain}
</option>
))}
</select>
);
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SEOmator</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
export * from './schema.js';
export * from './defaults.js';
export * from './loader.js';
export * from './writer.js';
export * from './validator.js';
Related skills
How it compares
Use seo-audit for broad automated multi-category CLI scans; use content-focused SEO skills when only on-page copy optimization is needed.
FAQ
How many rules does seo-audit cover?
seo-audit uses SEOmator CLI to evaluate websites against 251 rules organized into 20 categories, returning LLM-optimized reports with per-category health scores for remediation.
What are the prerequisites for seo-audit?
seo-audit requires Node.js 18+ and npm to run SEOmator CLI; Chrome or Chromium is optional but enables Core Web Vitals and JavaScript rendering checks during the audit.
Is Seo Audit safe to install?
skills.sh reports 1 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.