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:

  1. Workflow orchestration – The audit runner decides if and when Lighthouse executes based on configuration flags.
  2. Lighthouse execution and storage – A thin wrapper calls the DataforSEO Lighthouse API, persists the raw JSON payload to Cloudflare R2, and returns lightweight metadata.
  3. 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:

  1. API Call: fetchLighthouseResult contacts DataforSEO's lighthouse.live endpoint with retry logic.
  2. Storage: On success, putTextToR2 writes 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 lighthouseStrategy is not "none", sampling pages via selectLighthouseSample before calling DataforSEO.
  • Raw Lighthouse results are stored in Cloudflare R2 via putTextToR2, while the database stores only lightweight metadata including the r2Key.
  • Two server functionsgetAuditLighthouseIssues and exportAuditLighthouseIssues—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:

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 →