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:
4Ventures
2026-09-24 11:25:32 +02:00
parent 5c90bcf940
commit 81ede58330
4 changed files with 1322 additions and 599 deletions

View File

@@ -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)) {