How to Add Custom Page Analysis Checks to the OpenSEO Site Audit System
You can add custom page analysis checks to OpenSEO by implementing either per-page reporters in page-reporters.ts for single-page inspections or multipage checks in multipage-checks.ts for cross-page analysis, then registering them in the respective orchestrators.
The OpenSEO site audit system uses a modular, two-stage architecture that makes it straightforward to extend with custom validation logic. Whether you need to flag missing hreflang attributes on individual pages or detect duplicate titles across your entire site, the codebase provides clear extension points for both scenarios. This guide walks through the exact file locations and function signatures required to add custom page analysis checks to the site audit system.
Understanding the OpenSEO Audit Architecture
The audit engine processes crawled data in two distinct phases:
-
Per-page reporters – Pure functions that inspect a single
CrawledPageResultobject and emitDetectedIssueobjects. These run immediately as each page is crawled viarunPageReporters()in [src/server/lib/audit/issues/page-reporters.ts](https://github.com/every-app/open-seo/blob/main/src/server/lib/audit/issues/page-reporters.ts). -
Cross-page (multipage) checks – Functions that receive an array of
SlimPageobjects after the crawl finishes. These are implemented in [src/server/lib/audit/issues/multipage-checks.ts](https://github.com/every-app/open-seo/blob/main/src/server/lib/audit/issues/multipage-checks.ts) and orchestrated from [src/server/lib/audit/issues/multipage.ts](https://github.com/every-app/open-seo/blob/main/src/server/lib/audit/issues/multipage.ts).
Both stages produce DetectedIssue objects that are persisted to the audit database by [src/server/workflows/siteAuditWorkflowCrawl.ts](https://github.com/every-app/open-seo/blob/main/src/server/workflows/siteAuditWorkflowCrawl.ts).
Adding a Per-Page Analysis Check
Per-page checks are ideal for validating HTML content, metadata, or response headers on individual URLs.
Register the Issue Type
First, extend the AUDIT_ISSUE_TYPES map in [src/shared/audit-issues.ts](https://github.com/every-app/open-seo/blob/main/src/shared/audit-issues.ts) to define your check's metadata:
// src/shared/audit-issues.ts
export const AUDIT_ISSUE_TYPES = {
// ... existing types
"missing-hreflang": {
severity: "warning",
title: "Missing Hreflang Attribute",
explanation: "The page lacks hreflang annotations for international targeting.",
howToFix: "Add <link rel=\"alternate\" hreflang=\"x\" href=\"...\" /> tags.",
},
} as const;
Implement the Reporter Function
Create a pure function that accepts a CrawledPageResult and returns a DetectedIssue or null:
// src/server/lib/audit/issues/page-reporters.ts
function checkMissingHreflang(page: CrawledPageResult): DetectedIssue | null {
if (page.isHtml && !page.hreflang?.length) {
return {
issueType: "missing-hreflang",
pageId: page.id,
pageUrl: page.url,
details: { reason: "No <link rel=\"alternate\" hreflang> found" },
};
}
return null;
}
Integrate into the Pipeline
Hook your function into runPageReporters() (around line 42) to include it in the audit flow:
// src/server/lib/audit/issues/page-reporters.ts
export function runPageReporters(page: CrawledPageResult): DetectedIssue[] {
const issues: DetectedIssue[] = [];
// ... existing checks
const issue = checkMissingHreflang(page);
if (issue) issues.push(issue);
return issues;
}
Write Tests
Add test coverage in [src/server/lib/audit/issues/page-reporters.test.ts](https://github.com/every-app/open-seo/blob/main/src/server/lib/audit/issues/page-reporters.test.ts) to verify your check detects the condition correctly:
// src/server/lib/audit/issues/page-reporters.test.ts
it("should detect missing hreflang", () => {
const page = createMockPage({ isHtml: true, hreflang: [] });
const issues = runPageReporters(page);
expect(issues).toContainEqual(
expect.objectContaining({ issueType: "missing-hreflang" })
);
});
Adding a Cross-Page (Multipage) Check
Multipage checks are designed for site-wide analysis like finding duplicate titles or redirect chains.
Define the Issue Type
As with per-page checks, add your issue type to AUDIT_ISSUE_TYPES in [src/shared/audit-issues.ts](https://github.com/every-app/open-seo/blob/main/src/shared/audit-issues.ts):
// src/shared/audit-issues.ts
"excessive-word-count": {
severity: "info",
title: "Excessive Word Count",
explanation: "Page contains unusually high word count.",
howToFix: "Consider splitting content into multiple pages.",
},
Create the Check Function
Implement a function that analyzes the full array of SlimPage objects:
// src/server/lib/audit/issues/multipage-checks.ts
export function findHugePages(pages: SlimPage[]): DetectedIssue[] {
const issues: DetectedIssue[] = [];
for (const p of pages) {
if (p.wordCount > 10_000) {
issues.push({
issueType: "excessive-word-count",
pageId: p.id,
pageUrl: p.url,
details: { wordCount: p.wordCount },
});
}
}
return issues;
}
Wire into the Multipage Runner
Import and invoke your function in [src/server/lib/audit/issues/multipage.ts](https://github.com/every-app/open-seo/blob/main/src/server/lib/audit/issues/multipage.ts):
// src/server/lib/audit/issues/multipage.ts
import { findHugePages } from "./multipage-checks";
export async function runMultipageChecks(pages: SlimPage[]): Promise<DetectedIssue[]> {
const issues = [
...findDuplicates(pages),
...findRedirectChainsAndLoops(pages),
...findHugePages(pages), // ← new check added here
];
return issues;
}
The runMultipageChecks function is called after the crawl completes. The returned issues are automatically persisted by AuditRepository.insertCrawledBatch() via the workflow in [src/server/workflows/siteAuditWorkflowCrawl.ts](https://github.com/every-app/open-seo/blob/main/src/server/workflows/siteAuditWorkflowCrawl.ts).
Testing Cross-Page Logic
Add unit tests in [src/server/lib/audit/issues/multipage-checks.test.ts](https://github.com/every-app/open-seo/blob/main/src/server/lib/audit/issues/multipage-checks.test.ts):
// src/server/lib/audit/issues/multipage-checks.test.ts
it("should flag pages with excessive word count", () => {
const pages = [
{ id: "1", url: "/long-post", wordCount: 15000 },
{ id: "2", url: "/short-post", wordCount: 500 },
];
const issues = findHugePages(pages);
expect(issues).toHaveLength(1);
expect(issues[0].pageId).toBe("1");
});
Summary
- Per-page reporters validate individual
CrawledPageResultobjects during the crawl phase viarunPageReporters()inpage-reporters.ts. - Multipage checks analyze arrays of
SlimPageobjects after crawling completes viarunMultipageChecks()inmultipage.ts. - Issue registration requires adding entries to
AUDIT_ISSUE_TYPESinsrc/shared/audit-issues.tsfor proper metadata and severity levels. - Persistence is handled automatically by the site audit workflow; you only need to return
DetectedIssueobjects from your functions. - Testing should cover both positive detection cases and false-negative scenarios in the corresponding
*.test.tsfiles.
Frequently Asked Questions
What is the difference between per-page and multipage checks?
Per-page checks inspect a single page's content in isolation, such as validating meta descriptions or checking for specific HTML tags. Multipage checks analyze relationships across the entire site, such as detecting duplicate titles or redirect chains that span multiple URLs. Per-page checks run during the crawl via runPageReporters(), while multipage checks execute after the crawl finishes via runMultipageChecks().
How do I register a new issue type in OpenSEO?
Register new issue types by extending the AUDIT_ISSUE_TYPES constant object in [src/shared/audit-issues.ts](https://github.com/every-app/open-seo/blob/main/src/shared/audit-issues.ts). Each entry requires a unique key, severity level ("error", "warning", or "info"), title, explanation, and remediation instructions. This registry enables the dashboard to display human-readable descriptions of your custom checks.
Where are audit issues stored after detection?
Audit issues are persisted to the database by [src/server/workflows/siteAuditWorkflowCrawl.ts](https://github.com/every-app/open-seo/blob/main/src/server/workflows/siteAuditWorkflowCrawl.ts), which calls runPageReporters() for each crawled page and later aggregates multipage check results. The issues are inserted via AuditRepository.insertCrawledBatch(); no manual database code is required when adding new checks.
Can I access the full page HTML in multipage checks?
No. Multipage checks receive SlimPage objects, which contain lightweight metadata such as URL, title, word count, and status code, but not the full HTML content. If your check requires analyzing HTML structure, body content, or headers, implement it as a per-page reporter that receives the complete CrawledPageResult object containing the raw response body and parsed DOM.
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 →