Split Google Ads copy into titles, long titles and descriptions.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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('/')}`,
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user