feat: vista Orgánico con SEO (GA4/GSC) y GEO (visibilidad IA)
Nueva sección con métricas Search Console/GA4, cimientos SEO y Share of Model por motor (ChatGPT/Gemini/AI Overviews). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
167
src/lib/organic.ts
Normal file
167
src/lib/organic.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
/** 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;
|
||||
};
|
||||
|
||||
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)}%`;
|
||||
}
|
||||
|
||||
/** SEO technical health checklist (Vivegreen-style foundations). */
|
||||
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;
|
||||
|
||||
/** GEO engines tracked (Pixelclip-style AI visibility). */
|
||||
export const GEO_ENGINES = [
|
||||
{ id: 'chatgpt', label: 'ChatGPT' },
|
||||
{ id: 'gemini', label: 'Gemini' },
|
||||
{ id: 'ai_overviews', label: 'Google AI Overviews' },
|
||||
] as const;
|
||||
Reference in New Issue
Block a user