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;
|
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">
|
||||||
{headlines.length > 0 ? (
|
{ad.platform === 'google' ? (
|
||||||
<div>
|
<>
|
||||||
<p className="text-[9px] uppercase tracking-wider text-gray-500 mb-1">
|
{headlines.length > 0 ? (
|
||||||
Titulares ({headlines.length})
|
<CopyList
|
||||||
</p>
|
label={`Títulos (${headlines.length})`}
|
||||||
<ul className="space-y-1">
|
items={headlines}
|
||||||
{headlines.map((h) => (
|
strong
|
||||||
<li
|
/>
|
||||||
key={h}
|
) : null}
|
||||||
className="text-gray-900 font-medium leading-snug"
|
{longTitles.length > 0 ? (
|
||||||
>
|
<CopyList
|
||||||
{h}
|
label={`Títulos largos (${longTitles.length})`}
|
||||||
</li>
|
items={longTitles}
|
||||||
))}
|
strong
|
||||||
</ul>
|
/>
|
||||||
</div>
|
) : null}
|
||||||
) : null}
|
{googleDescs.length > 0 ? (
|
||||||
{bodies.length > 0 ? (
|
<CopyList
|
||||||
<div>
|
label={`Descripciones (${googleDescs.length})`}
|
||||||
<p className="text-[9px] uppercase tracking-wider text-gray-500 mb-1">
|
items={googleDescs}
|
||||||
Textos ({bodies.length})
|
/>
|
||||||
</p>
|
) : null}
|
||||||
<ul className="space-y-1.5">
|
</>
|
||||||
{bodies.map((b) => (
|
) : (
|
||||||
<li key={b} className="text-gray-600 leading-relaxed">
|
<>
|
||||||
{b}
|
{headlines.length > 0 ? (
|
||||||
</li>
|
<CopyList
|
||||||
))}
|
label={`Titulares (${headlines.length})`}
|
||||||
</ul>
|
items={headlines}
|
||||||
</div>
|
strong
|
||||||
) : null}
|
/>
|
||||||
{linkDescs.length > 0 ? (
|
) : null}
|
||||||
<div>
|
{bodies.length > 0 ? (
|
||||||
<p className="text-[9px] uppercase tracking-wider text-gray-500 mb-1">
|
<CopyList label={`Textos (${bodies.length})`} items={bodies} />
|
||||||
Descripción
|
) : null}
|
||||||
</p>
|
{linkDescs.length > 0 ? (
|
||||||
<ul className="space-y-1">
|
<CopyList label="Descripción" items={linkDescs} muted />
|
||||||
{linkDescs.map((d) => (
|
) : null}
|
||||||
<li key={d} className="text-gray-500 leading-relaxed">
|
</>
|
||||||
{d}
|
)}
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
) : 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
|
||||||
|
|||||||
@@ -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('/')}`,
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user