Build an SEO Audit Tool Using JavaScript: Complete Guide

Santaji GadeJavaScript3 days ago13 Views

SEO Audit Tool

Build an SEO audit tool using JavaScript with five real DOM-based checks and a weighted scoring system — turn it into a one-click bookmarklet.

Technical SEO SEO Audit Tool JavaScript 2026

Building an SEO audit tool using JavaScript means writing rule-based checks against the DOM, title length, meta description, heading hierarchy, missing alt text, canonical presence, then scoring the results. It's genuinely simpler than it sounds. A handful of focused check functions and a scoring pass cover the checks that matter most for a real page.

01Building an SEO Audit Tool Using JavaScript: The Core Approach

One reference audit platform bundles well over 100 checks across technical, on-page, and structured-data categories. A useful DIY tool doesn't need all of them. Focus on the checks with the biggest, most reliably measurable impact: title and meta length, heading hierarchy, image alt coverage, and canonical presence.

Each check follows the same shape: query the relevant DOM elements, evaluate them against a known-good threshold, and return a structured result with a pass/fail status and a human-readable message.

50-60
characters, the recommended title tag length before search engines truncate it
150-160
characters, the safe range for meta descriptions before SERP truncation
5
core checks cover most of what a lightweight audit tool actually needs
Advertisement
Advertisement

02Checking Title and Meta Description

SiteGuru's on-page checker treats these two as foundational: does the page have a title and description, and are they within the length range search engines display without truncating.

Title and Meta Description Checks
function checkTitle() {
  const title = document.querySelector('title')?.textContent?.trim() || '';
  const length = title.length;

  if (!title) {
    return { check: 'Title Tag', status: 'fail', message: 'Missing title tag entirely' };
  }
  if (length < 30) {
    return { check: 'Title Tag', status: 'warn', message: `Title is only ${length} chars, likely too short` };
  }
  if (length > 60) {
    return { check: 'Title Tag', status: 'warn', message: `Title is ${length} chars, may get truncated` };
  }
  return { check: 'Title Tag', status: 'pass', message: `Title length OK (${length} chars)` };
}

function checkMetaDescription() {
  const meta = document.querySelector('meta[name="description"]');
  const content = meta?.getAttribute('content')?.trim() || '';
  const length = content.length;

  if (!content) {
    return { check: 'Meta Description', status: 'fail', message: 'Missing meta description' };
  }
  if (length < 70 || length > 160) {
    return { check: 'Meta Description', status: 'warn', message: `Description is ${length} chars, outside 70-160 range` };
  }
  return { check: 'Meta Description', status: 'pass', message: `Description length OK (${length} chars)` };
}
Advertisement
Advertisement

03Checking Heading Hierarchy

This connects directly to the rules covered in our heading structure best practices guide: one H1, and no skipped levels when opening a new section.

Heading Hierarchy Check
function checkHeadings() {
  const h1s = document.querySelectorAll('h1');
  const issues = [];

  if (h1s.length === 0) {
    issues.push('No H1 found on page');
  } else if (h1s.length > 1) {
    issues.push(`Multiple H1 tags found (${h1s.length})`);
  }

  // Check for skipped levels when opening a new section
  const allHeadings = [...document.querySelectorAll('h1, h2, h3, h4, h5, h6')];
  let lastLevel = 0;

  allHeadings.forEach(heading => {
    const level = parseInt(heading.tagName[1]);
    if (lastLevel > 0 && level > lastLevel + 1) {
      issues.push(`Skipped from H${lastLevel} to H${level}: "${heading.textContent.trim().slice(0, 40)}"`);
    }
    lastLevel = level;
  });

  return {
    check: 'Heading Structure',
    status: issues.length === 0 ? 'pass' : 'fail',
    message: issues.length === 0 ? 'Heading hierarchy looks clean' : issues.join('; ')
  };
}
Quick Tip

Watch out for skipped levels when closing a subsection, that's actually valid per the W3C. An H4 closing a deep subsection can validly be followed by an H2 opening a new one. The check above only flags a jump when opening, tightening this rule further would produce false positives on perfectly correct pages.

04Checking Images and Canonical Tag

Wellows' 2026 audit checklist flags missing alt text as one of the most common, easily fixable issues on real sites. The canonical check confirms exactly one canonical tag exists, echoing the failure mode covered in our canonical tag implementation guide.

Image Alt Text and Canonical Tag Checks
function checkImageAltText() {
  const images = [...document.querySelectorAll('img')];
  const missingAlt = images.filter(img => !img.getAttribute('alt'));

  return {
    check: 'Image Alt Text',
    status: missingAlt.length === 0 ? 'pass' : 'warn',
    message: missingAlt.length === 0
      ? `All ${images.length} images have alt text`
      : `${missingAlt.length} of ${images.length} images missing alt text`
  };
}

function checkCanonical() {
  const canonicals = document.querySelectorAll('link[rel="canonical"]');

  if (canonicals.length === 0) {
    return { check: 'Canonical Tag', status: 'fail', message: 'No canonical tag found' };
  }
  if (canonicals.length > 1) {
    return { check: 'Canonical Tag', status: 'fail', message: `${canonicals.length} canonical tags found, should be exactly 1` };
  }
  return { check: 'Canonical Tag', status: 'pass', message: 'Single canonical tag present' };
}
Advertisement
Advertisement

05Running All Checks and Scoring the Result

Combine every check into one runner function that returns a structured report with a numeric score, weighting a failed check more heavily than a warning.

Full Audit Runner With Scoring
function runSeoAudit() {
  const results = [
    checkTitle(),
    checkMetaDescription(),
    checkHeadings(),
    checkImageAltText(),
    checkCanonical()
  ];

  let score = 100;
  results.forEach(result => {
    if (result.status === 'fail') score -= 20;
    if (result.status === 'warn') score -= 10;
  });

  return {
    score: Math.max(0, score),
    results: results
  };
}

// Run it and log a readable report
const audit = runSeoAudit();
console.log(`SEO Score: ${audit.score}/100`);
console.table(audit.results);

06Turning It Into a Bookmarklet

Wrapping the audit in a self-executing function and minifying it into a bookmarklet lets you run it on any page with one click, without opening DevTools manually.

Bookmarklet Wrapper (Paste as a Bookmark URL)
javascript:(function(){
  // ...paste the checkTitle, checkMetaDescription, checkHeadings,
  // checkImageAltText, checkCanonical, and runSeoAudit functions here...
  const audit = runSeoAudit();
  let report = `SEO Score: ${audit.score}/100\n\n`;
  audit.results.forEach(r => {
    report += `${r.status.toUpperCase()}: ${r.check} — ${r.message}\n`;
  });
  alert(report);
})();

07Client-Side Checks vs Full Crawlers

A DIY DOM-based tool has real limits worth understanding before relying on it.

FactorDOM-Based JS Tool (this guide)Full Crawler (Screaming Frog, Ahrefs)
ScopeOne page at a time, current DOM onlyEntire site, hundreds of thousands of URLs
Setup costNone, runs in any browser consoleSoftware install or paid subscription
Server-side signalsCan't see HTTP headers, status codesFull access to headers, redirects, response codes
Best use caseQuick spot-checks, learning, CI integrationComprehensive audits, migrations, ongoing monitoring

08Extending the Tool

A short list of natural next checks to add once the basics are working.

Add an Open Graph tag check, confirming og:title, og:description, and og:image are all present.

Check for structured data, scanning for at least one script[type="application/ld+json"] block.

Verify hreflang consistency, on multilingual pages, for return tags on every language variant.

Wrap it as a Node.js script using a headless browser like Puppeteer for JavaScript-rendered pages and batch URLs.

Export results as JSON, making the tool usable in a CI pipeline as an automated regression check.

09Try It: Sample Audit Score Preview

Select realistic values to see how the scoring logic above would grade a page.

Sample Audit Score Preview

Select the option that matches your page

100%
Score reflects the exact logic in runSeoAudit() above.

10Common Questions

No. A DOM-based tool only sees the current rendered page, it can't check HTTP status codes, response headers, or crawl an entire site. It's best for quick spot-checks, not comprehensive site audits.

Per W3C guidance, skipping is only a problem when opening a new section, like jumping from H2 straight to H4. It's valid when closing a subsection, so a strict "never skip" rule would create false positives.

Not directly from a browser console due to CORS restrictions. For auditing other URLs, wrap the same check functions in a Node.js script using a headless browser like Puppeteer.

Save the combined script as a bookmarklet. Once set up, clicking the bookmark runs the full audit against whatever page you're currently viewing, with no DevTools required.

For learning, quick checks, or CI integration, yes. For comprehensive, site-wide audits covering hundreds of pages, HTTP-level signals, and ongoing monitoring, a dedicated crawler tool remains more practical.

What We Learn Today

Five focused checks cover most of what a lightweight audit needs

Each check returns a structured pass/warn/fail result

Heading checks should only flag skips when opening, not closing

A weighted score combines all checks into one number

Bookmarklets make the tool usable on any page instantly

DOM tools complement, but don't replace, full site crawlers

Build a Complete Technical SEO Toolkit

This audit tool pairs naturally with the heading structure and canonical guides its checks are based on. Explore both next.

0 Votes: 0 Upvotes, 0 Downvotes (0 Points)

Leave a reply

Loading Next Post...
Search
Popular Now
Loading

Signing-in 3 seconds...

Signing-up 3 seconds...