How to Add Custom Audit Checks to OpenSEO: A Complete Guide

To add custom audit checks to OpenSEO, implement a pure function that returns DetectedIssue objects and register it in either runPageReporters (for page-level analysis) or runMultipageChecks (for site-wide validation).

OpenSEO (every-app/open-seo) provides a flexible site auditing engine designed for extensibility. Whether you need to flag missing meta descriptions or detect structural patterns across your entire site, you can add custom audit checks to OpenSEO by hooking into its two-phase reporting architecture. The system automatically persists your custom issues to the database, making them immediately available in the UI, CSV exports, and API responses.

Understanding the Two-Phase Audit Architecture

OpenSEO executes site audits in two distinct phases, each designed for specific types of analysis:

Both phases feed an array of DetectedIssue objects into AuditRepository.insertIssues, which stores them in the audit_issues table.

The DetectedIssue Contract

Every custom check must return objects conforming to the DetectedIssue interface defined in src/server/lib/audit/issues/page-reporters.ts:

export interface DetectedIssue {
  /** Human-readable title for the issue (e.g. “Missing meta description”). */
  title: string;
  /** Detailed description shown in the UI. */
  description: string;
  /** Identifier used for filtering / CSV export. */
  type: string;
  /** Optional URL of the page the issue belongs to (for per-page checks). */
  url?: string;
}

The type field serves as the unique identifier for filtering and reporting, while url associates the issue with a specific page when applicable.

Adding Per-Page Custom Checks

Per-page checks analyze individual CrawledPageResult objects immediately after crawling. Follow these three steps to add a custom audit check to the per-page pipeline:

  1. Create a reporter function that receives a CrawledPageResult and returns DetectedIssue[].
  2. Export the function from a dedicated file (e.g., custom-page-reporters.ts).
  3. Register the reporter by invoking it inside runPageReporters in src/server/lib/audit/issues/page-reporters.ts.

Example: Detecting Missing Meta Descriptions

Create the reporter in src/server/lib/audit/issues/custom-page-reporters.ts:

import type { CrawledPageResult } from '@/server/lib/audit/types';
import type { DetectedIssue } from '@/server/lib/audit/issues/page-reporters';

export function missingMetaDescription(page: CrawledPageResult): DetectedIssue[] {
  const hasDescription = page.html?.match(/<meta\s+name=["']description["']\s+content=["'][^"']+["']\s*\/?>/i);
  if (!hasDescription) {
    return [
      {
        title: 'Missing meta description',
        description: 'The page does not contain a <meta name="description"> tag, which is important for click-through rate.',
        type: 'missing_meta_description',
        url: page.url,
      },
    ];
  }
  return [];
}

Then register it in src/server/lib/audit/issues/page-reporters.ts:

import { missingMetaDescription } from './custom-page-reporters';

export function runPageReporters(page: CrawledPageResult): DetectedIssue[] {
  const issues: DetectedIssue[] = [];

  // Existing reporters …
  issues.push(...missingMetaDescription(page));

  return issues;
}

The runPageReporters implementation processes each crawled page independently at line 42 of the source file.

Adding Site-Wide Custom Checks

Site-wide checks analyze patterns across the entire crawl, such as duplicate content or redirect chains. These checks operate on SlimPage objects in src/server/lib/audit/issues/multipage.ts:

  1. Create a helper function in src/server/lib/audit/issues/multipage-checks.ts that receives SlimPage[] and returns DetectedIssue[].
  2. Export the helper for use in the main runner.
  3. Invoke the helper inside runMultipageChecks in multipage.ts.

Example: Detecting Overly Long H1 Headings

Add the check logic in src/server/lib/audit/issues/multipage-checks.ts:

import type { SlimPage } from '@/server/lib/audit/issues/multipage-checks';
import type { DetectedIssue } from '@/server/lib/audit/issues/page-reporters';

export function longH1Headings(pages: SlimPage[]): DetectedIssue[] {
  const issues: DetectedIssue[] = [];

  pages.forEach(p => {
    const h1Match = p.html?.match(/<h1[^>]*>([^<]+)<\/h1>/i);
    if (h1Match && h1Match[1].trim().length > 70) {
      issues.push({
        title: 'Very long H1 heading',
        description: `The H1 on ${p.url} is ${h1Match[1].trim().length} characters long. Google recommends keeping headings concise.`,
        type: 'long_h1_heading',
        url: p.url,
      });
    }
  });

  return issues;
}

Then integrate it into src/server/lib/audit/issues/multipage.ts:

import { longH1Headings } from './multipage-checks';

export async function runMultipageChecks(input: { auditId: string }) {
  // …pages are fetched and transformed into `SlimPage[]` called `pages`…

  const issues: DetectedIssue[] = [];

  // Existing site-wide checks
  issues.push(...longH1Headings(pages));

  return issues;
}

runMultipageChecks is defined at line 20 of multipage.ts and executes after all per-page checks complete.

How Issues Are Persisted

Both per-page and multi-page reporters return DetectedIssue objects that flow into AuditRepository.insertIssues. This repository method stores issues in the audit_issues table without requiring additional database configuration.

In src/server/workflows/siteAuditWorkflowPhases.ts, the workflow orchestrates persistence:

await AuditRepository.insertIssues(auditId, issues);

This call occurs at line 356 after running both phases of checks. Because the database layer maps the type field directly to the audit_issues.type column, your custom issues automatically appear in the UI at /features/site-audit, CSV exports, and API responses.

Summary

  • OpenSEO uses two-phase auditing: Per-page checks inspect individual CrawledPageResult objects via runPageReporters, while site-wide checks analyze the full page list via runMultipageChecks.
  • Implement DetectedIssue objects: Return objects with title, description, type, and optional url properties from your custom functions.
  • Register in the appropriate runner: Add per-page reporters to src/server/lib/audit/issues/page-reporters.ts and multi-page helpers to src/server/lib/audit/issues/multipage.ts.
  • Automatic persistence: Issues automatically save to the audit_issues table via AuditRepository.insertIssues and appear in all export formats.

Frequently Asked Questions

Can I add multiple custom checks to a single audit?

Yes. You can register multiple reporter functions in runPageReporters or multiple helpers in runMultipageChecks. Simply push the results of each function into the issues array. The system aggregates all DetectedIssue objects before persisting them to the database.

What data is available in the CrawledPageResult object?

According to the OpenSEO source code, CrawledPageResult contains the full HTML content of the crawled page, the URL, HTTP status code, headers, and load time metrics. For site-wide checks, SlimPage provides a lightweight representation containing the URL and HTML content to minimize memory usage when processing large sites.

How do I ensure my custom issues appear in the CSV export?

The CSV export functionality filters and displays issues based on the type field in the audit_issues table. As long as your custom check returns a DetectedIssue with a unique type identifier (e.g., missing_meta_description), it will automatically appear in exports without additional configuration.

What is the difference between per-page and multi-page checks?

Per-page checks in page-reporters.ts analyze individual pages in isolation using CrawledPageResult, making them ideal for HTML validation issues like missing tags or improper heading structures. Multi-page checks in multipage.ts receive the complete array of SlimPage objects, enabling analysis of cross-page patterns like duplicate content, redirect chains, or orphaned pages that require comparing multiple URLs simultaneously.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →