Why Automate SEO Audits?

SEO audits are tedious. Checking meta titles, descriptions, Open Graph tags, and structured data across hundreds of pages takes hours manually. An automated tool can scan your entire site in minutes and flag issues before they hurt your rankings.

In this tutorial, we’ll build an SEO audit tool using the ToolCenter Metadata API to extract and analyze meta tags at scale.

What the Metadata API Returns

The ToolCenter Metadata API extracts comprehensive metadata from any URL:

curl -X GET "https://api.toolcenter.dev/v1/metadata?url=https://example.com" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response:

{
  "title": "Example Domain",
  "description": "This domain is for use in illustrative examples.",
  "ogTitle": "Example Domain",
  "ogDescription": "This domain is for use in illustrative examples.",
  "ogImage": "https://example.com/og-image.png",
  "ogType": "website",
  "twitterCard": "summary_large_image",
  "canonical": "https://example.com/",
  "favicon": "https://example.com/favicon.ico",
  "language": "en",
  "robots": "index, follow",
  "h1": ["Example Domain"],
  "links": { "internal": 5, "external": 2 }
}

Step 1: Define SEO Rules

Create a set of rules that define what “good” SEO looks like:

const SEO_RULES = {
  title: {
    required: true,
    minLength: 30,
    maxLength: 60,
    check: (val) => {
      const issues = [];
      if (!val) issues.push('Missing title tag');
      else {
        if (val.length < 30) issues.push(`Title too short (${val.length} chars, min 30)`);
        if (val.length > 60) issues.push(`Title too long (${val.length} chars, max 60)`);
      }
      return issues;
    },
  },
  description: {
    required: true,
    minLength: 120,
    maxLength: 160,
    check: (val) => {
      const issues = [];
      if (!val) issues.push('Missing meta description');
      else {
        if (val.length < 120) issues.push(`Description too short (${val.length} chars, min 120)`);
        if (val.length > 160) issues.push(`Description too long (${val.length} chars, max 160)`);
      }
      return issues;
    },
  },
  ogImage: {
    required: true,
    check: (val) => val ? [] : ['Missing og:image tag'],
  },
  ogTitle: {
    required: true,
    check: (val) => val ? [] : ['Missing og:title tag'],
  },
  canonical: {
    required: true,
    check: (val) => val ? [] : ['Missing canonical URL'],
  },
  h1: {
    check: (val) => {
      if (!val || val.length === 0) return ['Missing H1 tag'];
      if (val.length > 1) return [`Multiple H1 tags found (${val.length})`];
      return [];
    },
  },
};

Step 2: Build the Audit Function

const axios = require('axios');

async function auditUrl(url) {
  const response = await axios.get(
    'https://api.toolcenter.dev/v1/metadata',
    {
      params: { url },
      headers: { 'Authorization': 'Bearer YOUR_API_KEY' },
    }
  );

  const metadata = response.data;
  const issues = [];
  let score = 100;

  for (const [field, rule] of Object.entries(SEO_RULES)) {
    const fieldIssues = rule.check(metadata[field]);
    if (fieldIssues.length > 0) {
      issues.push(...fieldIssues);
      score -= fieldIssues.length * 10;
    }
  }

  return {
    url,
    score: Math.max(0, score),
    issues,
    metadata,
    timestamp: new Date().toISOString(),
  };
}

Step 3: Crawl and Audit Multiple Pages

Python Implementation

import requests
from urllib.parse import urljoin, urlparse
from collections import deque

API_KEY = 'YOUR_API_KEY'
BASE_URL = 'https://api.toolcenter.dev/v1'

def extract_metadata(url):
    """Extract metadata from a single URL."""
    response = requests.get(
        f'{BASE_URL}/metadata',
        params={'url': url},
        headers={'Authorization': f'Bearer {API_KEY}'}
    )
    return response.json()

def audit_page(url):
    """Audit a single page for SEO issues."""
    meta = extract_metadata(url)
    issues = []

    # Title checks
    title = meta.get('title', '')
    if not title:
        issues.append({'severity': 'error', 'message': 'Missing title tag'})
    elif len(title) < 30:
        issues.append({'severity': 'warning', 'message': f'Title too short ({len(title)} chars)'})
    elif len(title) > 60:
        issues.append({'severity': 'warning', 'message': f'Title too long ({len(title)} chars)'})

    # Description checks
    desc = meta.get('description', '')
    if not desc:
        issues.append({'severity': 'error', 'message': 'Missing meta description'})
    elif len(desc) < 120:
        issues.append({'severity': 'warning', 'message': f'Description too short ({len(desc)} chars)'})
    elif len(desc) > 160:
        issues.append({'severity': 'warning', 'message': f'Description too long ({len(desc)} chars)'})

    # OG tags
    if not meta.get('ogImage'):
        issues.append({'severity': 'warning', 'message': 'Missing og:image'})
    if not meta.get('ogTitle'):
        issues.append({'severity': 'warning', 'message': 'Missing og:title'})

    # Canonical
    if not meta.get('canonical'):
        issues.append({'severity': 'error', 'message': 'Missing canonical URL'})

    # H1
    h1_tags = meta.get('h1', [])
    if len(h1_tags) == 0:
        issues.append({'severity': 'error', 'message': 'Missing H1 tag'})
    elif len(h1_tags) > 1:
        issues.append({'severity': 'warning', 'message': f'Multiple H1 tags ({len(h1_tags)})'})

    # Calculate score
    error_count = sum(1 for i in issues if i['severity'] == 'error')
    warning_count = sum(1 for i in issues if i['severity'] == 'warning')
    score = max(0, 100 - (error_count * 15) - (warning_count * 5))

    return {
        'url': url,
        'score': score,
        'issues': issues,
        'metadata': meta,
    }

def audit_site(start_url, max_pages=50):
    """Crawl and audit an entire site."""
    domain = urlparse(start_url).netloc
    visited = set()
    queue = deque([start_url])
    results = []

    while queue and len(visited) < max_pages:
        url = queue.popleft()
        if url in visited:
            continue
        visited.add(url)

        print(f'Auditing: {url}')
        result = audit_page(url)
        results.append(result)

    return results

Step 4: Generate the Audit Report

function generateReport(results) {
  const totalPages = results.length;
  const avgScore = results.reduce((sum, r) => sum + r.score, 0) / totalPages;
  const allIssues = results.flatMap(r => r.issues);

  const issueFrequency = {};
  for (const issue of allIssues) {
    issueFrequency[issue] = (issueFrequency[issue] || 0) + 1;
  }

  // Sort by frequency
  const topIssues = Object.entries(issueFrequency)
    .sort((a, b) => b[1] - a[1])
    .slice(0, 10);

  return {
    summary: {
      totalPages,
      averageScore: avgScore.toFixed(1),
      totalIssues: allIssues.length,
      pagesWithErrors: results.filter(r => r.score < 70).length,
    },
    topIssues: topIssues.map(([issue, count]) => ({ issue, count, percentage: ((count / totalPages) * 100).toFixed(1) })),
    worstPages: results.sort((a, b) => a.score - b.score).slice(0, 10),
    bestPages: results.sort((a, b) => b.score - a.score).slice(0, 5),
  };
}

Step 5: Export as PDF

Turn your audit into a professional PDF report:

async function exportAuditPDF(report) {
  const html = `
    <html><head><style>
      body { font-family: system-ui; padding: 40px; color: #333; }
      h1 { color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 10px; }
      .score { font-size: 48px; font-weight: bold; color: ${report.summary.averageScore >= 80 ? '#27ae60' : report.summary.averageScore >= 60 ? '#f39c12' : '#e74c3c'}; }
      .stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; margin: 20px 0; }
      .stat { background: #f8f9fa; padding: 15px; border-radius: 8px; text-align: center; }
      table { width: 100%; border-collapse: collapse; margin: 20px 0; }
      th { background: #3498db; color: white; padding: 10px; text-align: left; }
      td { padding: 8px 10px; border-bottom: 1px solid #eee; }
    </style></head><body>
      <h1>SEO Audit Report</h1>
      <div class="score">${report.summary.averageScore}/100</div>
      <div class="stats">
        <div class="stat"><strong>${report.summary.totalPages}</strong><br>Pages Scanned</div>
        <div class="stat"><strong>${report.summary.totalIssues}</strong><br>Issues Found</div>
        <div class="stat"><strong>${report.summary.pagesWithErrors}</strong><br>Pages with Errors</div>
      </div>
      <h2>Top Issues</h2>
      <table>
        <tr><th>Issue</th><th>Affected Pages</th><th>%</th></tr>
        ${report.topIssues.map(i => `<tr><td>${i.issue}</td><td>${i.count}</td><td>${i.percentage}%</td></tr>`).join('')}
      </table>
    </body></html>
  `;

  const response = await axios.post(
    'https://api.toolcenter.dev/v1/pdf',
    { html, format: 'A4', printBackground: true },
    { headers: { 'Authorization': 'Bearer YOUR_API_KEY' }, responseType: 'arraybuffer' }
  );

  return response.data;
}

Scheduling Regular Audits

const cron = require('node-cron');

// Weekly SEO audit every Monday at 8 AM
cron.schedule('0 8 * * 1', async () => {
  const results = await auditSite('https://yoursite.com', 100);
  const report = generateReport(results);
  const pdf = await exportAuditPDF(report);

  await sendEmail({
    to: '[email protected]',
    subject: `Weekly SEO Audit — Score: ${report.summary.averageScore}`,
    attachments: [{ filename: 'seo-audit.pdf', content: pdf }],
  });
});

Conclusion

Automated SEO audits catch issues before they affect your rankings. The ToolCenter Metadata API extracts everything you need — title, description, OG tags, structured data — from any URL with a single request. Combine it with a crawling strategy and a scoring system, and you have a powerful SEO monitoring tool that runs on autopilot.