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:
co-authored by
Claude Sonnet 4.6
parent
a4c58135df
commit
851ce96703
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user