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:
- Per-page checks – Located in
src/server/lib/audit/issues/page-reporters.ts, therunPageReportersfunction executes pure functions against individual crawled pages (CrawledPageResult). - Multi-page (site-wide) checks – Located in
src/server/lib/audit/issues/multipage.ts, therunMultipageChecksfunction analyzes the complete set of pages using helpers fromsrc/server/lib/audit/issues/multipage-checks.ts.
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:
- Create a reporter function that receives a
CrawledPageResultand returnsDetectedIssue[]. - Export the function from a dedicated file (e.g.,
custom-page-reporters.ts). - Register the reporter by invoking it inside
runPageReportersinsrc/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:
- Create a helper function in
src/server/lib/audit/issues/multipage-checks.tsthat receivesSlimPage[]and returnsDetectedIssue[]. - Export the helper for use in the main runner.
- Invoke the helper inside
runMultipageChecksinmultipage.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
CrawledPageResultobjects viarunPageReporters, while site-wide checks analyze the full page list viarunMultipageChecks. - Implement
DetectedIssueobjects: Return objects withtitle,description,type, and optionalurlproperties from your custom functions. - Register in the appropriate runner: Add per-page reporters to
src/server/lib/audit/issues/page-reporters.tsand multi-page helpers tosrc/server/lib/audit/issues/multipage.ts. - Automatic persistence: Issues automatically save to the
audit_issuestable viaAuditRepository.insertIssuesand 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →