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

View File

@@ -138,6 +138,14 @@ function enumName(v?: string): string {
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[] {
return [...new Set(arr.filter(Boolean))];
}
@@ -600,7 +608,7 @@ WHERE ad_group_ad.status != 'REMOVED'
const adId = str(row.adGroupAd?.ad?.id);
if (!adId) continue;
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 w = n(row.asset?.imageAsset?.fullSize?.widthPixels) || undefined;
const h = n(row.asset?.imageAsset?.fullSize?.heightPixels) || undefined;
@@ -698,7 +706,7 @@ WHERE ad_group_ad.status != 'REMOVED'
};
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 img = str(row.asset?.imageAsset?.fullSize?.url);
const imgW = n(row.asset?.imageAsset?.fullSize?.widthPixels) || undefined;
@@ -804,12 +812,12 @@ WHERE ad_group_ad.status != 'REMOVED'
}),
thumbnailUrl: preview,
imageUrl: preview,
headline: [...headlines, ...longHeadlines].join('\n'),
headline: headlines.join('\n'),
primaryText: descriptions.join('\n'),
headlines,
bodies: descriptions,
bodies: [],
longHeadlines,
descriptions: [],
descriptions,
sitelinks,
callouts,
snippets,
@@ -914,12 +922,12 @@ WHERE ad_group_ad.status != 'REMOVED'
}),
thumbnailUrl: preview,
imageUrl: preview,
headline: [...headlines, ...longHeadlines].join('\n'),
headline: headlines.join('\n'),
primaryText: descriptions.join('\n'),
headlines,
bodies: descriptions,
bodies: [],
longHeadlines,
descriptions: [],
descriptions,
assets,
linkUrl: (ad?.finalUrls || []).map(str).filter(Boolean)[0],
cta: str(rda?.callToActionText) || undefined,
@@ -928,7 +936,7 @@ WHERE ad_group_ad.status != 'REMOVED'
youtubeId: firstYt,
});
if (paths.length) {
cards[cards.length - 1].bodies = [
cards[cards.length - 1].descriptions = [
...descriptions,
`Path: /${paths.join('/')}`,
];