feat: add insight page with Lighthouse analysis

- New /dashboard/insight page: URL analizi, mobil/masaüstü strateji seçimi
- Core Web Vitals, fırsatlar ve teşhis bölümleri
- PSI throttling değerleri ile eşleştirildi
- Dockerfile: Chromium eklendi (insight için), .dockerignore oluşturuldu
- serverExternalPackages: lighthouse ve chrome-launcher bundle dışına alındı

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mstfyldz
2026-06-06 01:37:13 +03:00
co-authored by Claude Sonnet 4.6
parent a4c58135df
commit 851ce96703
8 changed files with 2594 additions and 23 deletions
+139
View File
@@ -0,0 +1,139 @@
import { NextRequest, NextResponse } from 'next/server'
import { requireAuth } from '@/lib/auth'
import lighthouse from 'lighthouse'
import * as chromeLauncher from 'chrome-launcher'
export const runtime = 'nodejs'
const OPPORTUNITY_IDS = [
'render-blocking-resources',
'unused-css-rules',
'unused-javascript',
'uses-optimized-images',
'uses-webp-images',
'uses-text-compression',
'uses-responsive-images',
'offscreen-images',
'server-response-time',
'redirects',
'efficient-animated-content',
'legacy-javascript',
'unminified-css',
'unminified-javascript',
]
const DIAGNOSTIC_IDS = [
'total-byte-weight',
'dom-size',
'bootup-time',
'mainthread-work-breakdown',
'font-display',
'third-party-summary',
'critical-request-chains',
]
function pickAudit(audit: any) {
if (!audit) return null
return {
id: audit.id,
title: audit.title,
description: audit.description,
score: audit.score,
scoreDisplayMode: audit.scoreDisplayMode,
displayValue: audit.displayValue ?? null,
numericValue: audit.numericValue ?? null,
}
}
export async function GET(request: NextRequest) {
const authErr = await requireAuth(request)
if (authErr) return authErr
const { searchParams } = new URL(request.url)
const url = searchParams.get('url')
const strategy = searchParams.get('strategy') || 'mobile'
if (!url) {
return NextResponse.json({ error: 'URL gerekli' }, { status: 400 })
}
try {
new URL(url)
} catch {
return NextResponse.json({ error: 'Geçersiz URL formatı' }, { status: 400 })
}
let chrome: chromeLauncher.LaunchedChrome | undefined
try {
chrome = await chromeLauncher.launch({
chromeFlags: ['--headless', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
})
const formFactor = strategy === 'desktop' ? 'desktop' : 'mobile'
const screenEmulation = formFactor === 'desktop'
? { mobile: false, width: 1350, height: 940, deviceScaleFactor: 1, disabled: false }
: { mobile: true, width: 375, height: 812, deviceScaleFactor: 3, disabled: false }
// PSI ile eşleşen throttling değerleri
const throttling = formFactor === 'desktop'
? { rttMs: 40, throughputKbps: 10_240, uploadThroughputKbps: 10_240, cpuSlowdownMultiplier: 1 }
: { rttMs: 150, throughputKbps: 1_638.4, uploadThroughputKbps: 750, cpuSlowdownMultiplier: 4 }
const runnerResult = await Promise.race([
lighthouse(url, {
port: chrome.port,
output: 'json',
logLevel: 'error',
formFactor,
screenEmulation,
throttling,
throttlingMethod: 'simulate',
}),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Lighthouse zaman aşımına uğradı')), 60_000)
),
])
if (!runnerResult) throw new Error('Lighthouse sonuç döndürmedi')
const { lhr } = runnerResult
return NextResponse.json({
id: url,
lighthouseResult: {
categories: {
performance: { score: lhr.categories?.performance?.score ?? 0 },
accessibility: { score: lhr.categories?.accessibility?.score ?? 0 },
'best-practices': { score: lhr.categories?.['best-practices']?.score ?? 0 },
seo: { score: lhr.categories?.seo?.score ?? 0 },
},
audits: {
'first-contentful-paint': pickAudit(lhr.audits?.['first-contentful-paint']),
'largest-contentful-paint': pickAudit(lhr.audits?.['largest-contentful-paint']),
'total-blocking-time': pickAudit(lhr.audits?.['total-blocking-time']),
'cumulative-layout-shift': pickAudit(lhr.audits?.['cumulative-layout-shift']),
'speed-index': pickAudit(lhr.audits?.['speed-index']),
'interactive': pickAudit(lhr.audits?.['interactive']),
},
opportunities: OPPORTUNITY_IDS
.map(id => pickAudit(lhr.audits?.[id]))
.filter((a): a is NonNullable<typeof a> => a !== null && a.score !== null && a.score < 0.9),
diagnostics: DIAGNOSTIC_IDS
.map(id => pickAudit(lhr.audits?.[id]))
.filter((a): a is NonNullable<typeof a> => a !== null),
},
})
} catch (error: any) {
console.error('Lighthouse hatası:', error)
return NextResponse.json(
{ error: error.message.includes('zaman aşımı')
? 'Analiz zaman aşımına uğradı, lütfen tekrar deneyin.'
: 'Analiz sırasında hata oluştu. Sunucuda Chrome yüklü olduğundan emin olun.' },
{ status: 500 }
)
} finally {
await chrome?.kill()
}
}