Split Google Ads copy into titles, long titles and descriptions.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-10-06 13:44:38 +02:00
parent b9ddc7b806
commit d411de0086
2 changed files with 136 additions and 61 deletions

View File

@@ -179,6 +179,42 @@ function formatThumbs(ad: AdCreativeCard): { url: string; aspect: CreativeAspect
return out; return out;
} }
function CopyList({
label,
items,
strong,
muted,
}: {
label: string;
items: string[];
strong?: boolean;
muted?: boolean;
}) {
return (
<div>
<p className="text-[9px] uppercase tracking-wider text-gray-500 mb-1">
{label}
</p>
<ul className="space-y-1">
{items.map((h) => (
<li
key={h}
className={`leading-snug ${
strong
? 'text-gray-900 font-medium'
: muted
? 'text-gray-500'
: 'text-gray-600'
}`}
>
{h}
</li>
))}
</ul>
</div>
);
}
function MiniKpi({ label, value }: { label: string; value: string }) { function MiniKpi({ label, value }: { label: string; value: string }) {
return ( return (
<div className="rounded-lg border border-gray-200 bg-gray-50 px-2.5 py-2"> <div className="rounded-lg border border-gray-200 bg-gray-50 px-2.5 py-2">
@@ -363,6 +399,13 @@ function CreativeCard({
const headlines = adHeadlines(ad); const headlines = adHeadlines(ad);
const bodies = adBodies(ad); const bodies = adBodies(ad);
const linkDescs = adLinkDescriptions(ad); const linkDescs = adLinkDescriptions(ad);
const longTitles = ad.longHeadlines || [];
const googleDescs =
ad.platform === 'google'
? ad.descriptions?.length
? ad.descriptions
: bodies
: [];
const assets = adAssets(ad); const assets = adAssets(ad);
const extraCount = Math.max(0, assets.length - 1); const extraCount = Math.max(0, assets.length - 1);
const pmaxFormats = formatThumbs(ad); const pmaxFormats = formatThumbs(ad);
@@ -409,9 +452,9 @@ function CreativeCard({
{h} {h}
</p> </p>
))} ))}
{bodies[0] ? ( {googleDescs[0] ? (
<p className="text-[11px] text-gray-600 leading-snug line-clamp-3"> <p className="text-[11px] text-gray-600 leading-snug line-clamp-3">
{bodies[0]} {googleDescs[0]}
</p> </p>
) : null} ) : null}
</div> </div>
@@ -510,53 +553,52 @@ function CreativeCard({
<p className="text-xs text-gray-400">Conjunto: {ad.adsetName}</p> <p className="text-xs text-gray-400">Conjunto: {ad.adsetName}</p>
) : null} ) : null}
</div> </div>
{(headlines.length > 0 || bodies.length > 0 || linkDescs.length > 0) && ( {(headlines.length > 0 ||
longTitles.length > 0 ||
bodies.length > 0 ||
linkDescs.length > 0 ||
googleDescs.length > 0) && (
<div className="space-y-2.5 text-xs border border-gray-200 rounded-xl p-3 bg-gray-50"> <div className="space-y-2.5 text-xs border border-gray-200 rounded-xl p-3 bg-gray-50">
{ad.platform === 'google' ? (
<>
{headlines.length > 0 ? ( {headlines.length > 0 ? (
<div> <CopyList
<p className="text-[9px] uppercase tracking-wider text-gray-500 mb-1"> label={`Títulos (${headlines.length})`}
Titulares ({headlines.length}) items={headlines}
</p> strong
<ul className="space-y-1"> />
{headlines.map((h) => ( ) : null}
<li {longTitles.length > 0 ? (
key={h} <CopyList
className="text-gray-900 font-medium leading-snug" label={`Títulos largos (${longTitles.length})`}
> items={longTitles}
{h} strong
</li> />
))} ) : null}
</ul> {googleDescs.length > 0 ? (
</div> <CopyList
label={`Descripciones (${googleDescs.length})`}
items={googleDescs}
/>
) : null}
</>
) : (
<>
{headlines.length > 0 ? (
<CopyList
label={`Titulares (${headlines.length})`}
items={headlines}
strong
/>
) : null} ) : null}
{bodies.length > 0 ? ( {bodies.length > 0 ? (
<div> <CopyList label={`Textos (${bodies.length})`} items={bodies} />
<p className="text-[9px] uppercase tracking-wider text-gray-500 mb-1">
Textos ({bodies.length})
</p>
<ul className="space-y-1.5">
{bodies.map((b) => (
<li key={b} className="text-gray-600 leading-relaxed">
{b}
</li>
))}
</ul>
</div>
) : null} ) : null}
{linkDescs.length > 0 ? ( {linkDescs.length > 0 ? (
<div> <CopyList label="Descripción" items={linkDescs} muted />
<p className="text-[9px] uppercase tracking-wider text-gray-500 mb-1">
Descripción
</p>
<ul className="space-y-1">
{linkDescs.map((d) => (
<li key={d} className="text-gray-500 leading-relaxed">
{d}
</li>
))}
</ul>
</div>
) : null} ) : null}
</>
)}
{ad.cta ? ( {ad.cta ? (
<p className="text-[10px] text-brand-700 uppercase tracking-wide pt-0.5"> <p className="text-[10px] text-brand-700 uppercase tracking-wide pt-0.5">
CTA · {ad.cta} CTA · {ad.cta}
@@ -589,6 +631,13 @@ export function CreativeZoomModal({
const bodies = adBodies(ad); const bodies = adBodies(ad);
const linkDescs = adLinkDescriptions(ad); const linkDescs = adLinkDescriptions(ad);
const longH = ad.longHeadlines || []; const longH = ad.longHeadlines || [];
const googleDescs =
ad.platform === 'google'
? ad.descriptions?.length
? ad.descriptions
: bodies
: [];
const isGoogle = ad.platform === 'google';
const assets = adAssets(ad); const assets = adAssets(ad);
const images = assets.filter((a) => a.kind === 'image' || a.kind === 'logo'); const images = assets.filter((a) => a.kind === 'image' || a.kind === 'logo');
const videos = assets.filter((a) => a.kind === 'youtube' || a.kind === 'video'); const videos = assets.filter((a) => a.kind === 'youtube' || a.kind === 'video');
@@ -802,7 +851,7 @@ export function CreativeZoomModal({
{headlines.length > 0 ? ( {headlines.length > 0 ? (
<section> <section>
<h3 className="text-sm font-semibold text-gray-900 mb-2"> <h3 className="text-sm font-semibold text-gray-900 mb-2">
Titulares ({headlines.length}) {isGoogle ? 'Títulos' : 'Titulares'} ({headlines.length})
</h3> </h3>
<ul className="space-y-1.5 text-sm text-gray-800"> <ul className="space-y-1.5 text-sm text-gray-800">
{headlines.map((h) => ( {headlines.map((h) => (
@@ -820,7 +869,7 @@ export function CreativeZoomModal({
{longH.length > 0 ? ( {longH.length > 0 ? (
<section> <section>
<h3 className="text-sm font-semibold text-gray-900 mb-2"> <h3 className="text-sm font-semibold text-gray-900 mb-2">
Titulares largos ({longH.length}) Títulos largos ({longH.length})
</h3> </h3>
<ul className="space-y-1.5 text-sm text-gray-800"> <ul className="space-y-1.5 text-sm text-gray-800">
{longH.map((h) => ( {longH.map((h) => (
@@ -835,7 +884,25 @@ export function CreativeZoomModal({
</section> </section>
) : null} ) : null}
{bodies.length > 0 ? ( {isGoogle && googleDescs.length > 0 ? (
<section>
<h3 className="text-sm font-semibold text-gray-900 mb-2">
Descripciones ({googleDescs.length})
</h3>
<ul className="space-y-2 text-sm text-gray-600">
{googleDescs.map((b) => (
<li
key={b}
className="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 leading-relaxed whitespace-pre-wrap"
>
{b}
</li>
))}
</ul>
</section>
) : null}
{!isGoogle && bodies.length > 0 ? (
<section> <section>
<h3 className="text-sm font-semibold text-gray-900 mb-2"> <h3 className="text-sm font-semibold text-gray-900 mb-2">
Textos ({bodies.length}) Textos ({bodies.length})
@@ -853,7 +920,7 @@ export function CreativeZoomModal({
</section> </section>
) : null} ) : null}
{linkDescs.length > 0 ? ( {!isGoogle && linkDescs.length > 0 ? (
<section> <section>
<h3 className="text-sm font-semibold text-gray-900 mb-2"> <h3 className="text-sm font-semibold text-gray-900 mb-2">
Descripción Descripción

View File

@@ -138,6 +138,14 @@ function enumName(v?: string): string {
return (v || '').replace(/^[A-Z0-9]+_/, '') || str(v); return (v || '').replace(/^[A-Z0-9]+_/, '') || str(v);
} }
/** Do not strip LONG_HEADLINE → HEADLINE (enumName would). */
function assetFieldType(v?: string): string {
return str(v)
.toUpperCase()
.replace(/^ASSET_FIELD_TYPE_/, '')
.replace(/^ASSETFIELDTYPE_/, '');
}
function uniq(arr: string[]): string[] { function uniq(arr: string[]): string[] {
return [...new Set(arr.filter(Boolean))]; return [...new Set(arr.filter(Boolean))];
} }
@@ -600,7 +608,7 @@ WHERE ad_group_ad.status != 'REMOVED'
const adId = str(row.adGroupAd?.ad?.id); const adId = str(row.adGroupAd?.ad?.id);
if (!adId) continue; if (!adId) continue;
const list = assetsByAdId.get(adId) || []; const list = assetsByAdId.get(adId) || [];
const ft = enumName(row.adGroupAdAssetView?.fieldType).toUpperCase(); const ft = assetFieldType(row.adGroupAdAssetView?.fieldType);
const img = str(row.asset?.imageAsset?.fullSize?.url); const img = str(row.asset?.imageAsset?.fullSize?.url);
const w = n(row.asset?.imageAsset?.fullSize?.widthPixels) || undefined; const w = n(row.asset?.imageAsset?.fullSize?.widthPixels) || undefined;
const h = n(row.asset?.imageAsset?.fullSize?.heightPixels) || undefined; const h = n(row.asset?.imageAsset?.fullSize?.heightPixels) || undefined;
@@ -698,7 +706,7 @@ WHERE ad_group_ad.status != 'REMOVED'
}; };
agMap.set(agid, entry); agMap.set(agid, entry);
} }
const ft = enumName(row.assetGroupAsset?.fieldType).toUpperCase(); const ft = assetFieldType(row.assetGroupAsset?.fieldType);
const text = str(row.asset?.textAsset?.text); const text = str(row.asset?.textAsset?.text);
const img = str(row.asset?.imageAsset?.fullSize?.url); const img = str(row.asset?.imageAsset?.fullSize?.url);
const imgW = n(row.asset?.imageAsset?.fullSize?.widthPixels) || undefined; const imgW = n(row.asset?.imageAsset?.fullSize?.widthPixels) || undefined;
@@ -804,12 +812,12 @@ WHERE ad_group_ad.status != 'REMOVED'
}), }),
thumbnailUrl: preview, thumbnailUrl: preview,
imageUrl: preview, imageUrl: preview,
headline: [...headlines, ...longHeadlines].join('\n'), headline: headlines.join('\n'),
primaryText: descriptions.join('\n'), primaryText: descriptions.join('\n'),
headlines, headlines,
bodies: descriptions, bodies: [],
longHeadlines, longHeadlines,
descriptions: [], descriptions,
sitelinks, sitelinks,
callouts, callouts,
snippets, snippets,
@@ -914,12 +922,12 @@ WHERE ad_group_ad.status != 'REMOVED'
}), }),
thumbnailUrl: preview, thumbnailUrl: preview,
imageUrl: preview, imageUrl: preview,
headline: [...headlines, ...longHeadlines].join('\n'), headline: headlines.join('\n'),
primaryText: descriptions.join('\n'), primaryText: descriptions.join('\n'),
headlines, headlines,
bodies: descriptions, bodies: [],
longHeadlines, longHeadlines,
descriptions: [], descriptions,
assets, assets,
linkUrl: (ad?.finalUrls || []).map(str).filter(Boolean)[0], linkUrl: (ad?.finalUrls || []).map(str).filter(Boolean)[0],
cta: str(rda?.callToActionText) || undefined, cta: str(rda?.callToActionText) || undefined,
@@ -928,7 +936,7 @@ WHERE ad_group_ad.status != 'REMOVED'
youtubeId: firstYt, youtubeId: firstYt,
}); });
if (paths.length) { if (paths.length) {
cards[cards.length - 1].bodies = [ cards[cards.length - 1].descriptions = [
...descriptions, ...descriptions,
`Path: /${paths.join('/')}`, `Path: /${paths.join('/')}`,
]; ];