How OpenSEO Lighthouse Integration Works: A Complete Technical Guide
OpenSEO embeds Lighthouse audits directly into its site-audit workflow through a three-layer integration: workflow orchestration, DataforSEO API execution with Cloudflare R2 storage, and server-side API endpoints that expose trimmed data to the UI.
The OpenSEO project (every-app/open-seo) implements a server-side-only integration with Google's Lighthouse auditing tool. Rather than running Lighthouse locally, the system leverages the DataforSEO Lighthouse API, stores raw JSON payloads in Cloudflare R2 object storage, and serves filtered results through TanStack Start server functions. This architecture ensures the UI never contacts third-party APIs directly while maintaining efficient access to performance metrics.
Architecture Overview
The OpenSEO Lighthouse integration consists of three distinct layers that handle the complete audit lifecycle:
- Workflow orchestration – The audit runner decides if and when Lighthouse executes based on configuration flags.
- Lighthouse execution and storage – A thin wrapper calls the DataforSEO Lighthouse API, persists the raw JSON payload to Cloudflare R2, and returns lightweight metadata.
- Server-side API – Two server functions expose stored Lighthouse data through endpoints for issues viewing and CSV/JSON export.
Step-by-Step Integration Flow
The following workflow executes whenever a site audit runs with Lighthouse enabled.
Configuration and Strategy Selection
Every audit request contains a lighthouseStrategy field configured through AuditConfig. Valid strategies include "none", "all", "auto", and custom variants defined in src/server/lib/audit/types.ts.
// src/server/lib/audit/types.ts
interface AuditConfig {
maxPages: number;
lighthouseStrategy: LighthouseStrategy; // "none" | "all" | "auto" | ...
// ... other audit options
}
The workflow checks this flag before invoking any Lighthouse logic.
Workflow Phase Execution
In src/server/workflows/siteAuditWorkflowPhases.ts, the runAuditPhases function conditionally triggers runLighthousePhase only when config.lighthouseStrategy !== "none":
// Conditional execution based on strategy
if (config.lighthouseStrategy !== "none") {
await runLighthousePhase(auditContext);
}
Page Sampling and Selection
When Lighthouse is enabled, selectLighthousePages builds a targeted URL list using selectLighthouseSample. This sample includes the homepage plus one representative URL per template pattern to optimize API usage:
// Page selection logic
const urlsToAudit = selectLighthouseSample(pages, startUrl, strategy);
// Returns: [homepage, templateA_instance, templateB_instance, ...]
API Execution and Cloud Storage
For each selected URL, fetchAndStoreLighthouseResult executes mobile and desktop audits. This function in src/server/lib/audit/lighthouse.ts performs two critical operations:
- API Call:
fetchLighthouseResultcontacts DataforSEO'slighthouse.liveendpoint with retry logic. - Storage: On success,
putTextToR2writes the raw JSON payload to Cloudflare R2 and returns a storage key.
// src/server/lib/audit/lighthouse.ts
export async function fetchAndStoreLighthouseResult(
url: string,
device: "mobile" | "desktop"
): Promise<LighthouseResult> {
const payload = await fetchLighthouseResult(url, device);
const r2Key = await putTextToR2(`lighthouse/${auditId}/${device}.json`, payload);
return {
url,
device,
r2Key,
payloadSizeBytes: payload.length,
// ... metadata
};
}
Data Persistence
The LighthouseResult objects (containing r2Key and payloadSizeBytes rather than full JSON) are persisted via AuditRepository.batchWriteResults alongside crawled page data in siteAuditWorkflowPhases.ts.
Server-Side API Endpoints
The UI accesses Lighthouse data through two server functions defined in src/serverFunctions/lighthouse.ts:
Issues Retrieval: getAuditLighthouseIssues loads the stored payload from R2 using getJsonFromR2, parses it with readStoredLighthousePayload, and returns a trimmed issue list:
// src/serverFunctions/lighthouse.ts (lines 45-67)
export const getAuditLighthouseIssues = createServerFn({ method: "POST" })
.handler(async ({ request }) => {
const { resultId } = await request.json();
const result = await auditRepository.findById(resultId);
const payload = await getJsonFromR2(result.r2Key);
return readStoredLighthousePayload(payload, {
filterCategories: ["performance", "accessibility"]
});
});
Export Endpoint: exportAuditLighthouseIssues rebuilds export files on demand via buildLighthouseExportFile, supporting three modes: "full" (complete payload), "issues" (trimmed reports only), or "category" (filtered by category):
// src/serverFunctions/lighthouse.ts (lines 69-88)
export const exportAuditLighthouseIssues = createServerFn({ method: "POST" })
.handler(async ({ request }) => {
const { resultId, mode, category } = await request.json();
const result = await auditRepository.findById(resultId);
const payload = await getJsonFromR2(result.r2Key);
const { filename, content } = buildLighthouseExportFile(payload, {
mode,
category,
auditDate: result.createdAt
});
return { filename, content };
});
Code Implementation Examples
Starting a Site Audit with Lighthouse Enabled
Trigger a new audit with Lighthouse strategy set to "auto":
import { startSiteAudit } from "@/client/api";
await startSiteAudit({
projectId: "proj_123",
startUrl: "https://example.com",
config: {
maxPages: 500,
lighthouseStrategy: "auto", // Enables Lighthouse phase
concurrency: 5
},
});
Behind the scenes, this invokes the Cloudflare Worker running runAuditPhases, which detects the non-"none" strategy and executes the Lighthouse workflow.
Retrieving Lighthouse Issues
Fetch parsed issues for a specific result ID:
const response = await fetch("/api/lighthouse/issues", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ resultId: "lhr_abc123" }),
});
const { issues, scores } = await response.json();
console.log(issues); // Array of StoredLighthouseIssue objects
Exporting Lighthouse Results
Request a full payload export or issues-only download:
const exportResponse = await fetch("/api/lighthouse/export", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
resultId: "lhr_abc123",
mode: "full", // Options: "full", "issues", "category"
category: "performance" // Required when mode === "category"
}),
});
const { filename, content } = await exportResponse.json();
// Download: new Blob([content], { type: "application/json" })
Customizing Page Selection Strategies
To implement a custom sampling strategy (e.g., "first-10-pages"), modify src/server/lib/audit/lighthouse.ts:
export function selectLighthouseSample(
pages: LighthouseSamplePage[],
startUrl: string,
strategy: LighthouseStrategy,
): string[] {
if (strategy === "first-10") {
return pages.slice(0, 10).map(p => p.url);
}
// Existing logic for "auto", "all", etc.
return defaultSampling(pages, startUrl);
}
Then extend LighthouseStrategy in src/server/lib/audit/types.ts to expose the new option through the UI.
Key Source Files and Responsibilities
| File | Responsibility |
|---|---|
src/server/workflows/siteAuditWorkflowPhases.ts |
Orchestrates the overall audit flow; conditionally runs runLighthousePhase based on strategy configuration. |
src/server/lib/audit/lighthouse.ts |
Wraps DataforSEO's Lighthouse API, handles retry logic, and manages R2 storage via putTextToR2. |
src/server/lib/lighthousePayload.ts |
Parses stored JSON payloads with readStoredLighthousePayload and generates export files via buildLighthouseExportFile. |
src/serverFunctions/lighthouse.ts |
Exposes public server functions: getAuditLighthouseIssues and exportAuditLighthouseIssues. |
src/server/lib/audit/types.ts |
Defines TypeScript interfaces including LighthouseResult, LighthouseStrategy, and AuditConfig. |
src/shared/lighthouse.ts |
Contains shared constants like LIGHTHOUSE_CATEGORIES used across client and server. |
Summary
- OpenSEO Lighthouse integration operates entirely server-side, keeping third-party API credentials and heavy JSON payloads away from the client.
- The workflow triggers only when
lighthouseStrategyis not"none", sampling pages viaselectLighthouseSamplebefore calling DataforSEO. - Raw Lighthouse results are stored in Cloudflare R2 via
putTextToR2, while the database stores only lightweight metadata including ther2Key. - Two server functions—
getAuditLighthouseIssuesandexportAuditLighthouseIssues—provide UI access to parsed issues and exportable data without exposing raw API responses. - The payload parsing layer supports filtering by category and multiple export modes ("full", "issues", "category") while generating descriptive filenames that encode audit dates.
Frequently Asked Questions
How does OpenSEO decide which pages to audit with Lighthouse?
OpenSEO uses the selectLighthouseSample function in src/server/lib/audit/lighthouse.ts to intelligently sample pages based on the configured lighthouseStrategy. For the "auto" strategy, it selects the homepage plus one representative URL from each unique page template pattern. This prevents redundant API calls on similar pages while ensuring comprehensive coverage of site structure.
Where are the raw Lighthouse JSON payloads stored?
The system stores complete Lighthouse results in Cloudflare R2 object storage using the putTextToR2 utility. The database persists only a LighthouseResult record containing the r2Key (storage path), payload size, and metadata. This architecture keeps the database lightweight while preserving full audit data for on-demand retrieval and export.
Can I export Lighthouse results in formats other than JSON?
Yes. The exportAuditLighthouseIssues server function supports three export modes via the buildLighthouseExportFile utility in src/server/lib/lighthousePayload.ts. You can request "full" (complete raw payload), "issues" (trimmed report containing only violations and scores), or "category" (filtered to a specific Lighthouse category like "performance" or "accessibility"). The response includes both the formatted content and a descriptive filename.
What happens if the DataforSEO Lighthouse API fails during an audit?
The fetchLighthouseResult function in src/server/lib/audit/lighthouse.ts implements retry logic for transient failures. If the API ultimately fails for a specific URL, the error is logged and the audit continues processing other URLs. The failed attempt is recorded in the audit results, allowing the UI to display partial data and retry options without crashing the entire site audit workflow.
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 →