Skip to Content
ChannelsWebsiteWebsite Issues — Per-Page Issue Detection & AI Code Fixer

Website Issues — Per-Page Issue Detection & AI Code Fixer

Status: [Live May 2026]

Issue detection runs automatically after every website-insights job. Issues are stored per page per type. Dashboard shows an Issues tab on the channel detail and an Issues card on each page detail. The “Fix with AI” button triggers the website-code-fixer agent which opens a GitHub PR with targeted source-code fixes.


Concept

After every crawl + insight cycle, a deterministic rule engine scans the WebPage records and writes one WebsiteIssue row per page per detected problem. This gives tenants a structured, actionable list distinct from the Claude-generated narrative in WebsiteInsight.

When a WebsiteGitHubSync config is present, users can ask the code-fixer agent to fix a specific issue type (or all open issues for a page) directly in their source repository. The agent reads the repo file tree, detects the framework, finds the responsible source files, applies surgical fixes, and opens a pull request.


Data Models

WebsiteIssue

One row per page per detected issue type. Regenerated on every insight run (open issues replaced; fixed/ignored rows preserved).

model WebsiteIssue { id String @id @default(cuid()) tenantId String connectedChannelId String webPageId String webCrawlJobId String issueType String // see Issue Types table below severity String // "critical" | "warning" | "info" pageUrl String // denormalized — display without join details Json? // { wordCount, httpStatus, duplicateCount, imageUrl, ... } status String @default("open") // "open" | "fixed" | "ignored" fixPrUrl String? fixPrNumber Int? fixedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([connectedChannelId, issueType]) @@index([connectedChannelId, status]) @@index([webPageId]) @@map("website_issue") }

WebsiteGitHubSync

Maps a Website channel to a GitHub repo for code-fix PRs. One record per website channel (unique).

model WebsiteGitHubSync { id String @id @default(cuid()) tenantId String connectedChannelId String @unique // Website ConnectedChannel githubChannelId String // GitHub OAuth ConnectedChannel repoOwner String repoName String branch String @default("main") lastFixRunAt DateTime? lastPrUrl String? lastPrNumber Int? lastFixStatus String? // "idle" | "running" | "success" | "failed" lastFixError String? @db.Text @@map("website_github_sync") }

Issue Types

issueTypeSeverityDetection ruledetails fields
missing_titlecriticalWebPage.title IS NULL
missing_meta_descriptioncriticalWebPage.description IS NULL
broken_pagecriticalWebPage.httpStatus >= 400{ httpStatus }
thin_contentwarningextractedText word count < 200{ wordCount }
duplicate_titlewarningMultiple pages share same lowercase title{ title, duplicateCount }
duplicate_metawarningMultiple pages share same lowercase description{ descriptionSnippet, duplicateCount }
missing_alt_textwarningWebMedia.altText IS NULL on IMAGE type{ imageUrl }

Issue Generation

Where: packages/agents/src/workers/insights/website-insights.worker.ts — exported generateWebsiteIssues() function.

When: Immediately after buildMetrics() in the existing website-insights worker (which runs after every completed crawl). Non-fatal — a generation failure is logged but does not block the Claude insight.

Algorithm:

generateWebsiteIssues(tenantId, connectedChannelId, webCrawlJobId) ├─ Load all WebPage rows for this crawl ├─ Load IMAGE WebMedia rows with no altText (via pageMedias join) ├─ For each page: │ ├─ title IS NULL → missing_title (critical) │ ├─ description IS NULL → missing_meta_description (critical) │ ├─ httpStatus >= 400 → broken_page (critical) │ └─ wordCount < 200 > 0 → thin_content (warning) ├─ Across all pages (duplicate detection): │ ├─ Group pages by lowercase title → titles shared by 2+ pages → duplicate_title (warning) per page │ └─ Group pages by lowercase desc → descs shared by 2+ pages → duplicate_meta (warning) per page ├─ For each image with no altText → missing_alt_text (warning) on its first associated page ├─ DELETE existing open issues for channel (fixed/ignored preserved) └─ createMany(issues)

API

Prefix: /tenant/v1/website-issues

MethodPathDescription
GET/channel/:idCursor-paginated list. Query: status, issueType, severity, cursor, limit
GET/channel/:id/summaryCounts by type (open only), by severity (open only), by status (all)
GET/page/:webPageIdIssues for a single page
PATCH/:issueIdUpdate status to "open" or "ignored"
POST/github-syncUpsert WebsiteGitHubSync config (body: connectedChannelId, githubChannelId, repoOwner, repoName, branch)
GET/github-sync/:connectedChannelIdGet sync config for a channel
POST/fixTrigger code-fixer agent (body: connectedChannelId, optional issueIds[])

Router file: apps/api/src/routers/website-issues.ts
Registered in both app.ts and index.ts.


Queue

// packages/queue/src/types.ts interface WebsiteCodeFixerJobData { tenantId: string; tenantName: string; connectedChannelId: string; websiteGitHubSyncId: string; issueIds: string[]; // empty = fix all open issues for channel }
// packages/queue/src/queues.ts enqueueWebsiteCodeFixer(data: WebsiteCodeFixerJobData): Promise<string> // Queue: agent__website-code-fixer // JobId: website-code-fixer__{tenantId}__{connectedChannelId}__{Date.now()}

Code-Fixer Agent Worker

File: packages/agents/src/workers/website-code-fixer.worker.ts
Queue: agent__website-code-fixer
Concurrency: 1 (prevents race conditions on the same repo)

Flow

processJob(job) ├─ 1. Load WebsiteGitHubSync → decrypt GitHub OAuth token ├─ 2. Load open WebsiteIssue rows (issueIds filter or all open) │ Cap at 50. Filter to code-fixable types only (excludes broken_page). ├─ 3. publishAgentEvent(agent:started) ├─ 4. GitHubService.getFileTree() → detect framework from file names │ Frameworks: nextjs / nuxt / gatsby / wordpress / astro / node-generic / html ├─ 5. Claude tool-use loop (max 20 iterations): │ Tools: │ read_file(filePath) → GitHubService.getFileContent() │ write_file(filePath, content, commitMessage) → GitHubService.putFile() │ (creates branch on first write: "leadmetrics/fix-website-issues-{timestamp}") │ Prompt: framework + file tree (first 300 paths) + issue list │ Model: from AgentConfig.role="website-code-fixer", default claude-sonnet-4-6 ├─ 6. GitHubService.createPR() — if any files were changed │ Title: "fix: website SEO issues detected by Leadmetrics (N issues)" │ Body: issue list + files changed + merge instructions ├─ 7. WebsiteIssue.updateMany → status="fixed", fixPrUrl, fixPrNumber, fixedAt ├─ 8. WebsiteGitHubSync.update → lastFixStatus, lastPrUrl, lastPrNumber └─ 9. publishAgentEvent(agent:completed)

Claude Prompt Structure

# Website Code Fix Task ## Framework {detected framework} ## Repository File Tree (first 300 lines) {file paths, one per line} ## Issues to Fix 1. [missing_title] https://example.com/about 2. [missing_meta_description] https://example.com/services — ... ## Instructions - read_file before editing, write_file after - Make ONLY targeted changes, no refactoring - For missing_title/meta: add/update HTML meta tags in template/layout file - For missing_alt_text: add descriptive alt to <img> tags - For thin_content: create a details.json note (do NOT fabricate content) - For duplicate_title/meta: make each page's tag unique by appending page name - Skip issues where the responsible file cannot be determined - Output a JSON summary block at the end: { fixed[], skipped[], filesChanged[] }

Framework Detection

Marker fileDetected as
next.config.js/ts/mjsnextjs
nuxt.config.ts/jsnuxt
gatsby-config.js/tsgatsby
wp-config.phpwordpress
any *.astro fileastro
package.json (no above)node-generic
none of the abovehtml

Code-Fixable Issues

The agent only attempts to fix: missing_title, missing_meta_description, missing_h1, thin_content, duplicate_title, duplicate_meta, missing_alt_text.

broken_page (HTTP 4xx/5xx) is excluded — these require infrastructure or routing changes that the agent cannot safely make.


UI

Channel Detail — Issues Tab

Location: apps/dashboard/src/app/(dashboard)/channels/[id]/WebsiteChannelDetail.tsx

Added as a fifth tab alongside Overview / Pages / Media / Insights. Tab label shows live count: Issues (12 critical, 8 warnings).

Summary row: 3 stat cards — Critical count (red / XCircle), Warning count (amber / AlertCircle), Fixed count (emerald / CheckCircle2).

Filter pills: Open / Fixed / Ignored

Issue groups: Issues grouped by issueType. Each group renders as a rounded-2xl card:

  • Header: icon + human-readable label + count badge + “Fix with AI” button (shown only for status=open)
  • Rows: pageUrl + details snippet + status badge
  • IntersectionObserver infinite scroll on the flat list

Banners:

  • Success (emerald): “Code fixer agent queued — a PR will be opened in your GitHub repo shortly.”
  • Warning (amber): “Connect a GitHub repo first in the GitHub Sync tab.” (when NO_GITHUB_SYNC error)
  • Error (red): generic API error message

Page Detail — Issues Card

Location: apps/dashboard/src/app/(dashboard)/channels/[id]/webpages/[webPageId]/WebPageDetailClient.tsx

An Issues card rendered between the metadata section and the content section. Shows:

  • Card header: “Issues” + count badge
  • Empty state: “No issues detected for this page.”
  • Each issue row: severity badge (critical=red, warning=amber) + human-readable label + details snippet + status badge + optional “View PR #N” link
  • “Fix with AI” button — POSTs all open issue IDs for the page to /tenant/v1/website-issues/fix

Files Changed (May 2026)

FileChange
packages/db/prisma/schema.prismaAdd WebsiteIssue + WebsiteGitHubSync models + back-relations
packages/queue/src/types.tsAdd WebsiteCodeFixerJobData + "website-code-fixer" to AgentRole
packages/queue/src/queues.tsAdd enqueueWebsiteCodeFixer()
packages/queue/src/index.tsExport new type + function
packages/agents/src/workers/insights/website-insights.worker.tsAdd generateWebsiteIssues() + call in processJob()
packages/agents/src/workers/website-code-fixer.worker.tsNew — code-fixer agent worker
apps/servers/agents/src/index.tsRegister startWebsiteCodeFixerWorker + stop
apps/api/src/routers/website-issues.tsNew — 7 endpoints
apps/api/src/app.tsRegister websiteIssuesRouter
apps/api/src/index.tsRegister websiteIssuesRouter
apps/dashboard/src/app/(dashboard)/channels/[id]/WebsiteChannelDetail.tsxAdd Issues tab (state, fetch, render)
apps/dashboard/src/app/(dashboard)/channels/[id]/webpages/[webPageId]/WebPageDetailClient.tsxAdd Issues card + fix trigger
apps/dashboard/src/app/(dashboard)/channels/[id]/webpages/[webPageId]/page.tsxPass apiUrl + token to client

© 2026 Leadmetrics — Internal use only