feat: suite Orgánico tipo Semrush (SEO + GEO por secciones)
Navegación por visión general, competencia, prompts, percepción, auditoría, keywords, backlinks y prioridades accionables por cliente. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -54,7 +54,7 @@ export async function GET(req: NextRequest) {
|
||||
since && until
|
||||
? `&day=gte.${since}&day=lte.${until}`
|
||||
: '';
|
||||
const [oRes, gRes, iRes] = await Promise.all([
|
||||
const [oRes, gRes, iRes, tRes] = await Promise.all([
|
||||
fetch(
|
||||
`${base}/rest/v1/organic_daily?tenant_id=eq.${encodeURIComponent(tid)}&select=*&order=day.asc${dayFilter}`,
|
||||
{ headers, cache: 'no-store' }
|
||||
@@ -67,12 +67,17 @@ export async function GET(req: NextRequest) {
|
||||
`${base}/rest/v1/integrations?tenant_id=eq.${encodeURIComponent(tid)}&provider=in.(ga4,search_console,tag_manager)&select=provider,status,enabled,external_account_id,last_sync_at,error_message`,
|
||||
{ headers, cache: 'no-store' }
|
||||
),
|
||||
fetch(
|
||||
`${base}/rest/v1/tenants?id=eq.${encodeURIComponent(tid)}&select=settings`,
|
||||
{ headers, cache: 'no-store' }
|
||||
),
|
||||
]);
|
||||
|
||||
const warningParts: string[] = [];
|
||||
let organic: OrganicDailyRow[] = [];
|
||||
let geo: GeoSnapshot[] = [];
|
||||
let integrations: unknown[] = [];
|
||||
let workspace = {};
|
||||
|
||||
if (oRes.ok) organic = await oRes.json();
|
||||
else {
|
||||
@@ -91,6 +96,10 @@ export async function GET(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
if (iRes.ok) integrations = await iRes.json();
|
||||
if (tRes.ok) {
|
||||
const rows = await tRes.json();
|
||||
workspace = rows[0]?.settings?.organico || {};
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
portfolioWide: false,
|
||||
@@ -99,6 +108,7 @@ export async function GET(req: NextRequest) {
|
||||
organic,
|
||||
geo,
|
||||
integrations,
|
||||
workspace,
|
||||
warning: warningParts[0] || null,
|
||||
});
|
||||
}
|
||||
@@ -110,7 +120,7 @@ export async function GET(req: NextRequest) {
|
||||
.order('day', { ascending: true });
|
||||
if (since) organicQ = organicQ.gte('day', since);
|
||||
if (until) organicQ = organicQ.lte('day', until);
|
||||
const [o, ge, integ] = await Promise.all([
|
||||
const [o, ge, integ, ten] = await Promise.all([
|
||||
organicQ,
|
||||
supabase
|
||||
.from('geo_snapshots')
|
||||
@@ -123,6 +133,7 @@ export async function GET(req: NextRequest) {
|
||||
'provider,status,enabled,external_account_id,last_sync_at,error_message'
|
||||
)
|
||||
.in('provider', ['ga4', 'search_console', 'tag_manager']),
|
||||
supabase.from('tenants').select('settings').eq('id', tid).maybeSingle(),
|
||||
]);
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -132,6 +143,7 @@ export async function GET(req: NextRequest) {
|
||||
organic: o.data || [],
|
||||
geo: ge.data || [],
|
||||
integrations: integ.data || [],
|
||||
workspace: (ten.data?.settings as { organico?: unknown } | null)?.organico || {},
|
||||
warning: o.error?.message || ge.error?.message || null,
|
||||
});
|
||||
}
|
||||
@@ -159,9 +171,61 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
const body = await req.json().catch(() => ({}));
|
||||
const kind = body.kind === 'geo' ? 'geo' : 'organic';
|
||||
const kind =
|
||||
body.kind === 'geo'
|
||||
? 'geo'
|
||||
: body.kind === 'workspace'
|
||||
? 'workspace'
|
||||
: 'organic';
|
||||
const base = supabaseUrl();
|
||||
|
||||
if (kind === 'workspace') {
|
||||
// Merge organico workspace into tenants.settings
|
||||
const cur = await fetch(
|
||||
`${base}/rest/v1/tenants?id=eq.${encodeURIComponent(ctx.tenantId)}&select=settings`,
|
||||
{ headers, cache: 'no-store' }
|
||||
);
|
||||
let settings: Record<string, unknown> = {};
|
||||
if (cur.ok) {
|
||||
const rows = await cur.json();
|
||||
settings =
|
||||
rows[0]?.settings && typeof rows[0].settings === 'object'
|
||||
? { ...rows[0].settings }
|
||||
: {};
|
||||
}
|
||||
const prev =
|
||||
settings.organico && typeof settings.organico === 'object'
|
||||
? (settings.organico as Record<string, unknown>)
|
||||
: {};
|
||||
settings.organico = {
|
||||
...prev,
|
||||
...(body.workspace && typeof body.workspace === 'object'
|
||||
? body.workspace
|
||||
: {}),
|
||||
};
|
||||
const r = await fetch(
|
||||
`${base}/rest/v1/tenants?id=eq.${encodeURIComponent(ctx.tenantId)}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': 'application/json',
|
||||
Prefer: 'return=representation',
|
||||
},
|
||||
body: JSON.stringify({ settings }),
|
||||
}
|
||||
);
|
||||
if (!r.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: (await r.text()).slice(0, 300) },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json({
|
||||
workspace: (await r.json())[0]?.settings?.organico || settings.organico,
|
||||
});
|
||||
}
|
||||
|
||||
if (kind === 'organic') {
|
||||
const day = typeof body.day === 'string' ? body.day.slice(0, 10) : '';
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(day)) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -130,7 +130,6 @@ 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',
|
||||
@@ -159,9 +158,509 @@ export const SEO_FOUNDATIONS = [
|
||||
},
|
||||
] 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;
|
||||
|
||||
/* ── 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' | 'geo-metrics' | 'geo-competitors' | 'geo-categories' | 'seo-foundations';
|
||||
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',
|
||||
],
|
||||
},
|
||||
{
|
||||
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 ———
|
||||
{
|
||||
id: 'seo-overview',
|
||||
pillar: 'seo',
|
||||
group: 'Rendimiento del sitio',
|
||||
label: 'Visión general',
|
||||
blurb:
|
||||
'Hazte visible donde busca tu audiencia: datos de mercado, acción y resultados medibles.',
|
||||
actions: [
|
||||
'Conectar Search Console y GA4',
|
||||
'Cargar o sincronizar serie diaria',
|
||||
'Definir 3 prioridades del mes',
|
||||
],
|
||||
live: 'seo-metrics',
|
||||
},
|
||||
{
|
||||
id: 'seo-audit',
|
||||
pillar: 'seo',
|
||||
group: 'Rendimiento del sitio',
|
||||
label: 'Auditoría del sitio',
|
||||
blurb:
|
||||
'Consulta problemas de SEO que limitan posicionamiento: salud, contenido y UX.',
|
||||
actions: [
|
||||
'Auditar indexación y CWV',
|
||||
'Corregir errores críticos',
|
||||
'Plan de remediación por impacto',
|
||||
],
|
||||
live: 'seo-foundations',
|
||||
},
|
||||
{
|
||||
id: 'seo-rankings',
|
||||
pillar: 'seo',
|
||||
group: 'Rendimiento del sitio',
|
||||
label: 'Rastreo de posición',
|
||||
blurb:
|
||||
'Rastrea posicionamiento en Google (y más adelante Bing / Modo IA) para keywords de alto valor.',
|
||||
actions: [
|
||||
'Definir cartera de keywords',
|
||||
'Registrar posición media GSC',
|
||||
'Alertar caídas de top 10',
|
||||
],
|
||||
live: 'seo-metrics',
|
||||
},
|
||||
{
|
||||
id: 'seo-competition',
|
||||
pillar: 'seo',
|
||||
group: 'Análisis de la competencia',
|
||||
label: 'Visión general del dominio',
|
||||
blurb:
|
||||
'Supera a tu competencia con análisis SEO: tráfico, keywords y estrategias.',
|
||||
actions: [
|
||||
'Fijar 3 dominios rivales',
|
||||
'Comparar autoridad y temáticas',
|
||||
'Extraer oportunidades de contenido',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'seo-organic',
|
||||
pillar: 'seo',
|
||||
group: 'Análisis de la competencia',
|
||||
label: 'Posicionamiento orgánico',
|
||||
blurb:
|
||||
'Mide visibilidad orgánica y evolución de clics/impresiones frente al mercado.',
|
||||
actions: [
|
||||
'Revisar serie GSC MoM',
|
||||
'Identificar queries en ascenso/descenso',
|
||||
'Alinear con objetivos de negocio',
|
||||
],
|
||||
live: 'seo-metrics',
|
||||
},
|
||||
{
|
||||
id: 'seo-pages',
|
||||
pillar: 'seo',
|
||||
group: 'Análisis de la competencia',
|
||||
label: 'Páginas principales',
|
||||
blurb: 'Landings que concentran tráfico y conversión orgánica.',
|
||||
actions: [
|
||||
'Listar top páginas por clics',
|
||||
'Optimizar títulos/meta y UX',
|
||||
'Reforzar enlazado interno',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'seo-compare',
|
||||
pillar: 'seo',
|
||||
group: 'Análisis de la competencia',
|
||||
label: 'Comparación de dominios',
|
||||
blurb: 'Compara tu dominio con rivales en una sola vista.',
|
||||
actions: [
|
||||
'Elegir métricas comunes (tráfico, KW, backlinks)',
|
||||
'Marcar gaps críticos',
|
||||
'Asignar owners internos',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'seo-keyword-gap',
|
||||
pillar: 'seo',
|
||||
group: 'Análisis de la competencia',
|
||||
label: 'Brecha de palabras clave',
|
||||
blurb:
|
||||
'Keywords donde rivales rankean y tú no — hoja de ruta clara.',
|
||||
actions: [
|
||||
'Exportar gap vs top rivales',
|
||||
'Priorizar por intención de compra',
|
||||
'Crear o potenciar páginas',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'seo-backlink-gap',
|
||||
pillar: 'seo',
|
||||
group: 'Análisis de la competencia',
|
||||
label: 'Brecha de backlinks',
|
||||
blurb:
|
||||
'Dominios que enlazan a rivales y no a ti: autoridad y confianza.',
|
||||
actions: [
|
||||
'Listar dominios de referencia únicos de rivales',
|
||||
'Pitch / digital PR',
|
||||
'Monitorizar nuevos enlaces',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'seo-keywords',
|
||||
pillar: 'seo',
|
||||
group: 'Investigación de palabras clave',
|
||||
label: 'Visión general de keywords',
|
||||
blurb:
|
||||
'Palabras clave y prompts con intención de compra para Google y ChatGPT.',
|
||||
actions: [
|
||||
'Mapear KW → intención → URL',
|
||||
'Detectar huecos de cobertura',
|
||||
'Alinear con calendario editorial',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'seo-keyword-magic',
|
||||
pillar: 'seo',
|
||||
group: 'Investigación de palabras clave',
|
||||
label: 'Keyword Magic / ideas',
|
||||
blurb: 'Explora ideas de keywords y variantes semánticas.',
|
||||
actions: [
|
||||
'Sembrar seed keywords del negocio',
|
||||
'Agrupar por clusters',
|
||||
'Seleccionar quick wins',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'seo-strategy',
|
||||
pillar: 'seo',
|
||||
group: 'Investigación de palabras clave',
|
||||
label: 'Keyword Strategy Builder',
|
||||
blurb: 'Construye la estrategia de keywords del trimestre.',
|
||||
actions: [
|
||||
'Definir pilares y clusters',
|
||||
'Asignar prioridad P1/P2/P3',
|
||||
'Vincular a OKRs',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'seo-content-ideas',
|
||||
pillar: 'seo',
|
||||
group: 'Ideas de contenido',
|
||||
label: 'Ideas de contenido',
|
||||
blurb:
|
||||
'Temas en tendencia, planes y briefings optimizados para posicionar.',
|
||||
actions: [
|
||||
'Topic research del mes',
|
||||
'Briefs con outline + KW + FAQs',
|
||||
'Publicar y medir en GSC',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'seo-writing',
|
||||
pillar: 'seo',
|
||||
group: 'Ideas de contenido',
|
||||
label: 'SEO Writing Assistant',
|
||||
blurb: 'Asistencia para redactar y optimizar on-page.',
|
||||
actions: [
|
||||
'Revisar densidad y entidades',
|
||||
'Mejorar claridad y E-E-A-T',
|
||||
'Checklist pre-publicación',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'seo-topics',
|
||||
pillar: 'seo',
|
||||
group: 'Ideas de contenido',
|
||||
label: 'Topic Research',
|
||||
blurb: 'Tendencias de temas que alimentan SEO y GEO.',
|
||||
actions: [
|
||||
'Cruzar topics con prompts IA',
|
||||
'Detectar ángulos diferenciales',
|
||||
'Planificar pieza pilar + satélites',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'seo-template',
|
||||
pillar: 'seo',
|
||||
group: 'Ideas de contenido',
|
||||
label: 'SEO Content Template',
|
||||
blurb: 'Plantillas de contenido basadas en lo que rankea.',
|
||||
actions: [
|
||||
'Analizar SERP / respuestas IA',
|
||||
'Definir estructura ganadora',
|
||||
'Reutilizar en el equipo',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'seo-backlinks',
|
||||
pillar: 'seo',
|
||||
group: 'Generación de enlaces',
|
||||
label: 'Backlinks',
|
||||
blurb:
|
||||
'Gana autoridad con backlinks de calidad que impulsan confianza y SEO.',
|
||||
actions: [
|
||||
'Auditar perfil actual',
|
||||
'Campañas de link earning',
|
||||
'Desautorizar tóxicos',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'seo-refdomains',
|
||||
pillar: 'seo',
|
||||
group: 'Generación de enlaces',
|
||||
label: 'Dominios de referencia',
|
||||
blurb: 'Diversidad y calidad de dominios que te enlazan.',
|
||||
actions: [
|
||||
'Medir referidos únicos',
|
||||
'Buscar nichos afines',
|
||||
'Relaciones con medios / partners',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'seo-backlink-audit',
|
||||
pillar: 'seo',
|
||||
group: 'Generación de enlaces',
|
||||
label: 'Backlink Audit',
|
||||
blurb: 'Salud del perfil de enlaces y riesgo reputacional.',
|
||||
actions: [
|
||||
'Clasificar enlaces',
|
||||
'Disavow si procede',
|
||||
'Informe trimestral',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'seo-onpage',
|
||||
pillar: 'seo',
|
||||
group: 'Extras',
|
||||
label: 'On Page SEO Checker',
|
||||
blurb: 'Comprueba on-page en URLs prioritarias.',
|
||||
actions: [
|
||||
'Auditar top landings',
|
||||
'Corregir title/H1/internal links',
|
||||
'Re-crawl',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'seo-traffic-insights',
|
||||
pillar: 'seo',
|
||||
group: 'Extras',
|
||||
label: 'Organic Traffic Insights',
|
||||
blurb:
|
||||
'Cruza GSC + GA4: cómo el orgánico mueve tráfico y conversiones reales.',
|
||||
actions: [
|
||||
'Conectar GA4',
|
||||
'Atribuir landings a leads/ventas',
|
||||
'Informar ROI orgánico',
|
||||
],
|
||||
live: 'seo-metrics',
|
||||
},
|
||||
];
|
||||
|
||||
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<string, OrganicoSection[]>();
|
||||
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<Record<OrganicoSectionId, string>>;
|
||||
};
|
||||
|
||||
@@ -100,6 +100,11 @@ export type TenantSettings = {
|
||||
modules?: TenantModuleMap;
|
||||
/** When true, modules were explicitly saved by agency (don't auto-overwrite). */
|
||||
modulesLocked?: boolean;
|
||||
/** SEO/GEO workspace notes & priorities (Orgánico). */
|
||||
organico?: {
|
||||
priorities?: { seo: string[]; geo: string[] };
|
||||
sectionNotes?: Record<string, string>;
|
||||
};
|
||||
};
|
||||
|
||||
export function defaultModulesAllOn(): Record<DashboardModuleId, boolean> {
|
||||
|
||||
Reference in New Issue
Block a user