From 5e8fd66e352236c9fb3eccf3fe957857c937672d Mon Sep 17 00:00:00 2001 From: 4Ventures <4ventures@MacBook-Air-de-4Ventures.local> Date: Mon, 28 Sep 2026 18:50:56 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20Meta=20creatividades=20=E2=80=94=205=20t?= =?UTF-8?q?itulares,=205=20textos=20y=20descripci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- src/components/AdCreativesGallery.tsx | 124 ++++++++++++++++++++------ src/components/DriveConnectPanel.tsx | 3 + src/lib/ad-creatives-google.ts | 10 ++- src/lib/ad-creatives-meta.ts | 65 +++++++------- 4 files changed, 141 insertions(+), 61 deletions(-) diff --git a/src/components/AdCreativesGallery.tsx b/src/components/AdCreativesGallery.tsx index ae9b1f6..9dee4ad 100644 --- a/src/components/AdCreativesGallery.tsx +++ b/src/components/AdCreativesGallery.tsx @@ -16,19 +16,27 @@ export function proxiedMediaUrl(raw: string): string { } function adHeadlines(ad: AdCreativeCard): string[] { - if (ad.headlines?.length) return ad.headlines; + if (ad.headlines?.length) return ad.headlines.slice(0, 5); return (ad.headline || '') .split('\n') .map((s) => s.trim()) - .filter(Boolean); + .filter(Boolean) + .slice(0, 5); } -function adDescriptions(ad: AdCreativeCard): string[] { - if (ad.descriptions?.length) return ad.descriptions; +function adBodies(ad: AdCreativeCard): string[] { + if (ad.bodies?.length) return ad.bodies.slice(0, 5); + // Legacy: primaryText used to hold bodies return (ad.primaryText || '') .split('\n') .map((s) => s.trim()) - .filter(Boolean); + .filter(Boolean) + .slice(0, 5); +} + +function adLinkDescriptions(ad: AdCreativeCard): string[] { + if (ad.descriptions?.length) return ad.descriptions.slice(0, 5); + return []; } function adAssets(ad: AdCreativeCard): CreativeAsset[] { @@ -186,8 +194,9 @@ function CreativeCard({ }) { const preview = previewUrlFor(ad); const isVideo = ad.mediaType === 'video' || Boolean(ad.youtubeId); - const headline = adHeadlines(ad)[0] || ''; - const body = adDescriptions(ad)[0] || ''; + const headlines = adHeadlines(ad); + const bodies = adBodies(ad); + const linkDescs = adLinkDescriptions(ad); const assets = adAssets(ad); const extraCount = Math.max(0, assets.length - 1); @@ -206,14 +215,19 @@ function CreativeCard({ className="w-full h-full object-cover" loading="lazy" /> - ) : ad.platform === 'google' && headline ? ( + ) : ad.platform === 'google' && headlines.length ? (

Google · {ad.mediaType || 'rsa'}

-

- {headline} -

+ {headlines.slice(0, 3).map((h) => ( +

+ {h} +

+ ))}
) : (
@@ -259,18 +273,55 @@ function CreativeCard({

Conjunto: {ad.adsetName}

) : null}
- {(headline || body) && ( -
- {headline ? ( -

- {headline} -

+ {(headlines.length > 0 || bodies.length > 0 || linkDescs.length > 0) && ( +
+ {headlines.length > 0 ? ( +
+

+ Titulares ({headlines.length}) +

+
    + {headlines.map((h) => ( +
  • + {h} +
  • + ))} +
+
) : null} - {body ? ( -

{body}

+ {bodies.length > 0 ? ( +
+

+ Textos ({bodies.length}) +

+
    + {bodies.map((b) => ( +
  • + {b} +
  • + ))} +
+
+ ) : null} + {linkDescs.length > 0 ? ( +
+

+ Descripción +

+
    + {linkDescs.map((d) => ( +
  • + {d} +
  • + ))} +
+
) : null} {ad.cta ? ( -

+

CTA · {ad.cta}

) : null} @@ -298,7 +349,8 @@ export function CreativeZoomModal({ onClose: () => void; }) { const headlines = adHeadlines(ad); - const bodies = adDescriptions(ad); + const bodies = adBodies(ad); + const linkDescs = adLinkDescriptions(ad); const longH = ad.longHeadlines || []; const assets = adAssets(ad); const images = assets.filter((a) => a.kind === 'image' || a.kind === 'logo'); @@ -494,10 +546,10 @@ export function CreativeZoomModal({ {headlines.length > 0 ? (

- Titular + Titulares ({headlines.length})

    - {headlines.slice(0, 2).map((h) => ( + {headlines.map((h) => (
  • 0 ? (

    - Titulares largos + Titulares largos ({longH.length})

      - {longH.slice(0, 2).map((h) => ( + {longH.slice(0, 5).map((h) => (
    • 0 ? (

      - Texto + Textos ({bodies.length})

        - {bodies.slice(0, 2).map((b) => ( + {bodies.map((b) => (
      ) : null} + + {linkDescs.length > 0 ? ( +
      +

      + Descripción +

      +
        + {linkDescs.map((d) => ( +
      • + {d} +
      • + ))} +
      +
      + ) : null}
diff --git a/src/components/DriveConnectPanel.tsx b/src/components/DriveConnectPanel.tsx index c938722..189641e 100644 --- a/src/components/DriveConnectPanel.tsx +++ b/src/components/DriveConnectPanel.tsx @@ -107,6 +107,9 @@ export default function DriveConnectPanel() { Google Ads. Sirve para carpetas Drive, GA4, GSC y creatividades Google Ads. Si ves error de token caducado en Ads, pulsa Reconectar Google. + En Google Cloud el consent screen debe estar en{' '} + In production (si está + en Testing el refresh token caduca a los 7 días).

diff --git a/src/lib/ad-creatives-google.ts b/src/lib/ad-creatives-google.ts index b4d6842..d3bd76c 100644 --- a/src/lib/ad-creatives-google.ts +++ b/src/lib/ad-creatives-google.ts @@ -210,6 +210,7 @@ function cardsFromDaily( headline: agg.campaignName || agg.adName, primaryText: description, headlines: [agg.campaignName || agg.adName].filter(Boolean), + bodies: [], descriptions: [description], assets: [] as CreativeAsset[], status: undefined, @@ -497,8 +498,9 @@ WHERE ${dateWhere} headline: [...headlines, ...longHeadlines].join('\n'), primaryText: descriptions.join('\n'), headlines, + bodies: descriptions, longHeadlines, - descriptions, + descriptions: [], assets: entry.assets, linkUrl: entry.finalUrls[0], cta: uniq(entry.ctas)[0], @@ -553,8 +555,9 @@ WHERE ${dateWhere} headline: [...headlines, ...longHeadlines].join('\n'), primaryText: descriptions.join('\n'), headlines, + bodies: descriptions, longHeadlines, - descriptions, + descriptions: [], assets, linkUrl: (ad?.finalUrls || []).map(str).filter(Boolean)[0], cta: str(rda?.callToActionText) || undefined, @@ -562,8 +565,7 @@ WHERE ${dateWhere} status: enumName(row.adGroupAd?.status), }); if (paths.length) { - // encode paths into descriptions note for zoom - cards[cards.length - 1].descriptions = [ + cards[cards.length - 1].bodies = [ ...descriptions, `Path: /${paths.join('/')}`, ]; diff --git a/src/lib/ad-creatives-meta.ts b/src/lib/ad-creatives-meta.ts index dbf1bd7..42c1de8 100644 --- a/src/lib/ad-creatives-meta.ts +++ b/src/lib/ad-creatives-meta.ts @@ -48,10 +48,14 @@ export type AdCreativeCard = { /** Best preview URL (prefer vertical / portrait). */ thumbnailUrl: string; imageUrl: string; - /** Joined for backwards compat; prefer headlines/descriptions arrays. */ + /** Joined for backwards compat; prefer headlines/bodies/descriptions arrays. */ headline: string; primaryText: string; + /** Titulares (Meta titles / Google headlines), máx. 5. */ headlines: string[]; + /** Textos principales (Meta bodies), máx. 5. */ + bodies: string[]; + /** Descripción del enlace (Meta descriptions), suele ser 1. */ descriptions: string[]; longHeadlines?: string[]; assets: CreativeAsset[]; @@ -122,6 +126,7 @@ function detectMediaType(creative: Record): string { type Extracted = { headlines: string[]; + bodies: string[]; descriptions: string[]; assets: CreativeAsset[]; linkUrl: string; @@ -131,6 +136,7 @@ type Extracted = { function extractCreativePack(creative: Record): Extracted { const headlines: string[] = []; + const bodies: string[] = []; const descriptions: string[] = []; const assets: CreativeAsset[] = []; const hashes: string[] = []; @@ -155,50 +161,56 @@ function extractCreativePack(creative: Record): Extracted { }); }; - // Primary serving copy = object_story_spec (what Ads Manager shows as the ad). - // Do NOT dump every asset_feed_spec A/B variant into the card. const oss = creative.object_story_spec as Record | undefined; const link = oss?.link_data as Record | undefined; const video = oss?.video_data as Record | undefined; const photo = oss?.photo_data as Record | undefined; + // Story (anuncio publicado): 1 titular + texto + descripción de enlace. if (link?.name) headlines.push(str(link.name)); - if (link?.message) descriptions.push(str(link.message)); - else if (link?.description) descriptions.push(str(link.description)); + if (link?.message) bodies.push(str(link.message)); + if (link?.description) descriptions.push(str(link.description)); pushImg(link?.picture, '4:5 story', 1080, 1350); pushImg(link?.image_url, '4:5 story', 1080, 1350); pushHash(link?.image_hash); if (video?.title) headlines.push(str(video.title)); - if (video?.message) descriptions.push(str(video.message)); - else if (video?.link_description) descriptions.push(str(video.link_description)); + if (video?.message) bodies.push(str(video.message)); + if (video?.link_description) descriptions.push(str(video.link_description)); pushImg(video?.image_url, 'video_poster 9:16', 1080, 1920); pushHash(video?.image_hash); pushImg(photo?.url, 'photo 4:5', 1080, 1350); pushHash(photo?.image_hash); - if (!headlines.length && creative.title) headlines.push(str(creative.title)); - if (!descriptions.length && creative.body) descriptions.push(str(creative.body)); + if (creative.title) headlines.push(str(creative.title)); + if (creative.body) bodies.push(str(creative.body)); pushImg(creative.image_url, 'image 4:5', 1080, 1350); pushImg(creative.thumbnail_url, 'thumbnail 9:16', 1080, 1920); pushHash(creative.image_hash); - // Carousel: keep images; only first card's name as optional secondary headline. if (Array.isArray(link?.child_attachments)) { (link!.child_attachments as Record[]).forEach((c, i) => { pushImg(c.picture || c.image_url, `carousel-${i + 1} 4:5`, 1080, 1350); pushHash(c.image_hash); - if (i === 0 && c.name && !headlines.length) headlines.push(str(c.name)); - if (i === 0 && c.description && !descriptions.length) { - descriptions.push(str(c.description)); - } + if (c.name) headlines.push(str(c.name)); + if (c.message) bodies.push(str(c.message)); + if (c.description) descriptions.push(str(c.description)); }); } + // Dynamic creative / Advantage+: hasta 5 titles, 5 bodies, descriptions. const feed = creative.asset_feed_spec as Record | undefined; if (feed) { - // Images/videos from dynamic creative — label formats; texts only if still empty. + for (const t of (feed.titles as { text?: string }[]) || []) { + if (t?.text) headlines.push(str(t.text)); + } + for (const b of (feed.bodies as { text?: string }[]) || []) { + if (b?.text) bodies.push(str(b.text)); + } + for (const d of (feed.descriptions as { text?: string }[]) || []) { + if (d?.text) descriptions.push(str(d.text)); + } for (const img of (feed.images as Record[]) || []) { const labels = Array.isArray(img.adlabels) ? (img.adlabels as { name?: string }[]) @@ -206,10 +218,9 @@ function extractCreativePack(creative: Record): Extracted { .filter(Boolean) .join(' ') : ''; - const tag = labels || 'feed'; pushImg( img.url || img.permalink_url, - tag, + labels || 'feed', n(img.width) || undefined, n(img.height) || undefined ); @@ -227,16 +238,6 @@ function extractCreativePack(creative: Record): Extracted { }); } } - if (!headlines.length) { - const t = (feed.titles as { text?: string }[] | undefined)?.[0]?.text; - if (t) headlines.push(str(t)); - } - if (!descriptions.length) { - const b = - (feed.bodies as { text?: string }[] | undefined)?.[0]?.text || - (feed.descriptions as { text?: string }[] | undefined)?.[0]?.text; - if (b) descriptions.push(str(b)); - } } const linkUrl = @@ -248,8 +249,9 @@ function extractCreativePack(creative: Record): Extracted { const cta = str(creative.call_to_action_type) || str(ctaRaw); return { - headlines: uniq(headlines).slice(0, 3), - descriptions: uniq(descriptions).slice(0, 3), + headlines: uniq(headlines).slice(0, 5), + bodies: uniq(bodies).slice(0, 5), + descriptions: uniq(descriptions).slice(0, 5), assets, linkUrl, cta, @@ -549,6 +551,7 @@ function cardFromBundle( str(creative.image_url) || str(creative.thumbnail_url) || ''; const preview = pickVerticalPreview(assets, fallback); const headlines = pack.headlines; + const bodies = pack.bodies; const descriptions = pack.descriptions; return { platform: 'meta', @@ -565,8 +568,9 @@ function cardFromBundle( thumbnailUrl: preview, imageUrl: preview, headline: headlines.join('\n'), - primaryText: descriptions.join('\n'), + primaryText: bodies.join('\n') || descriptions.join('\n'), headlines, + bodies, descriptions, assets, linkUrl: pack.linkUrl || undefined, @@ -751,6 +755,7 @@ export function buildGoogleCardsFromDaily( headline: agg.adName, primaryText: agg.campaignName, headlines: agg.adName ? [agg.adName] : [], + bodies: [], descriptions: agg.campaignName ? [agg.campaignName] : [], assets: [], status: undefined,