diff --git a/src/lib/transformPlan.ts b/src/lib/transformPlan.ts index 8b34a04..b48bc85 100644 --- a/src/lib/transformPlan.ts +++ b/src/lib/transformPlan.ts @@ -1,4 +1,5 @@ -import type { MarketingPlan, ChannelStrategyCard, ContentPillar, CalendarWeek, CalendarEntry, ContentCountSummary } from '../types/plan'; +import type { MarketingPlan, ChannelStrategyCard, ContentPillar, CalendarWeek, CalendarEntry, ContentCountSummary, AssetCard, YouTubeRepurposeItem } from '../types/plan'; +import type { EnrichmentData } from './transformReport'; /** * Raw report data from Supabase marketing_reports table. @@ -148,6 +149,179 @@ function buildCalendar(channels: ChannelStrategyCard[]): { }; } +function buildAssets(enrichment: EnrichmentData | undefined): { assets: AssetCard[]; youtubeRepurpose: YouTubeRepurposeItem[] } { + if (!enrichment) return { assets: [], youtubeRepurpose: [] }; + + const assets: AssetCard[] = []; + let assetIdx = 0; + + // YouTube videos → video assets + if (enrichment.youtube?.videos) { + for (const v of enrichment.youtube.videos.slice(0, 5)) { + assets.push({ + id: `yt-${assetIdx++}`, + source: 'youtube', + sourceLabel: 'YouTube', + type: 'video', + title: v.title || '영상', + description: `조회수 ${(v.views || 0).toLocaleString()} · 좋아요 ${(v.likes || 0).toLocaleString()}`, + repurposingSuggestions: ['Shorts 추출', '블로그 스크립트', '카드뉴스'], + status: 'collected', + }); + } + } + + // Instagram posts → photo assets + const igAccounts = enrichment.instagramAccounts || (enrichment.instagram ? [enrichment.instagram] : []); + for (const ig of igAccounts) { + if (ig.latestPosts) { + for (const p of (ig.latestPosts as { type?: string; likes?: number; caption?: string }[]).slice(0, 3)) { + assets.push({ + id: `ig-${assetIdx++}`, + source: 'social', + sourceLabel: `Instagram @${ig.username || ''}`, + type: 'photo', + title: (p.caption || '').slice(0, 60) || 'Instagram 게시물', + description: `좋아요 ${(p.likes || 0).toLocaleString()}`, + repurposingSuggestions: ['피드 리포스트', '스토리 하이라이트'], + status: 'collected', + }); + } + } + } + + // Naver blog posts → text assets + if (enrichment.naverBlog?.posts) { + for (const post of enrichment.naverBlog.posts.slice(0, 3)) { + assets.push({ + id: `nb-${assetIdx++}`, + source: 'blog', + sourceLabel: '네이버 블로그', + type: 'text', + title: post.title || '블로그 포스트', + description: post.description || '', + repurposingSuggestions: ['SNS 카드뉴스', '영상 스크립트'], + status: 'collected', + }); + } + } + + // YouTube repurpose items + const youtubeRepurpose: YouTubeRepurposeItem[] = (enrichment.youtube?.videos || []) + .slice(0, 5) + .map(v => ({ + title: v.title || '', + views: v.views || 0, + type: ((v.duration && parseInt(v.duration) < 60) ? 'Short' : 'Long') as 'Short' | 'Long', + repurposeAs: ['Shorts 3개 추출', '블로그 포스트 변환', '카드뉴스 4장', '카카오톡 CTA'], + })); + + return { assets, youtubeRepurpose }; +} + +function buildChannelBrandingFromEnrichment(enrichment: EnrichmentData | undefined) { + if (!enrichment) return []; + + const rules: { channel: string; icon: string; profilePhoto: string; bannerSpec: string; bioTemplate: string; currentStatus: 'correct' | 'incorrect' | 'missing' }[] = []; + + if (enrichment.youtube) { + rules.push({ + channel: 'YouTube', + icon: 'youtube', + profilePhoto: enrichment.youtube.thumbnailUrl || '', + bannerSpec: '2560×1440px 배너', + bioTemplate: enrichment.youtube.description?.slice(0, 200) || '', + currentStatus: enrichment.youtube.description ? 'correct' : 'missing', + }); + } + + const igAccounts = enrichment.instagramAccounts || (enrichment.instagram ? [enrichment.instagram] : []); + for (const ig of igAccounts) { + rules.push({ + channel: `Instagram @${ig.username || ''}`, + icon: 'instagram', + profilePhoto: '', + bannerSpec: 'N/A (하이라이트 커버)', + bioTemplate: ig.bio || '', + currentStatus: ig.bio ? 'correct' : 'missing', + }); + } + + if (enrichment.facebook) { + rules.push({ + channel: 'Facebook', + icon: 'facebook', + profilePhoto: enrichment.facebook.profilePictureUrl || '', + bannerSpec: '820×312px 커버 사진', + bioTemplate: enrichment.facebook.intro || '', + currentStatus: enrichment.facebook.intro ? 'correct' : 'missing', + }); + } + + return rules; +} + +function buildBrandInconsistencies(enrichment: EnrichmentData | undefined, clinicName: string): { field: string; values: { channel: string; value: string; isCorrect: boolean }[]; impact: string; recommendation: string }[] { + if (!enrichment) return []; + const items: { field: string; values: { channel: string; value: string; isCorrect: boolean }[]; impact: string; recommendation: string }[] = []; + + // Collect names across channels + const names: { channel: string; value: string }[] = []; + if (clinicName) names.push({ channel: '웹사이트', value: clinicName }); + if (enrichment.youtube?.channelName) names.push({ channel: 'YouTube', value: enrichment.youtube.channelName }); + const igAccounts = enrichment.instagramAccounts || (enrichment.instagram ? [enrichment.instagram] : []); + for (const ig of igAccounts) { + if (ig.username) names.push({ channel: `Instagram @${ig.username}`, value: ig.username }); + } + if (enrichment.facebook?.pageName) names.push({ channel: 'Facebook', value: enrichment.facebook.pageName }); + if (enrichment.gangnamUnni?.name) names.push({ channel: '강남언니', value: enrichment.gangnamUnni.name }); + + if (names.length >= 2) { + const websiteName = names[0].value; + items.push({ + field: '병원명', + values: names.map(n => ({ channel: n.channel, value: n.value, isCorrect: n.value.toLowerCase().includes(websiteName.toLowerCase().slice(0, 4)) })), + impact: '채널마다 다른 이름은 브랜드 인지도를 분산시킵니다', + recommendation: '모든 채널에서 동일한 공식 병원명을 사용하세요', + }); + } + + // Collect phone numbers + const phones: { channel: string; value: string }[] = []; + if (enrichment.googleMaps?.phone) phones.push({ channel: 'Google Maps', value: enrichment.googleMaps.phone as string }); + if (enrichment.naverPlace?.telephone) phones.push({ channel: '네이버 플레이스', value: enrichment.naverPlace.telephone }); + if (enrichment.facebook?.phone) phones.push({ channel: 'Facebook', value: enrichment.facebook.phone as string }); + if (phones.length >= 2) { + const ref = phones[0].value.replace(/[^0-9]/g, ''); + items.push({ + field: '연락처', + values: phones.map(p => ({ channel: p.channel, value: p.value, isCorrect: p.value.replace(/[^0-9]/g, '') === ref })), + impact: '다른 전화번호는 고객 혼란을 유발합니다', + recommendation: '모든 플랫폼에 동일한 대표 전화번호를 등록하세요', + }); + } + + return items; +} + +function buildBrandColors(branding: Record | undefined): { name: string; hex: string; usage: string }[] { + if (!branding) return []; + const colors: { name: string; hex: string; usage: string }[] = []; + if (branding.primaryColor) colors.push({ name: 'Primary', hex: branding.primaryColor as string, usage: '메인 브랜드 색상' }); + if (branding.accentColor) colors.push({ name: 'Accent', hex: branding.accentColor as string, usage: '강조 색상' }); + if (branding.backgroundColor) colors.push({ name: 'Background', hex: branding.backgroundColor as string, usage: '배경 색상' }); + if (branding.textColor) colors.push({ name: 'Text', hex: branding.textColor as string, usage: '본문 텍스트' }); + return colors; +} + +function buildBrandFonts(branding: Record | undefined): { family: string; weight: string; usage: string; sampleText: string }[] { + if (!branding) return []; + const fonts: { family: string; weight: string; usage: string; sampleText: string }[] = []; + if (branding.headingFont) fonts.push({ family: branding.headingFont as string, weight: 'Bold', usage: '제목/헤딩', sampleText: '안전이 예술이 되는 곳' }); + if (branding.bodyFont) fonts.push({ family: branding.bodyFont as string, weight: 'Regular', usage: '본문 텍스트', sampleText: '프리미엄 의료 서비스를 경험하세요' }); + return fonts; +} + /** * Transform a raw Supabase report row into a MarketingPlan. * Uses report data (channel analysis, recommendations, services) @@ -159,6 +333,9 @@ export function transformReportToPlan(row: RawReportRow): MarketingPlan { const channelAnalysis = report.channelAnalysis as Record> | undefined; const recommendations = report.recommendations as Record[] | undefined; const services = (clinicInfo?.services as string[]) || []; + const enrichment = report.channelEnrichment as EnrichmentData | undefined; + const scrapeData = row.scrape_data as Record | undefined; + const branding = scrapeData?.branding as Record | undefined; const channelStrategies = buildChannelStrategies(channelAnalysis, recommendations); const pillars = buildContentPillars(recommendations, services); @@ -168,13 +345,13 @@ export function transformReportToPlan(row: RawReportRow): MarketingPlan { id: row.id, reportId: row.id, clinicName: (clinicInfo?.name as string) || row.clinic_name || '', - clinicNameEn: '', + clinicNameEn: (clinicInfo?.nameEn as string) || '', createdAt: row.created_at, targetUrl: row.url, brandGuide: { - colors: [], - fonts: [], + colors: buildBrandColors(branding), + fonts: buildBrandFonts(branding), logoRules: [], toneOfVoice: { personality: ['전문적', '친근한', '신뢰할 수 있는'], @@ -182,8 +359,8 @@ export function transformReportToPlan(row: RawReportRow): MarketingPlan { doExamples: ['정확한 의학 용어 사용', '환자 성공 사례 공유', '전문의 인사이트 제공'], dontExamples: ['과장된 효과 주장', '비교 광고', '의학적 보장 표현'], }, - channelBranding: [], - brandInconsistencies: [], + channelBranding: buildChannelBrandingFromEnrichment(enrichment), + brandInconsistencies: buildBrandInconsistencies(enrichment, (clinicInfo?.name as string) || row.clinic_name || ''), }, channelStrategies, @@ -214,9 +391,6 @@ export function transformReportToPlan(row: RawReportRow): MarketingPlan { calendar, - assetCollection: { - assets: [], - youtubeRepurpose: [], - }, + assetCollection: buildAssets(enrichment), }; } diff --git a/src/lib/transformReport.ts b/src/lib/transformReport.ts index 16a3a4e..1983ed8 100644 --- a/src/lib/transformReport.ts +++ b/src/lib/transformReport.ts @@ -7,11 +7,14 @@ import type { MarketingReport, Severity, ChannelScore, DiagnosisItem, TopVideo } interface ApiReport { clinicInfo?: { name?: string; + nameEn?: string; + established?: string; address?: string; phone?: string; services?: string[]; doctors?: { name: string; specialty: string }[]; }; + newChannelProposals?: { channel?: string; priority?: string; rationale?: string }[]; executiveSummary?: string; overallScore?: number; channelAnalysis?: Record !!p?.channel) + .map(p => ({ + channel: p.channel || '', + priority: p.priority || 'P2', + rationale: p.rationale || '', + })), }, roadmap: [1, 2, 3].map(month => ({ @@ -455,6 +478,100 @@ export interface EnrichmentData { }; } +/** + * Generate data-driven diagnosis items from enrichment data. + */ +function generateEnrichmentDiagnosis(enrichment: EnrichmentData): DiagnosisItem[] { + const items: DiagnosisItem[] = []; + + // YouTube diagnosis + if (enrichment.youtube) { + const yt = enrichment.youtube; + const videos = yt.videos || []; + const shorts = videos.filter(v => v.duration && parseInt(v.duration) < 60); + const shortsRatio = videos.length > 0 ? (shorts.length / videos.length) * 100 : 0; + + if (shortsRatio === 0) { + items.push({ category: 'YouTube', detail: 'Shorts 콘텐츠가 없습니다. 숏폼 영상은 신규 유입에 가장 효과적인 포맷입니다.', severity: 'warning' }); + } + if (!yt.description || yt.description.length < 50) { + items.push({ category: 'YouTube', detail: '채널 설명이 미비합니다. SEO와 채널 신뢰도를 위해 키워드 포함 설명을 작성하세요.', severity: 'warning' }); + } + if (yt.totalVideos && yt.totalViews) { + const avgViews = yt.totalViews / yt.totalVideos; + if (avgViews < 500) { + items.push({ category: 'YouTube', detail: `영상당 평균 조회수 ${Math.round(avgViews)}회로 낮습니다. 썸네일과 제목 최적화가 필요합니다.`, severity: 'warning' }); + } + } + if (yt.subscribers && yt.subscribers < 10000) { + items.push({ category: 'YouTube', detail: `구독자 ${yt.subscribers.toLocaleString()}명으로 성장 여지가 큽니다. 일관된 업로드 스케줄을 권장합니다.`, severity: 'good' }); + } + } + + // Instagram diagnosis + const igAccounts = enrichment.instagramAccounts || (enrichment.instagram ? [enrichment.instagram] : []); + for (const ig of igAccounts) { + const handle = ig.username || 'Instagram'; + if (!ig.bio) { + items.push({ category: 'Instagram', detail: `@${handle} 바이오가 비어있습니다. CTA 링크와 소개를 추가하세요.`, severity: 'warning' }); + } + if (!ig.externalUrl) { + items.push({ category: 'Instagram', detail: `@${handle} 외부 링크(웹사이트/예약)가 설정되지 않았습니다.`, severity: 'warning' }); + } + if (!ig.isBusinessAccount) { + items.push({ category: 'Instagram', detail: `@${handle} 비즈니스 계정이 아닙니다. 인사이트 분석을 위해 비즈니스 계정 전환을 권장합니다.`, severity: 'critical' }); + } + if (ig.followers && ig.followers > 1000 && ig.posts && ig.posts < 30) { + items.push({ category: 'Instagram', detail: `@${handle} 팔로워 대비 게시물이 적습니다 (${ig.posts}개). 콘텐츠 업로드 빈도를 높이세요.`, severity: 'warning' }); + } + } + + // Facebook diagnosis + if (enrichment.facebook) { + const fb = enrichment.facebook; + if ((fb.followers ?? 0) < 500) { + items.push({ category: 'Facebook', detail: `팔로워 ${fb.followers?.toLocaleString() ?? 0}명으로 페이지 활성화가 필요합니다.`, severity: 'warning' }); + } + if (!fb.intro) { + items.push({ category: 'Facebook', detail: 'Facebook 페이지 소개글이 없습니다. 병원 정보와 CTA를 추가하세요.', severity: 'warning' }); + } + } + + // 강남언니 diagnosis + if (enrichment.gangnamUnni) { + const gu = enrichment.gangnamUnni; + if (gu.rating && gu.rating < 9.0) { + items.push({ category: '강남언니', detail: `평점 ${gu.rating}/10 — 업계 상위권(9.5+) 대비 개선 여지가 있습니다.`, severity: 'warning' }); + } + if (gu.doctors && gu.doctors.length < 3) { + items.push({ category: '강남언니', detail: `등록 의사 ${gu.doctors.length}명 — 의료진 프로필을 더 등록하면 신뢰도가 높아집니다.`, severity: 'good' }); + } + if (gu.totalReviews && gu.totalReviews > 5000) { + items.push({ category: '강남언니', detail: `리뷰 ${gu.totalReviews.toLocaleString()}건 — 우수한 리뷰 수입니다. 리뷰 관리와 답변에 집중하세요.`, severity: 'excellent' }); + } + } + + // Google Maps diagnosis + if (enrichment.googleMaps) { + const gm = enrichment.googleMaps; + if (gm.rating && gm.rating < 4.5) { + items.push({ category: 'Google Maps', detail: `평점 ${gm.rating}/5 — 부정 리뷰 대응과 만족도 개선이 필요합니다.`, severity: 'warning' }); + } + if (gm.reviewCount && gm.reviewCount < 100) { + items.push({ category: 'Google Maps', detail: `리뷰 ${gm.reviewCount}건 — 더 많은 환자 리뷰를 유도하세요.`, severity: 'warning' }); + } + } + + // Naver diagnosis + if (enrichment.naverBlog) { + if (enrichment.naverBlog.totalResults && enrichment.naverBlog.totalResults < 100) { + items.push({ category: '네이버 블로그', detail: `블로그 검색 노출 ${enrichment.naverBlog.totalResults}건 — SEO 최적화 블로그 포스팅을 늘리세요.`, severity: 'warning' }); + } + } + + return items; +} + /** * Merge enrichment data into an existing MarketingReport. * Returns a new object — does not mutate the original. @@ -590,13 +707,21 @@ export function mergeEnrichment( const gu = enrichment.gangnamUnni; // Update clinic snapshot with real gangnamUnni data - if (gu.rating) { - merged.clinicSnapshot = { - ...merged.clinicSnapshot, - overallRating: gu.rating, - totalReviews: gu.totalReviews ?? merged.clinicSnapshot.totalReviews, - certifications: gu.badges?.length ? gu.badges : merged.clinicSnapshot.certifications, - }; + merged.clinicSnapshot = { + ...merged.clinicSnapshot, + overallRating: gu.rating ?? merged.clinicSnapshot.overallRating, + totalReviews: gu.totalReviews ?? merged.clinicSnapshot.totalReviews, + certifications: gu.badges?.length ? gu.badges : merged.clinicSnapshot.certifications, + staffCount: gu.doctors?.length ?? merged.clinicSnapshot.staffCount, + }; + + // Extract nearest station from address (Korean station name pattern) + const addressToSearch = gu.address || merged.clinicSnapshot.location; + if (addressToSearch && !merged.clinicSnapshot.nearestStation) { + const stationMatch = addressToSearch.match(/(\S+역)/); + if (stationMatch) { + merged.clinicSnapshot = { ...merged.clinicSnapshot, nearestStation: stationMatch[1] }; + } } // Update lead doctor with gangnamUnni doctor data @@ -694,5 +819,11 @@ export function mergeEnrichment( } } + // Generate data-driven diagnosis from enrichment data + const enrichDiagnosis = generateEnrichmentDiagnosis(enrichment); + if (enrichDiagnosis.length > 0) { + merged.problemDiagnosis = [...merged.problemDiagnosis, ...enrichDiagnosis]; + } + return merged; } diff --git a/supabase/functions/generate-report/index.ts b/supabase/functions/generate-report/index.ts index 0a6206a..321b69c 100644 --- a/supabase/functions/generate-report/index.ts +++ b/supabase/functions/generate-report/index.ts @@ -88,7 +88,9 @@ ${JSON.stringify(analyzeResult.data?.analysis || {}, null, 2)} { "clinicInfo": { - "name": "병원명", + "name": "병원명 (한국어)", + "nameEn": "영문 병원명", + "established": "개원년도 (예: 2005)", "address": "주소", "phone": "전화번호", "services": ["시술1", "시술2"], @@ -96,20 +98,23 @@ ${JSON.stringify(analyzeResult.data?.analysis || {}, null, 2)} "socialMedia": { "instagramAccounts": ["국내용 핸들", "해외/영문 핸들", "기타 관련 계정 (@ 없이, 예: banobagi_ps, english_banobagi)"], "youtube": "YouTube 채널 핸들 또는 URL", - "facebook": "Facebook 페이지명", + "facebook": "Facebook 페이지명 또는 URL", "naverBlog": "네이버 블로그 ID" } }, "executiveSummary": "경영진 요약 (3-5문장)", "overallScore": 0-100, "channelAnalysis": { - "naverBlog": { "score": 0-100, "status": "active|inactive|weak", "posts": 0, "recommendation": "추천사항" }, - "instagram": { "score": 0-100, "status": "active|inactive|weak", "followers": 0, "recommendation": "추천사항" }, - "youtube": { "score": 0-100, "status": "active|inactive|weak", "subscribers": 0, "recommendation": "추천사항" }, - "naverPlace": { "score": 0-100, "rating": 0, "reviews": 0, "recommendation": "추천사항" }, - "gangnamUnni": { "score": 0-100, "rating": 0, "reviews": 0, "recommendation": "추천사항" }, - "website": { "score": 0-100, "issues": [], "recommendation": "추천사항", "trackingPixels": [{"name": "Google Analytics|Facebook Pixel|Naver Analytics|etc", "installed": true}], "snsLinksOnSite": true, "additionalDomains": [{"domain": "example.com", "purpose": "용도"}], "mainCTA": "주요 전환 유도 요소" } + "naverBlog": { "score": 0-100, "status": "active|inactive|weak", "posts": 0, "recommendation": "추천사항", "diagnosis": [{"issue": "구체적 문제", "severity": "critical|warning|good", "recommendation": "개선 방안"}] }, + "instagram": { "score": 0-100, "status": "active|inactive|weak", "followers": 0, "recommendation": "추천사항", "diagnosis": [{"issue": "구체적 문제", "severity": "critical|warning|good", "recommendation": "개선 방안"}] }, + "youtube": { "score": 0-100, "status": "active|inactive|weak", "subscribers": 0, "recommendation": "추천사항", "diagnosis": [{"issue": "구체적 문제", "severity": "critical|warning|good", "recommendation": "개선 방안"}] }, + "naverPlace": { "score": 0-100, "rating": 0, "reviews": 0, "recommendation": "추천사항", "diagnosis": [{"issue": "구체적 문제", "severity": "critical|warning|good", "recommendation": "개선 방안"}] }, + "gangnamUnni": { "score": 0-100, "rating": 0, "reviews": 0, "recommendation": "추천사항", "diagnosis": [{"issue": "구체적 문제", "severity": "critical|warning|good", "recommendation": "개선 방안"}] }, + "website": { "score": 0-100, "issues": [], "recommendation": "추천사항", "trackingPixels": [{"name": "Google Analytics|Facebook Pixel|Naver Analytics|etc", "installed": true}], "snsLinksOnSite": true, "additionalDomains": [{"domain": "example.com", "purpose": "용도"}], "mainCTA": "주요 전환 유도 요소", "diagnosis": [{"issue": "구체적 문제", "severity": "critical|warning|good", "recommendation": "개선 방안"}] } }, + "newChannelProposals": [ + { "channel": "제안 채널명", "priority": "P0|P1|P2", "rationale": "채널 개설 근거" } + ], "competitors": [ { "name": "경쟁병원명", "strengths": ["강점1"], "weaknesses": ["약점1"], "marketingChannels": ["채널1"] } ], @@ -130,7 +135,7 @@ ${JSON.stringify(analyzeResult.data?.analysis || {}, null, 2)} { "area": "포지셔닝", "asIs": "현재 시장 포지셔닝", "toBe": "목표 포지셔닝" } ], "kpiTargets": [ - { "metric": "지표명 (종합 점수, Instagram 팔로워, YouTube 구독자, 네이버 블로그 방문자, 월간 상담 문의 등)", "current": "현재 수치", "target3Month": "3개월 목표 (현실적)", "target12Month": "12개월 목표 (도전적)" } + { "metric": "외부 측정 가능한 지표만 포함 (종합 점수, Instagram 팔로워, YouTube 구독자/조회수, 네이버 블로그 검색 노출 수, 강남언니 리뷰 수, Google Maps 평점 등). 병원 내부에서만 알 수 있는 지표(상담 문의, 매출, 예약 수 등)는 절대 포함하지 마세요.", "current": "현재 수치", "target3Month": "3개월 목표 (현실적)", "target12Month": "12개월 목표 (도전적)" } ], "recommendations": [ { "priority": "high|medium|low", "category": "카테고리", "title": "제목", "description": "설명", "expectedImpact": "기대 효과" } diff --git a/supabase/functions/scrape-website/index.ts b/supabase/functions/scrape-website/index.ts index 5c00433..9dfda4a 100644 --- a/supabase/functions/scrape-website/index.ts +++ b/supabase/functions/scrape-website/index.ts @@ -122,9 +122,48 @@ Deno.serve(async (req) => { const searchData = await searchResponse.json(); + // Step 4: Extract branding (colors, fonts, logos) from the website + let brandingData: Record = {}; + try { + const brandResponse = await fetch("https://api.firecrawl.dev/v1/scrape", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${FIRECRAWL_API_KEY}`, + }, + body: JSON.stringify({ + url, + formats: ["json"], + jsonOptions: { + prompt: "Extract brand identity from this website: primary brand colors (hex codes), accent colors, background colors, font families used (headings and body), logo image URLs (header logo, favicon), and main tagline/slogan", + schema: { + type: "object", + properties: { + primaryColor: { type: "string" }, + accentColor: { type: "string" }, + backgroundColor: { type: "string" }, + textColor: { type: "string" }, + headingFont: { type: "string" }, + bodyFont: { type: "string" }, + logoUrl: { type: "string" }, + faviconUrl: { type: "string" }, + tagline: { type: "string" }, + }, + }, + }, + waitFor: 3000, + }), + }); + const brandResult = await brandResponse.json(); + brandingData = brandResult.data?.json || {}; + } catch { + // Branding extraction is optional — don't fail the whole scrape + } + // Combine all data const result = { clinic: scrapeData.data?.json || {}, + branding: brandingData, siteLinks: scrapeData.data?.links || [], siteMap: mapData.success ? mapData.links || [] : [], reviews: searchData.data || [],