Per-tenant brandbooks power ads, RRSS calendars, LinkedIn B2B and blog; Nov calendars now create from trends + client DNA instead of copying history. Co-authored-by: Cursor <cursoragent@cursor.com>
572 lines
16 KiB
TypeScript
572 lines
16 KiB
TypeScript
/**
|
||
* Brandbook per tenant — source of truth for ads, RRSS calendars,
|
||
* LinkedIn B2B and corporate blog strategy.
|
||
* Stored in documents as type=brandbook (one active doc per tenant).
|
||
*/
|
||
|
||
import { brandBrief } from '@/lib/calendar-generate-brand';
|
||
|
||
export const BRANDBOOK_DOC_TYPE = 'brandbook';
|
||
export const BRANDBOOK_DOC_TITLE = 'Brandbook';
|
||
|
||
export type BrandbookSectionId =
|
||
| 'identidad'
|
||
| 'voz'
|
||
| 'visual'
|
||
| 'audiencia'
|
||
| 'oferta'
|
||
| 'pilares'
|
||
| 'canales'
|
||
| 'ads'
|
||
| 'b2b'
|
||
| 'blog'
|
||
| 'compliance'
|
||
| 'fuentes';
|
||
|
||
export type Brandbook = {
|
||
version: 1;
|
||
updated_at?: string;
|
||
/** Identity */
|
||
brand_name: string;
|
||
tagline: string;
|
||
mission: string;
|
||
vision: string;
|
||
values: string[];
|
||
vertical: string;
|
||
/** Voice */
|
||
tone: string;
|
||
personality: string[];
|
||
voice_do: string[];
|
||
voice_dont: string[];
|
||
vocabulary_prefer: string[];
|
||
vocabulary_avoid: string[];
|
||
/** Visual */
|
||
colors_primary: string[];
|
||
colors_secondary: string[];
|
||
fonts: string[];
|
||
visual_notes: string;
|
||
logo_usage: string;
|
||
/** Audience */
|
||
icp: string;
|
||
personas: Array<{ name: string; description: string; pains: string }>;
|
||
geo: string;
|
||
languages: string[];
|
||
/** Offer */
|
||
products_services: string[];
|
||
differentiators: string[];
|
||
proof_points: string[];
|
||
pricing_notes: string;
|
||
/** Content pillars */
|
||
pillars_rrss: string[];
|
||
series_recurrentes: string[];
|
||
formats_priority: string[];
|
||
/** Channels */
|
||
channels: {
|
||
instagram: string;
|
||
facebook: string;
|
||
linkedin: string;
|
||
tiktok: string;
|
||
blog: string;
|
||
other: string;
|
||
};
|
||
/** Ads */
|
||
ads_usps: string[];
|
||
ads_objections: string[];
|
||
ads_ctas: string[];
|
||
ads_offers: string[];
|
||
ads_angles: string[];
|
||
/** B2B / LinkedIn */
|
||
linkedin_themes: string[];
|
||
linkedin_formats: string[];
|
||
thought_leadership: string[];
|
||
buyer_roles: string[];
|
||
/** Blog corporativo */
|
||
blog_themes: string[];
|
||
blog_seo_keywords: string[];
|
||
blog_cadence: string;
|
||
/** Compliance */
|
||
claims_forbidden: string[];
|
||
legal_notes: string;
|
||
sector_rules: string;
|
||
/** Uploaded / pasted source material */
|
||
source_excerpts: Array<{
|
||
id: string;
|
||
filename: string;
|
||
excerpt: string;
|
||
added_at: string;
|
||
}>;
|
||
freeform_notes: string;
|
||
};
|
||
|
||
export const BRANDBOOK_SECTIONS: {
|
||
id: BrandbookSectionId;
|
||
label: string;
|
||
hint: string;
|
||
}[] = [
|
||
{
|
||
id: 'identidad',
|
||
label: 'Identidad',
|
||
hint: 'Nombre, misión, visión, valores, vertical',
|
||
},
|
||
{
|
||
id: 'voz',
|
||
label: 'Voz y tono',
|
||
hint: 'Cómo habla la marca; do/don’t y léxico',
|
||
},
|
||
{
|
||
id: 'visual',
|
||
label: 'Visual',
|
||
hint: 'Colores, tipografías, logo y notas de arte',
|
||
},
|
||
{
|
||
id: 'audiencia',
|
||
label: 'Audiencia',
|
||
hint: 'ICP, personas, geo e idiomas',
|
||
},
|
||
{
|
||
id: 'oferta',
|
||
label: 'Oferta',
|
||
hint: 'Productos/servicios, diferenciadores, prueba',
|
||
},
|
||
{
|
||
id: 'pilares',
|
||
label: 'Pilares de contenido',
|
||
hint: 'Series RRSS y formatos prioritarios',
|
||
},
|
||
{
|
||
id: 'canales',
|
||
label: 'Canales',
|
||
hint: 'Rol de cada red y blog',
|
||
},
|
||
{
|
||
id: 'ads',
|
||
label: 'Anuncios',
|
||
hint: 'USPs, objeciones, CTAs, ofertas y ángulos',
|
||
},
|
||
{
|
||
id: 'b2b',
|
||
label: 'LinkedIn B2B',
|
||
hint: 'Temas, formatos y thought leadership',
|
||
},
|
||
{
|
||
id: 'blog',
|
||
label: 'Blog corporativo',
|
||
hint: 'Temas, SEO y cadencia',
|
||
},
|
||
{
|
||
id: 'compliance',
|
||
label: 'Compliance',
|
||
hint: 'Claims prohibidos y notas legales',
|
||
},
|
||
{
|
||
id: 'fuentes',
|
||
label: 'Fuentes',
|
||
hint: 'Textos subidos / pegados del brandbook',
|
||
},
|
||
];
|
||
|
||
export function emptyBrandbook(label = ''): Brandbook {
|
||
return {
|
||
version: 1,
|
||
brand_name: label,
|
||
tagline: '',
|
||
mission: '',
|
||
vision: '',
|
||
values: [],
|
||
vertical: '',
|
||
tone: '',
|
||
personality: [],
|
||
voice_do: [],
|
||
voice_dont: [],
|
||
vocabulary_prefer: [],
|
||
vocabulary_avoid: [],
|
||
colors_primary: [],
|
||
colors_secondary: [],
|
||
fonts: [],
|
||
visual_notes: '',
|
||
logo_usage: '',
|
||
icp: '',
|
||
personas: [],
|
||
geo: 'España',
|
||
languages: ['es'],
|
||
products_services: [],
|
||
differentiators: [],
|
||
proof_points: [],
|
||
pricing_notes: '',
|
||
pillars_rrss: [],
|
||
series_recurrentes: [],
|
||
formats_priority: ['Reel', 'Carrusel', 'Stories', 'Post'],
|
||
channels: {
|
||
instagram: '',
|
||
facebook: '',
|
||
linkedin: '',
|
||
tiktok: '',
|
||
blog: '',
|
||
other: '',
|
||
},
|
||
ads_usps: [],
|
||
ads_objections: [],
|
||
ads_ctas: [],
|
||
ads_offers: [],
|
||
ads_angles: [],
|
||
linkedin_themes: [],
|
||
linkedin_formats: ['Carrusel', 'Documento', 'Post texto', 'Video corto'],
|
||
thought_leadership: [],
|
||
buyer_roles: [],
|
||
blog_themes: [],
|
||
blog_seo_keywords: [],
|
||
blog_cadence: '',
|
||
claims_forbidden: [],
|
||
legal_notes: '',
|
||
sector_rules: '',
|
||
source_excerpts: [],
|
||
freeform_notes: '',
|
||
};
|
||
}
|
||
|
||
/** Seed a usable brandbook from hard-coded brand DNA + sensible defaults. */
|
||
export function seedBrandbook(slug: string, label: string): Brandbook {
|
||
const brief = brandBrief(slug, label);
|
||
const base = emptyBrandbook(label);
|
||
const seeds: Record<string, Partial<Brandbook>> = {
|
||
bps: {
|
||
vertical: 'Escuela de idiomas',
|
||
tagline: 'Inglés real, cultura pop y certificaciones',
|
||
mission:
|
||
'Ayudar a alumnos a hablar inglés con confianza para trabajo, viajes y exámenes.',
|
||
tone: brief.tone,
|
||
personality: ['cercano', 'divertido', 'riguroso', 'pop'],
|
||
voice_do: [
|
||
'Explicar con ejemplos reales',
|
||
'Usar series FALSE FRIEND / SLANG / MTW',
|
||
'Micro-lecciones accionables',
|
||
],
|
||
voice_dont: [
|
||
'Listas genéricas de Black Friday',
|
||
'Tono académico seco',
|
||
'Copiar títulos mes a mes',
|
||
],
|
||
pillars_rrss: brief.pillars,
|
||
series_recurrentes: [
|
||
'FALSE FRIEND',
|
||
'SLANG',
|
||
'Same word different level',
|
||
'MTW',
|
||
'Canción del día',
|
||
'English Through TV',
|
||
'Fix it',
|
||
],
|
||
products_services: [
|
||
'Cursos generales',
|
||
'Intensivos',
|
||
'Trinity / IELTS / Cambridge',
|
||
'Inglés oposiciones',
|
||
'Madrid / Málaga',
|
||
],
|
||
ads_usps: [
|
||
'Metodología práctica',
|
||
'Profes nativos / preparados',
|
||
'Certificaciones oficiales',
|
||
],
|
||
ads_ctas: ['Test de nivel', 'Reservar plaza', 'WhatsApp'],
|
||
ads_objections: [
|
||
'No tengo tiempo',
|
||
'Ya probé y no aprendí',
|
||
'Es caro',
|
||
],
|
||
linkedin_themes: [
|
||
'Inglés profesional',
|
||
'Employability',
|
||
'Casos de alumnos',
|
||
],
|
||
blog_themes: [
|
||
'Guías de certificación',
|
||
'Errores comunes',
|
||
'Cómo elegir curso',
|
||
],
|
||
channels: {
|
||
instagram: 'Alcance + comunidad (Reels + Stories)',
|
||
facebook: 'Comunidad local / padres',
|
||
linkedin: 'B2B ligero / empleabilidad',
|
||
tiktok: 'Opcional micro-lecciones',
|
||
blog: 'SEO certificaciones y objeciones',
|
||
other: '',
|
||
},
|
||
},
|
||
'talento-y-deporte': {
|
||
vertical: brief.vertical,
|
||
tone: brief.tone,
|
||
pillars_rrss: brief.pillars,
|
||
series_recurrentes: [
|
||
'Nuevo alumno',
|
||
'Beca académica',
|
||
'Beca deportiva',
|
||
'Campus USA',
|
||
'Objeciones',
|
||
'English tips',
|
||
],
|
||
products_services: [
|
||
'Becas deportivas USA',
|
||
'Acompañamiento académico',
|
||
'Campus visits',
|
||
],
|
||
ads_usps: ['Becas reales', 'Acompañamiento end-to-end', 'Historias de alumnos'],
|
||
ads_ctas: ['Agenda llamada', 'Evalúa tu perfil'],
|
||
linkedin_themes: ['Educación internacional', 'Deporte + estudios'],
|
||
blog_themes: ['Guía becas USA', 'Requisitos TOEFL/SAT', 'Vida en campus'],
|
||
},
|
||
'halal-safety': {
|
||
vertical: brief.vertical,
|
||
tone: brief.tone,
|
||
pillars_rrss: brief.pillars,
|
||
products_services: [
|
||
'Certificación Halal',
|
||
'Consultoría exportación',
|
||
'Formación',
|
||
],
|
||
ads_usps: ['Autoridad técnica', 'Mercados export', 'Proceso claro'],
|
||
ads_ctas: ['Solicitar info', 'Auditoría'],
|
||
linkedin_themes: [
|
||
'Exportación Halal',
|
||
'Normativa',
|
||
'Casos sector alimentación/cosmética',
|
||
],
|
||
thought_leadership: [
|
||
'Tendencias mercados musulmanes',
|
||
'Compliance vs marketing Halal',
|
||
],
|
||
blog_themes: ['Guías de certificación', 'Mercados clave', 'FAQ técnica'],
|
||
buyer_roles: ['Quality', 'Export manager', 'CEO pyme alimentaria'],
|
||
},
|
||
'brava-dermoestetica': {
|
||
vertical: brief.vertical,
|
||
tone: brief.tone,
|
||
pillars_rrss: brief.pillars,
|
||
products_services: ['Peelings', 'Tratamientos facial', 'Rutinas'],
|
||
ads_usps: ['Resultados naturales', 'Equipo médico', 'Protocolos personalizados'],
|
||
ads_ctas: ['Pedir cita', 'Consulta valoración'],
|
||
claims_forbidden: [
|
||
'Promesas milagro sin matiz',
|
||
'Antes/después sin consentimiento',
|
||
],
|
||
sector_rules: 'Publicidad sanitaria: claims prudentes, sin garantías absolutas.',
|
||
},
|
||
noberplast: {
|
||
vertical: brief.vertical,
|
||
tone: brief.tone,
|
||
pillars_rrss: brief.pillars,
|
||
formats_priority: ['Carrusel', 'Post LinkedIn', 'Documento', 'Reel'],
|
||
linkedin_themes: [
|
||
'Plásticos técnicos',
|
||
'Casos industriales',
|
||
'Innovación materiales',
|
||
],
|
||
thought_leadership: [
|
||
'Sostenibilidad industrial',
|
||
'Especificaciones técnicas',
|
||
],
|
||
buyer_roles: ['Ingeniería', 'Compras', 'Producción'],
|
||
blog_themes: ['Fichas técnicas', 'Aplicaciones sectoriales', 'FAQ producto'],
|
||
channels: {
|
||
instagram: 'Marca / producto visual',
|
||
facebook: 'Bajo',
|
||
linkedin: 'Canal principal B2B',
|
||
tiktok: 'No prioritario',
|
||
blog: 'SEO técnico + captación inbound',
|
||
other: '',
|
||
},
|
||
},
|
||
'4ventures-hub': {
|
||
vertical: brief.vertical,
|
||
tone: brief.tone,
|
||
pillars_rrss: brief.pillars,
|
||
linkedin_themes: ['Ops de agencia', 'Growth', 'Casos clientes'],
|
||
thought_leadership: ['Centro de mando', 'Atribución', 'Contenido que vende'],
|
||
},
|
||
};
|
||
|
||
const resolvedSlug = slug === 'blue-parrot-school' ? 'bps' : slug;
|
||
|
||
const extra = seeds[resolvedSlug] || {
|
||
vertical: brief.vertical,
|
||
tone: brief.tone,
|
||
pillars_rrss: brief.pillars,
|
||
products_services: [],
|
||
freeform_notes: brief.notes,
|
||
};
|
||
|
||
return {
|
||
...base,
|
||
...extra,
|
||
brand_name: label,
|
||
vertical: extra.vertical || brief.vertical,
|
||
tone: extra.tone || brief.tone,
|
||
pillars_rrss: extra.pillars_rrss?.length ? extra.pillars_rrss : brief.pillars,
|
||
freeform_notes: extra.freeform_notes || brief.notes,
|
||
updated_at: new Date().toISOString(),
|
||
};
|
||
}
|
||
|
||
export function parseBrandbook(raw: unknown): Brandbook | null {
|
||
if (!raw || typeof raw !== 'object') return null;
|
||
const o = raw as Partial<Brandbook>;
|
||
if (o.version !== 1 && !o.brand_name && !o.vertical) {
|
||
// Allow loose objects that look like brandbooks
|
||
if (!('tone' in o) && !('pillars_rrss' in o) && !('mission' in o)) {
|
||
return null;
|
||
}
|
||
}
|
||
const empty = emptyBrandbook(String(o.brand_name || ''));
|
||
return {
|
||
...empty,
|
||
...o,
|
||
version: 1,
|
||
values: arr(o.values),
|
||
personality: arr(o.personality),
|
||
voice_do: arr(o.voice_do),
|
||
voice_dont: arr(o.voice_dont),
|
||
vocabulary_prefer: arr(o.vocabulary_prefer),
|
||
vocabulary_avoid: arr(o.vocabulary_avoid),
|
||
colors_primary: arr(o.colors_primary),
|
||
colors_secondary: arr(o.colors_secondary),
|
||
fonts: arr(o.fonts),
|
||
personas: Array.isArray(o.personas) ? o.personas : [],
|
||
languages: arr(o.languages).length ? arr(o.languages) : ['es'],
|
||
products_services: arr(o.products_services),
|
||
differentiators: arr(o.differentiators),
|
||
proof_points: arr(o.proof_points),
|
||
pillars_rrss: arr(o.pillars_rrss),
|
||
series_recurrentes: arr(o.series_recurrentes),
|
||
formats_priority: arr(o.formats_priority),
|
||
channels: { ...empty.channels, ...(o.channels || {}) },
|
||
ads_usps: arr(o.ads_usps),
|
||
ads_objections: arr(o.ads_objections),
|
||
ads_ctas: arr(o.ads_ctas),
|
||
ads_offers: arr(o.ads_offers),
|
||
ads_angles: arr(o.ads_angles),
|
||
linkedin_themes: arr(o.linkedin_themes),
|
||
linkedin_formats: arr(o.linkedin_formats),
|
||
thought_leadership: arr(o.thought_leadership),
|
||
buyer_roles: arr(o.buyer_roles),
|
||
blog_themes: arr(o.blog_themes),
|
||
blog_seo_keywords: arr(o.blog_seo_keywords),
|
||
claims_forbidden: arr(o.claims_forbidden),
|
||
source_excerpts: Array.isArray(o.source_excerpts) ? o.source_excerpts : [],
|
||
};
|
||
}
|
||
|
||
function arr(v: unknown): string[] {
|
||
if (!Array.isArray(v)) return [];
|
||
return v.map((x) => String(x || '').trim()).filter(Boolean);
|
||
}
|
||
|
||
/** Lines / commas → string[] for form fields. */
|
||
export function linesToList(text: string): string[] {
|
||
return text
|
||
.split(/\n|,/)
|
||
.map((s) => s.trim())
|
||
.filter(Boolean);
|
||
}
|
||
|
||
export function listToLines(list: string[]): string {
|
||
return (list || []).join('\n');
|
||
}
|
||
|
||
/** Heuristic: fold uploaded plain text into freeform + optional lists. */
|
||
export function ingestSourceText(
|
||
book: Brandbook,
|
||
filename: string,
|
||
text: string
|
||
): Brandbook {
|
||
const excerpt = text.slice(0, 12000).trim();
|
||
if (!excerpt) return book;
|
||
const id = `src_${Date.now().toString(36)}`;
|
||
const next: Brandbook = {
|
||
...book,
|
||
source_excerpts: [
|
||
{
|
||
id,
|
||
filename: filename || 'pegado.txt',
|
||
excerpt,
|
||
added_at: new Date().toISOString(),
|
||
},
|
||
...(book.source_excerpts || []),
|
||
].slice(0, 20),
|
||
freeform_notes: [book.freeform_notes, `--- ${filename} ---\n${excerpt.slice(0, 4000)}`]
|
||
.filter(Boolean)
|
||
.join('\n\n')
|
||
.slice(0, 50000),
|
||
updated_at: new Date().toISOString(),
|
||
};
|
||
|
||
// Light keyword pulls into empty fields
|
||
const lower = excerpt.toLowerCase();
|
||
if (!next.mission && /misi[oó]n/.test(lower)) {
|
||
const m = excerpt.match(/misi[oó]n[:\s]+(.{20,200})/i);
|
||
if (m) next.mission = m[1].trim();
|
||
}
|
||
if (!next.tone && /tono[:\s]+(.{10,120})/i.test(excerpt)) {
|
||
const m = excerpt.match(/tono[:\s]+(.{10,120})/i);
|
||
if (m) next.tone = m[1].trim();
|
||
}
|
||
return next;
|
||
}
|
||
|
||
/** Compact prompt block for strategists / Anthropic. */
|
||
export function brandbookToPrompt(book: Brandbook): string {
|
||
const lines = [
|
||
`Marca: ${book.brand_name}`,
|
||
book.tagline && `Tagline: ${book.tagline}`,
|
||
book.vertical && `Vertical: ${book.vertical}`,
|
||
book.mission && `Misión: ${book.mission}`,
|
||
book.tone && `Tono: ${book.tone}`,
|
||
book.personality.length && `Personalidad: ${book.personality.join(', ')}`,
|
||
book.voice_do.length && `Hacer: ${book.voice_do.join('; ')}`,
|
||
book.voice_dont.length && `Evitar: ${book.voice_dont.join('; ')}`,
|
||
book.series_recurrentes.length &&
|
||
`Series RRSS: ${book.series_recurrentes.join(', ')}`,
|
||
book.pillars_rrss.length && `Pilares: ${book.pillars_rrss.join(', ')}`,
|
||
book.products_services.length &&
|
||
`Oferta: ${book.products_services.join(', ')}`,
|
||
book.differentiators.length &&
|
||
`Diferenciadores: ${book.differentiators.join(', ')}`,
|
||
book.icp && `ICP: ${book.icp}`,
|
||
book.ads_usps.length && `USPs ads: ${book.ads_usps.join('; ')}`,
|
||
book.ads_ctas.length && `CTAs: ${book.ads_ctas.join('; ')}`,
|
||
book.ads_objections.length &&
|
||
`Objeciones: ${book.ads_objections.join('; ')}`,
|
||
book.linkedin_themes.length &&
|
||
`LinkedIn: ${book.linkedin_themes.join(', ')}`,
|
||
book.thought_leadership.length &&
|
||
`Thought leadership: ${book.thought_leadership.join(', ')}`,
|
||
book.blog_themes.length && `Blog: ${book.blog_themes.join(', ')}`,
|
||
book.blog_seo_keywords.length &&
|
||
`SEO: ${book.blog_seo_keywords.join(', ')}`,
|
||
book.claims_forbidden.length &&
|
||
`Prohibido: ${book.claims_forbidden.join('; ')}`,
|
||
book.legal_notes && `Legal: ${book.legal_notes}`,
|
||
book.freeform_notes && `Notas: ${book.freeform_notes.slice(0, 1500)}`,
|
||
].filter(Boolean);
|
||
return lines.join('\n');
|
||
}
|
||
|
||
export function brandbookCompleteness(book: Brandbook): {
|
||
score: number;
|
||
missing: string[];
|
||
} {
|
||
const checks: [string, boolean][] = [
|
||
['Identidad (misión/vertical)', !!(book.mission || book.vertical)],
|
||
['Voz/tono', !!book.tone],
|
||
['Audiencia/ICP', !!(book.icp || book.personas.length)],
|
||
['Oferta', book.products_services.length > 0],
|
||
['Pilares/series RRSS', !!(book.pillars_rrss.length || book.series_recurrentes.length)],
|
||
['Ads (USP o CTA)', !!(book.ads_usps.length || book.ads_ctas.length)],
|
||
['LinkedIn B2B o blog', !!(book.linkedin_themes.length || book.blog_themes.length)],
|
||
];
|
||
const missing = checks.filter(([, ok]) => !ok).map(([l]) => l);
|
||
const score = Math.round(
|
||
((checks.length - missing.length) / checks.length) * 100
|
||
);
|
||
return { score, missing };
|
||
}
|