seo_audit
Check 18 SEO signals including meta tags, headings, OG/Twitter cards, structured data, and alt text. Get a 0-100 score with specific recommendations for each failing check.
Instructions
Run a comprehensive SEO audit. Checks 18 SEO signals including meta tags, heading hierarchy, Open Graph tags, Twitter cards, structured data (JSON-LD), canonical URLs, image alt text, and more. Returns a 0-100 score and specific recommendations for each failing check.
Use this when the user wants to check their page's SEO health, improve search engine visibility, or ensure proper social sharing metadata.
This tool is FREE — runs entirely within Claude Code.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| url | Yes | URL of the page to audit (e.g., http://localhost:3000) |
Implementation Reference
- src/tools/seo.ts:45-166 (handler)The main handler function 'runSeoAudit' that performs the comprehensive SEO audit. Opens a Puppeteer page, evaluates DOM for SEO signals (meta tags, headings, OG, structured data, etc.), builds checks, and returns a SeoAuditResult with score.
export async function runSeoAudit(url: string): Promise<SeoAuditResult> { const page = await createPage(1440, 900); try { await page.goto(url, { waitUntil: "networkidle2", timeout: 30000 }); const pageData = await page.evaluate(() => { const getMeta = (name: string): string | null => { const el = document.querySelector(`meta[name="${name}"]`) ?? document.querySelector(`meta[property="${name}"]`); return el?.getAttribute("content") ?? null; }; const getLink = (rel: string): string | null => { const el = document.querySelector(`link[rel="${rel}"]`); return el?.getAttribute("href") ?? null; }; // Collect headings const headings: { tag: string; text: string }[] = []; document.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((el) => { headings.push({ tag: el.tagName.toLowerCase(), text: (el.textContent ?? "").trim().slice(0, 100), }); }); // Collect images without alt const images = document.querySelectorAll("img"); let imagesTotal = 0; let imagesMissingAlt = 0; images.forEach((img) => { imagesTotal++; if (!img.getAttribute("alt") && !img.getAttribute("aria-label")) { imagesMissingAlt++; } }); // Collect links const links = document.querySelectorAll("a[href]"); let linksTotal = 0; let linksNoText = 0; links.forEach((link) => { linksTotal++; const text = (link.textContent ?? "").trim(); const ariaLabel = link.getAttribute("aria-label"); const title = link.getAttribute("title"); if (!text && !ariaLabel && !title) { linksNoText++; } }); // Check for structured data const jsonLdScripts = document.querySelectorAll( 'script[type="application/ld+json"]' ); const structuredDataBlocks: string[] = []; jsonLdScripts.forEach((script) => { const content = (script.textContent ?? "").trim(); if (content) { structuredDataBlocks.push(content.slice(0, 200)); } }); return { title: document.title, metaDescription: getMeta("description"), metaViewport: document .querySelector('meta[name="viewport"]') ?.getAttribute("content") ?? null, metaRobots: getMeta("robots"), canonical: getLink("canonical"), lang: document.documentElement.getAttribute("lang"), charset: document.querySelector("meta[charset]")?.getAttribute("charset") ?? null, // Open Graph ogTitle: getMeta("og:title"), ogDescription: getMeta("og:description"), ogImage: getMeta("og:image"), ogType: getMeta("og:type"), ogUrl: getMeta("og:url"), // Twitter Card twitterCard: getMeta("twitter:card"), twitterTitle: getMeta("twitter:title"), twitterDescription: getMeta("twitter:description"), // Content headings, h1Count: headings.filter((h) => h.tag === "h1").length, imagesTotal, imagesMissingAlt, linksTotal, linksNoText, structuredDataBlocks, hasStructuredData: jsonLdScripts.length > 0, }; }); const checks = buildSeoChecks(pageData, url); const passed = checks.filter((c) => c.passed).length; const failed = checks.filter((c) => !c.passed).length; const score = computeSeoScore(checks); const summary = buildSeoSummary(passed, failed, score); return { url, timestamp: new Date().toISOString(), checks, passed, failed, score, summary, }; } finally { await closePage(page); } } - src/server.ts:857-889 (registration)Registration of the 'seo_audit' MCP tool in server.ts using server.tool(). Defines the tool name, description, input schema (url), and handler that calls runSeoAudit() and formatSeoReport().
server.tool( "seo_audit", `Run a comprehensive SEO audit. Checks 18 SEO signals including meta tags, heading hierarchy, Open Graph tags, Twitter cards, structured data (JSON-LD), canonical URLs, image alt text, and more. Returns a 0-100 score and specific recommendations for each failing check. Use this when the user wants to check their page's SEO health, improve search engine visibility, or ensure proper social sharing metadata. This tool is FREE — runs entirely within Claude Code.`, { url: z.string().url().describe("URL of the page to audit (e.g., http://localhost:3000)"), }, async ({ url }) => { try { const result = await runSeoAudit(url); const report = formatSeoReport(result); return { content: [ { type: "text" as const, text: report }, { type: "text" as const, text: `\n\n<raw_data>\n${JSON.stringify(result, null, 2)}\n</raw_data>`, }, ], }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { content: [{ type: "text" as const, text: `SEO audit failed: ${message}` }], isError: true, }; } } ); - src/tools/seo.ts:11-28 (schema)Type definitions: SeoCheckResult interface (id, title, passed, value, recommendation, impact) and SeoAuditResult interface (url, timestamp, checks, passed, failed, score, summary).
export interface SeoCheckResult { readonly id: string; readonly title: string; readonly passed: boolean; readonly value: string | null; readonly recommendation: string; readonly impact: "critical" | "high" | "medium" | "low"; } export interface SeoAuditResult { readonly url: string; readonly timestamp: string; readonly checks: readonly SeoCheckResult[]; readonly passed: number; readonly failed: number; readonly score: number; readonly summary: string; } - src/tools/seo.ts:595-637 (helper)The 'formatSeoReport' function that formats the SEO audit result as a readable markdown report, grouping issues by impact (critical, high, medium, low) and listing passing checks.
export function formatSeoReport(result: SeoAuditResult): string { const sections: string[] = [ `## SEO Audit Results`, ``, `**URL:** ${result.url}`, `**Score:** ${result.score}/100`, `**Passed:** ${result.passed} | **Failed:** ${result.failed}`, ``, ]; const failed = result.checks.filter((c) => !c.passed); const passed = result.checks.filter((c) => c.passed); if (failed.length > 0) { sections.push(`### Issues Found`); sections.push(``); // Group by impact const byImpact = groupByImpact(failed); for (const [impact, checks] of byImpact) { sections.push(`#### ${impact.toUpperCase()}`); for (const check of checks) { sections.push(`- **${check.title}**: ${check.recommendation}`); if (check.value) { sections.push(` Current: ${check.value}`); } } sections.push(``); } } if (passed.length > 0) { sections.push(`### Passing Checks`); sections.push(``); for (const check of passed) { const detail = check.value ? ` (${check.value})` : ""; sections.push(`- ${check.title}${detail}`); } sections.push(``); } return sections.join("\n"); } - src/tools/seo.ts:639-653 (helper)Helper function 'groupByImpact' that sorts SEO check results by severity order (critical > high > medium > low) for the report.
function groupByImpact( checks: readonly SeoCheckResult[] ): readonly [string, readonly SeoCheckResult[]][] { const order = ["critical", "high", "medium", "low"] as const; const grouped = new Map<string, SeoCheckResult[]>(); for (const check of checks) { const existing = grouped.get(check.impact) ?? []; grouped.set(check.impact, [...existing, check]); } return order .filter((impact) => grouped.has(impact)) .map((impact) => [impact, grouped.get(impact) ?? []] as const); }