SEO Audit Engine
A client-side SEO analyzer that audits any public URL across 15+ metrics — meta tags, heading hierarchy, Open Graph, SSL, content length, and more — with a one-click PDF export. No backend, no login, instant results.

Introduction
Most SEO tools are either too expensive, too slow, or locked behind a login. I wanted something instant — paste a URL, get a real audit in seconds. So I built SEO Audit Engine: a client-side SEO analyzer that fetches any public webpage and runs a full multi-category audit right in the browser, no backend required.
Here's how I architected it, what problems I ran into, and why I made each technical decision.
What it does
SEO Audit Engine crawls a target URL (via a CORS proxy), parses the raw HTML in-browser using the native DOMParser, and evaluates it across three metric categories:
- Basic On-Page Metrics — title tag length, meta description, H1 tags, heading hierarchy, canonical tag, robots meta, language attribute, favicon, and Open Graph tags.
- HTTP & Technical Metrics — robots.txt presence, sitemap.xml presence, SSL certificate (HTTPS check), page speed score, and mobile-friendliness.
- Advanced SEO Metrics — content word count, image alt text coverage, internal link count, external link count, and keyword presence in URL.
Every metric returns one of four statuses: good, warning, error, or info — each with a current value and a human-readable recommendation. The results render into expandable metric cards grouped by category, with an overall score calculated as the ratio of passed metrics to total metrics.
Tech stack
- React 18 + TypeScript — component-based UI with strict typing throughout.
- Vite — fast dev server and build toolchain.
- Tailwind CSS — utility-first styling for rapid, consistent UI.
- jsPDF — client-side PDF generation for downloadable audit reports.
- Lucide React — icon set for status indicators and UI controls.
- CORS Proxy (allorigins.win) — server-side proxy to bypass browser CORS restrictions when fetching third-party URLs.
Project structure
The codebase is split into four layers: components, services, types, and entry point. The service layer does all the heavy lifting; the components are purely presentational.
src/
├── components/
│ ├── SEOAnalyzer.tsx ← orchestrates state and analysis flow
│ ├── URLInput.tsx ← URL entry with auto-protocol formatting
│ ├── AnalysisResults.tsx ← score summary + PDF export
│ ├── MetricCard.tsx ← expandable card with status, value, recommendation
│ ├── LoadingState.tsx ← animated loading UI
│ └── Layout.tsx ← header, footer, page shell
├── services/
│ ├── seoAnalyzer.ts ← all SEO analysis logic
│ └── corsProxy.ts ← proxy fetching with fallback
└── types/
└── seo.ts ← SEOMetric, SEOData, SEOMetricCategory types
How the analysis works
The core flow is straightforward: fetch HTML → parse DOM → run metric functions → return structured data.
export const analyzeSEO = async (url: string): Promise => {
const html = await fetchHTML(url);
const doc = new DOMParser().parseFromString(html, 'text/html');
return {
[SEOMetricCategory.STATIC]: await analyzeStaticMetrics(doc, url),
[SEOMetricCategory.HTTP]: await analyzeHttpMetrics(url),
[SEOMetricCategory.ADVANCED]: await analyzeAdvancedMetrics(doc, url),
};
};
Each category runs independently. Static metrics are synchronous DOM queries. HTTP metrics make additional proxy requests (robots.txt, sitemap.xml). Advanced metrics combine DOM analysis with URL parsing. All three return the same SEOMetric[] shape, so the UI renders them identically.
The CORS problem — and how I solved it
Browsers block cross-origin fetches by default. You can't just fetch('https://somesite.com') from a React app — the browser will reject the response unless that site explicitly allows it via CORS headers. Most sites don't.
My solution was a proxy service layer that routes requests through allorigins.win, a public CORS proxy that forwards the response with the appropriate headers. I also built in automatic failover to a secondary proxy (cors-anywhere.herokuapp.com) in case the primary fails:
const CORS_PROXIES = [
'https://api.allorigins.win/raw?url=',
'https://cors-anywhere.herokuapp.com/'
];
export const fetchWithProxy = async (url: string): Promise => {
const proxiedUrl = `${CORS_PROXIES[currentProxyIndex]}${encodeURIComponent(url)}`;
try {
const response = await fetch(proxiedUrl);
if (!response.ok) {
currentProxyIndex = (currentProxyIndex + 1) % CORS_PROXIES.length;
return fetchWithProxy(url);
}
return response;
} catch (error) {
currentProxyIndex = (currentProxyIndex + 1) % CORS_PROXIES.length;
if (currentProxyIndex === 0) throw new Error('Failed to fetch URL through any proxy');
return fetchWithProxy(url);
}
};
The trade-off is that this approach depends on third-party proxies being available. For a production version, I'd replace this with a lightweight serverless function (a Vercel Edge Function or Cloudflare Worker) that acts as the proxy — giving full control over availability, rate limiting, and caching.
Metric scoring logic
Each metric evaluation follows a consistent pattern: extract a value from the DOM, apply a scoring rule, and attach a recommendation. Here's the title tag evaluator as an example:
const evaluateTitleTag = (title: string): SEOMetricStatus => {
if (!title) return 'error';
if (title.length < 10 || title.length > 60) return 'warning';
return 'good';
};
More complex metrics like heading hierarchy check structural relationships between tag levels. The Open Graph check evaluates partial vs. complete tag sets. Image alt text computes a percentage coverage score. Every metric produces a value (what the tool found) and a recommendation (what to do about it) — keeping the output actionable, not just diagnostic.
The MetricCard component
The most interesting component in the project is MetricCard. It's not just a status display — it adapts its expanded content based on the metric type. For heading structure metrics, it renders a horizontal bar chart. For image alt text, it renders a coverage progress bar. For favicon and Open Graph tags, it attempts to render the actual image.
const renderHeadingStructure = (value: string) => {
const headings = value.split(',').map(h => {
const [type, count] = h.trim().split(':');
return { type, count: parseInt(count) };
});
return (
{headings.map(({ type, count }) => (
{type}
{count}
))}
);
};
PDF export
The export feature generates a full audit report as a downloadable PDF using jsPDF, entirely in the browser — no server involved. It iterates over all categories and metrics, handles automatic page breaks when content overflows, and names the file after the domain being audited:
const domain = url
.replace(/^https?:\/\//, '')
.replace(/\/.*$/, '')
.replace(/[^a-zA-Z0-9.-]/g, '-');
doc.save(`seo-analysis-${domain}-${new Date().toISOString().split('T')[0]}.pdf`);
This is one of my favorite features because it makes the tool genuinely useful for agencies and freelancers who need to hand off audit reports to clients without any additional tooling.
What I'd improve
- Real page speed data — currently simulated with
Math.random(). The right solution is integrating the Google PageSpeed Insights API, which is free and returns Lighthouse scores for any public URL. - Own proxy server — replacing the public CORS proxies with a Vercel Edge Function would eliminate the dependency on third-party uptime and add request caching.
- Historical tracking — storing past audits in
localStorageor a lightweight database so users can track how a site's score changes over time. - Structured data validation — checking for JSON-LD schema markup, which is increasingly important for Google's rich results.
- Twitter Card tags — the current social tag analysis covers Open Graph but not Twitter's
twitter:cardandtwitter:imagemeta tags.
Conclusion
SEO Audit Engine taught me a lot about browser security constraints, DOM parsing edge cases, and building tools that are actually useful rather than just technically interesting. The CORS proxy architecture is a real limitation at scale, but for a zero-backend, instant-results tool it works well. If you're building something similar, start with DOMParser — it's more capable than most developers realize, and you can get surprisingly deep analysis purely client-side.
// Tech Stack