/** Types + helpers for SEO (GA4/GSC) and GEO (AI visibility). */ export type OrganicDailyRow = { day?: string; date?: string; gsc_clicks?: number; gsc_impressions?: number; gsc_ctr?: number | null; gsc_avg_position?: number | null; ga4_sessions?: number; ga4_users?: number; ga4_organic_sessions?: number; ga4_conversions?: number; source?: string; }; export type GeoEngineScores = { chatgpt?: number; gemini?: number; ai_overviews?: number; }; export type GeoCompetitor = { brand: string; share: number; rank?: number; }; export type GeoCategory = { category: string; visibility: number; rank?: number; }; export type GeoSource = { name: string; share: number; kind?: 'official' | 'pharmacy' | 'media' | 'other'; }; export type GeoSnapshot = { id?: string; tenant_id?: string; label?: string; period_start?: string | null; period_end?: string | null; share_of_model?: number | null; engines?: GeoEngineScores; official_citation_pct?: number | null; sentiment_score?: number | null; competitors?: GeoCompetitor[]; categories?: GeoCategory[]; top_sources?: GeoSource[]; prompts_analyzed?: number | null; responses_analyzed?: number | null; study_url?: string | null; notes?: string | null; created_at?: string; }; export type IntegrationLite = { provider: string; status?: string; enabled?: boolean; external_account_id?: string | null; last_sync_at?: string | null; error_message?: string | null; meta?: { property_id?: string; site_url?: string }; }; export type SeoAuditCheckRow = { id: string; category: string; title: string; status: 'ok' | 'info' | 'warn' | 'critical'; detail: string; }; export type SeoAuditRow = { id?: string; tenant_id?: string; site_url?: string; score?: number; grade?: string; warning_count?: number; critical_count?: number; checks?: SeoAuditCheckRow[]; summary?: string | null; created_at?: string; }; function n(v: unknown): number { const x = Number(v); return Number.isFinite(x) ? x : 0; } export type OrganicTotals = { gscClicks: number; gscImpressions: number; gscCtr: number; gscAvgPosition: number; ga4Sessions: number; ga4Users: number; ga4Organic: number; ga4Conversions: number; days: number; }; export function sumOrganic(rows: OrganicDailyRow[]): OrganicTotals { let clicks = 0; let impr = 0; let posW = 0; let posN = 0; let sessions = 0; let users = 0; let organic = 0; let conv = 0; for (const r of rows) { clicks += n(r.gsc_clicks); impr += n(r.gsc_impressions); const pos = r.gsc_avg_position; if (pos != null && Number.isFinite(Number(pos))) { posW += Number(pos) * Math.max(1, n(r.gsc_impressions)); posN += Math.max(1, n(r.gsc_impressions)); } sessions += n(r.ga4_sessions); users += n(r.ga4_users); organic += n(r.ga4_organic_sessions); conv += n(r.ga4_conversions); } return { gscClicks: clicks, gscImpressions: impr, gscCtr: impr > 0 ? (100 * clicks) / impr : 0, gscAvgPosition: posN > 0 ? posW / posN : 0, ga4Sessions: sessions, ga4Users: users, ga4Organic: organic, ga4Conversions: conv, days: rows.length, }; } export function formatNum(v: number, digits = 0): string { return v.toLocaleString('es-ES', { maximumFractionDigits: digits, minimumFractionDigits: digits, }); } export function formatPct(v: number, digits = 1): string { return `${formatNum(v, digits)}%`; } export const SEO_FOUNDATIONS = [ { id: 'indexacion', title: 'Indexación y rastreo', hint: 'Sitemap, robots, casuísticas de URLs dinámicas', }, { id: 'arquitectura', title: 'Arquitectura y enlazado interno', hint: 'Distribuir autoridad hacia landings clave', }, { id: 'render', title: 'Renderizado y Core Web Vitals', hint: 'Velocidad, JS crítico, UX móvil', }, { id: 'contenido', title: 'Contenido indexable', hint: 'Guías, FAQs, fichas que la IA y Google puedan citar', }, { id: 'medicion', title: 'Medición GA4 + GSC', hint: 'Property + propiedad Search Console conectadas', }, ] as const; export const GEO_ENGINES = [ { id: 'chatgpt', label: 'ChatGPT' }, { id: 'gemini', label: 'Gemini' }, { id: 'ai_overviews', label: 'Google AI Overviews' }, ] as const; /* ── Product IA (Semrush-style) ─────────────────────────────────── */ export type OrganicoPillar = 'seo' | 'geo'; export type OrganicoSectionId = // GEO | 'geo-overview' | 'geo-competition' | 'geo-prompts' | 'geo-brand' | 'geo-perception' | 'geo-narrative' | 'geo-questions' | 'geo-audit' | 'geo-tracking' | 'geo-content' | 'geo-roi' // SEO | 'seo-overview' | 'seo-audit' | 'seo-rankings' | 'seo-competition' | 'seo-domain' | 'seo-organic' | 'seo-pages' | 'seo-compare' | 'seo-keyword-gap' | 'seo-backlink-gap' | 'seo-keywords' | 'seo-keyword-magic' | 'seo-strategy' | 'seo-content-ideas' | 'seo-writing' | 'seo-topics' | 'seo-template' | 'seo-backlinks' | 'seo-refdomains' | 'seo-backlink-audit' | 'seo-onpage' | 'seo-traffic-insights'; export type OrganicoSection = { id: OrganicoSectionId; pillar: OrganicoPillar; group: string; label: string; blurb: string; /** What the user should do next when empty. */ actions: string[]; live?: | 'seo-metrics' | 'seo-foundations' | 'seo-rankings' | 'seo-trend' | 'seo-pages' | 'seo-queries' | 'seo-cross' | 'seo-content' | 'geo-metrics' | 'geo-competitors' | 'geo-categories'; soon?: boolean; }; export const ORGANICO_SECTIONS: OrganicoSection[] = [ // ——— GEO ——— { id: 'geo-overview', pillar: 'geo', group: 'Visibilidad en la IA', label: 'Visión general', blurb: 'Monitoriza la visibilidad de tu marca en la IA. Share of Model, motores y prioridades claras de mejora.', actions: [ 'Registrar medición GEO (Share of Model + motores)', 'Priorizar 3 prompts donde no apareces', 'Revisar % de citas a web oficial', ], live: 'geo-metrics', }, { id: 'geo-competition', pillar: 'geo', group: 'Visibilidad en la IA', label: 'Análisis de la competencia', blurb: 'Identifica prompts, menciones y fuentes donde tus rivales aparecen pero tú no, y reduce esa brecha.', actions: [ 'Listar competidores con más Share of Voice', 'Mapear fuentes que citan a rivales y no a ti', 'Definir 5 acciones para cerrar la brecha', ], live: 'geo-competitors', }, { id: 'geo-prompts', pillar: 'geo', group: 'Visibilidad en la IA', label: 'Investigación de prompts', blurb: 'Descubre los prompts que generan conversaciones de IA en tu sector y las fuentes de las respuestas.', actions: [ 'Clusterizar prompts por intención (informativa / compra / comparativa)', 'Marcar prompts prioritarios por volumen o negocio', 'Anotar marcas y URLs citadas en cada respuesta', ], soon: true, }, { id: 'geo-brand', pillar: 'geo', group: 'Rendimiento de marca', label: 'Rendimiento de marca', blurb: 'Verifica puntuaciones de visibilidad en IA y los prompts más relevantes para tu marca.', actions: [ 'Comparar Share of Model vs top 3', 'Filtrar menciones por plataforma (ChatGPT / Gemini / AIO)', 'Detectar caídas día a día cuando haya rastreo', ], live: 'geo-metrics', }, { id: 'geo-perception', pillar: 'geo', group: 'Rendimiento de marca', label: 'Percepción', blurb: 'Evalúa sentimiento positivo/negativo y cómo te percibe cada plataforma de IA.', actions: [ 'Clasificar menciones + / − / neutro', 'Anotar narrativas por motor', 'Priorizar correcciones de reputación', ], }, { id: 'geo-narrative', pillar: 'geo', group: 'Rendimiento de marca', label: 'Elementos narrativos', blurb: 'Aduéñate de la narrativa: mensajes, claims y atributos que la IA asocia a tu marca.', actions: [ 'Extraer claims recurrentes en respuestas', 'Contrastar con messaging oficial', 'Definir mensajes a reforzar en contenido citables', ], }, { id: 'geo-questions', pillar: 'geo', group: 'Rendimiento de marca', label: 'Preguntas', blurb: 'Las preguntas reales de la audiencia orientan el contenido que la IA citará.', actions: [ 'Listar top preguntas por categoría', 'Crear FAQ / guías que respondan con datos propios', 'Enlazar a landings de conversión', ], live: 'geo-categories', }, { id: 'geo-audit', pillar: 'geo', group: 'Impulso y monitorización', label: 'Auditoría del sitio (GEO)', blurb: 'Elimina barreras a la visibilidad: que la IA entienda, confíe y cite tu web.', actions: [ 'Revisar schema, autoría y E-E-A-T', 'Asegurar respuestas claras y citables en páginas clave', 'Mejorar velocidad y acceso de crawlers', ], live: 'seo-foundations', }, { id: 'geo-tracking', pillar: 'geo', group: 'Impulso y monitorización', label: 'Rastreo de prompts', blurb: 'Rastrea visibilidad a diario, detecta prompts prioritarios y obtén recomendaciones de crecimiento.', actions: [ 'Fijar cartera de prompts a monitorizar', 'Registrar aparición / no aparición por motor', 'Alertar caídas de Share of Model', ], }, { id: 'geo-content', pillar: 'geo', group: 'Impulso y monitorización', label: 'Creación de contenidos', blurb: 'Prepara contenido para búsqueda de IA: fortalece visibilidad y sentimiento.', actions: [ 'Briefs a partir de prompts ganadores de competidores', 'Optimizar para cita (datos, definiciones, tablas)', 'Publicar y re-medir Share of Model', ], }, { id: 'geo-roi', pillar: 'geo', group: 'Impulso y monitorización', label: 'ROI de visibilidad IA', blurb: 'Informes de visibilidad IA junto a tráfico y conversiones (GA4). Disponible en breve.', actions: [ 'Conectar GA4', 'Cruzar sesiones orgánicas con picos de menciones IA', 'Informe ejecutivo con un clic (próximamente)', ], soon: true, }, // ——— SEO (solo secciones con datos reales GSC/GA4 o checklist operable) ——— { id: 'seo-overview', pillar: 'seo', group: 'Rendimiento del sitio', label: 'Visión general', blurb: 'KPIs diarios GA4 + GSC. El histórico vive en organic_daily (nosotros lo acumulamos; la API de GSC sola no basta).', actions: [ 'Conectar Search Console y GA4', 'Revisar serie 28d', 'Definir prioridades del mes', ], live: 'seo-metrics', }, { id: 'seo-audit', pillar: 'seo', group: 'Rendimiento del sitio', label: 'Auditoría del sitio', blurb: 'Checklist operativo de salud SEO (indexación, arquitectura, CWV, contenido, medición). No es un crawl Semrush: es la base que hay que cerrar con los datos conectados.', actions: [ 'Marcar OK/KO por bloque', 'Priorizar indexación + medición', 'CWV en landings de conversión', ], live: 'seo-foundations', }, { id: 'seo-rankings', pillar: 'seo', group: 'Rendimiento del sitio', label: 'Rastreo de posición', blurb: 'Sí: posición media diaria en organic_daily (histórico nuestro) + ranking actual de queries GSC. No es un rank tracker de 10k KW tipo Semrush; es lo que GSC reporta para tus impresiones reales.', actions: [ 'Mirar tendencia de posición media', 'Atacar queries en pos. 4–15 (quick wins)', 'Ampliar cartera vía contenido', ], live: 'seo-rankings', }, { id: 'seo-organic', pillar: 'seo', group: 'Tráfico y páginas', label: 'Posicionamiento orgánico', blurb: 'Evolución de clics e impresiones (visibilidad). Distinto de «Páginas principales».', actions: [ 'Leer tendencia MoM de clics', 'Cruzar con campañas de contenido', 'Alertar caídas bruscas', ], live: 'seo-trend', }, { id: 'seo-pages', pillar: 'seo', group: 'Tráfico y páginas', label: 'Páginas principales', blurb: 'URLs que concentran clics GSC + landings orgánicas GA4. No es el mismo panel que posicionamiento.', actions: [ 'Optimizar top 10 URLs', 'Alinear title/H1/CTA', 'Enlazar desde hubs', ], live: 'seo-pages', }, { id: 'seo-keywords', pillar: 'seo', group: 'Keywords', label: 'Visión general de keywords', blurb: 'Queries reales con impresiones/clics/CTR/posición desde Search Console (28d).', actions: [ 'Mapear KW → URL', 'Separar informativas vs compra', 'Priorizar quick wins', ], live: 'seo-queries', }, { id: 'seo-traffic-insights', pillar: 'seo', group: 'Keywords', label: 'Organic Traffic Insights', blurb: 'Cruce GSC (páginas) + GA4 (sesiones orgánicas / landings) para ver qué rankea y qué convierte.', actions: [ 'Comparar top pages GSC vs landings GA4', 'Detectar páginas con clics y poca conversión', 'Informar ROI orgánico', ], live: 'seo-cross', }, { id: 'seo-competition', pillar: 'seo', group: 'Competencia y enlaces', label: 'Competencia / backlinks', blurb: 'Requiere Ahrefs/Semrush u otra fuente de backlinks y dominio rival. No está en GSC/GA4. Aquí solo el marco de trabajo hasta integrar una herramienta.', actions: [ 'Fijar 3 dominios rivales', 'Importar gap KW / backlinks desde herramienta externa', 'Priorizar 5 acciones/mes', ], soon: true, }, { id: 'seo-content-ideas', pillar: 'seo', group: 'Contenido', label: 'Ideas de contenido', blurb: 'Briefs a partir de queries GSC con tracción (clics o impresiones altas y CTR bajo).', actions: [ 'Partir de queries con CTR bajo', 'Crear/ampliar pieza', 'Medir de nuevo en 28d', ], live: 'seo-content', }, ]; export function sectionsForPillar(pillar: OrganicoPillar): OrganicoSection[] { return ORGANICO_SECTIONS.filter((s) => s.pillar === pillar); } export function groupsForPillar( pillar: OrganicoPillar ): { group: string; items: OrganicoSection[] }[] { const list = sectionsForPillar(pillar); const order: string[] = []; const map = new Map(); for (const s of list) { if (!map.has(s.group)) { map.set(s.group, []); order.push(s.group); } map.get(s.group)!.push(s); } return order.map((group) => ({ group, items: map.get(group)! })); } export type OrganicoWorkspace = { priorities?: { seo: string[]; geo: string[] }; sectionNotes?: Partial>; }; /** Prioridades por defecto (SEO/GEO expert) cuando el workspace está vacío. */ export const DEFAULT_EXPERT_PRIORITIES: { seo: string[]; geo: string[] } = { seo: [ 'Verificar GSC (URL-prefix vs sc-domain) y GA4 property: datos diarios en organic_daily', 'Top landings por clics GSC: title/H1/CTA alineados con intención de búsqueda', 'Cerrar gap de indexación: sitemap, noindex accidental, soft-404', 'Core Web Vitals móvil en landings de conversión (LCP < 2.5s)', 'Keyword gap vs 3 competidores: 10 URLs nuevas o ampliadas este mes', 'Enlazado interno desde hub → money pages (autoridad hacia conversión)', 'Cruzar sesiones orgánicas GA4 con leads CRM (mismo embudo que ads)', ], geo: [ 'Definir cartera de 30–50 prompts de compra/comparativa del sector', 'Medir Share of Model en ChatGPT, Gemini y AI Overviews (baseline)', 'Aumentar % de citas a web oficial (schema, autoría, datos propios)', 'FAQs citables: respuesta directa + tabla/dato en las 10 URLs clave', 'Mapear fuentes que citan a competidores y no a la marca', 'Briefs de contenido a partir de prompts donde SoM = 0', 'Re-medir mensualmente y priorizar caídas de sentimiento / menciones', ], };