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
|
since && until
|
||||||
? `&day=gte.${since}&day=lte.${until}`
|
? `&day=gte.${since}&day=lte.${until}`
|
||||||
: '';
|
: '';
|
||||||
const [oRes, gRes, iRes] = await Promise.all([
|
const [oRes, gRes, iRes, tRes] = await Promise.all([
|
||||||
fetch(
|
fetch(
|
||||||
`${base}/rest/v1/organic_daily?tenant_id=eq.${encodeURIComponent(tid)}&select=*&order=day.asc${dayFilter}`,
|
`${base}/rest/v1/organic_daily?tenant_id=eq.${encodeURIComponent(tid)}&select=*&order=day.asc${dayFilter}`,
|
||||||
{ headers, cache: 'no-store' }
|
{ 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`,
|
`${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' }
|
{ headers, cache: 'no-store' }
|
||||||
),
|
),
|
||||||
|
fetch(
|
||||||
|
`${base}/rest/v1/tenants?id=eq.${encodeURIComponent(tid)}&select=settings`,
|
||||||
|
{ headers, cache: 'no-store' }
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const warningParts: string[] = [];
|
const warningParts: string[] = [];
|
||||||
let organic: OrganicDailyRow[] = [];
|
let organic: OrganicDailyRow[] = [];
|
||||||
let geo: GeoSnapshot[] = [];
|
let geo: GeoSnapshot[] = [];
|
||||||
let integrations: unknown[] = [];
|
let integrations: unknown[] = [];
|
||||||
|
let workspace = {};
|
||||||
|
|
||||||
if (oRes.ok) organic = await oRes.json();
|
if (oRes.ok) organic = await oRes.json();
|
||||||
else {
|
else {
|
||||||
@@ -91,6 +96,10 @@ export async function GET(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (iRes.ok) integrations = await iRes.json();
|
if (iRes.ok) integrations = await iRes.json();
|
||||||
|
if (tRes.ok) {
|
||||||
|
const rows = await tRes.json();
|
||||||
|
workspace = rows[0]?.settings?.organico || {};
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
portfolioWide: false,
|
portfolioWide: false,
|
||||||
@@ -99,6 +108,7 @@ export async function GET(req: NextRequest) {
|
|||||||
organic,
|
organic,
|
||||||
geo,
|
geo,
|
||||||
integrations,
|
integrations,
|
||||||
|
workspace,
|
||||||
warning: warningParts[0] || null,
|
warning: warningParts[0] || null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -110,7 +120,7 @@ export async function GET(req: NextRequest) {
|
|||||||
.order('day', { ascending: true });
|
.order('day', { ascending: true });
|
||||||
if (since) organicQ = organicQ.gte('day', since);
|
if (since) organicQ = organicQ.gte('day', since);
|
||||||
if (until) organicQ = organicQ.lte('day', until);
|
if (until) organicQ = organicQ.lte('day', until);
|
||||||
const [o, ge, integ] = await Promise.all([
|
const [o, ge, integ, ten] = await Promise.all([
|
||||||
organicQ,
|
organicQ,
|
||||||
supabase
|
supabase
|
||||||
.from('geo_snapshots')
|
.from('geo_snapshots')
|
||||||
@@ -123,6 +133,7 @@ export async function GET(req: NextRequest) {
|
|||||||
'provider,status,enabled,external_account_id,last_sync_at,error_message'
|
'provider,status,enabled,external_account_id,last_sync_at,error_message'
|
||||||
)
|
)
|
||||||
.in('provider', ['ga4', 'search_console', 'tag_manager']),
|
.in('provider', ['ga4', 'search_console', 'tag_manager']),
|
||||||
|
supabase.from('tenants').select('settings').eq('id', tid).maybeSingle(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
@@ -132,6 +143,7 @@ export async function GET(req: NextRequest) {
|
|||||||
organic: o.data || [],
|
organic: o.data || [],
|
||||||
geo: ge.data || [],
|
geo: ge.data || [],
|
||||||
integrations: integ.data || [],
|
integrations: integ.data || [],
|
||||||
|
workspace: (ten.data?.settings as { organico?: unknown } | null)?.organico || {},
|
||||||
warning: o.error?.message || ge.error?.message || null,
|
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 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();
|
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') {
|
if (kind === 'organic') {
|
||||||
const day = typeof body.day === 'string' ? body.day.slice(0, 10) : '';
|
const day = typeof body.day === 'string' ? body.day.slice(0, 10) : '';
|
||||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(day)) {
|
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)}%`;
|
return `${formatNum(v, digits)}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** SEO technical health checklist (Vivegreen-style foundations). */
|
|
||||||
export const SEO_FOUNDATIONS = [
|
export const SEO_FOUNDATIONS = [
|
||||||
{
|
{
|
||||||
id: 'indexacion',
|
id: 'indexacion',
|
||||||
@@ -159,9 +158,509 @@ export const SEO_FOUNDATIONS = [
|
|||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/** GEO engines tracked (Pixelclip-style AI visibility). */
|
|
||||||
export const GEO_ENGINES = [
|
export const GEO_ENGINES = [
|
||||||
{ id: 'chatgpt', label: 'ChatGPT' },
|
{ id: 'chatgpt', label: 'ChatGPT' },
|
||||||
{ id: 'gemini', label: 'Gemini' },
|
{ id: 'gemini', label: 'Gemini' },
|
||||||
{ id: 'ai_overviews', label: 'Google AI Overviews' },
|
{ id: 'ai_overviews', label: 'Google AI Overviews' },
|
||||||
] as const;
|
] 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;
|
modules?: TenantModuleMap;
|
||||||
/** When true, modules were explicitly saved by agency (don't auto-overwrite). */
|
/** When true, modules were explicitly saved by agency (don't auto-overwrite). */
|
||||||
modulesLocked?: boolean;
|
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> {
|
export function defaultModulesAllOn(): Record<DashboardModuleId, boolean> {
|
||||||
|
|||||||
Reference in New Issue
Block a user