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.

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.
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:
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.
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
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.
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.
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 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}
))}
);
};
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.
Math.random(). The right solution is integrating the Google PageSpeed Insights API, which is free and returns Lighthouse scores for any public URL.localStorage or a lightweight database so users can track how a site's score changes over time.twitter:card and twitter:image meta tags.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