Show every Google PMax text, image and YouTube asset in Ads zoom.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -183,7 +183,7 @@ export async function GET(req: NextRequest) {
|
||||
dailyRows: daily,
|
||||
since,
|
||||
until,
|
||||
limit: 48,
|
||||
limit: 80,
|
||||
});
|
||||
googleAds = google.ads.length
|
||||
? google.ads
|
||||
|
||||
@@ -17,26 +17,29 @@ export function proxiedMediaUrl(raw: string): string {
|
||||
}
|
||||
|
||||
function adHeadlines(ad: AdCreativeCard): string[] {
|
||||
if (ad.headlines?.length) return ad.headlines.slice(0, 5);
|
||||
return (ad.headline || '')
|
||||
.split('\n')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 5);
|
||||
const all = ad.headlines?.length
|
||||
? ad.headlines
|
||||
: (ad.headline || '')
|
||||
.split('\n')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
if (ad.platform === 'google') return all;
|
||||
return all.slice(0, 5);
|
||||
}
|
||||
|
||||
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)
|
||||
.slice(0, 5);
|
||||
const all = ad.bodies?.length
|
||||
? ad.bodies
|
||||
: (ad.primaryText || '')
|
||||
.split('\n')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
if (ad.platform === 'google') return all;
|
||||
return all.slice(0, 5);
|
||||
}
|
||||
|
||||
function adLinkDescriptions(ad: AdCreativeCard): string[] {
|
||||
if (ad.descriptions?.length) return ad.descriptions.slice(0, 5);
|
||||
if (ad.descriptions?.length) return ad.descriptions;
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -158,19 +161,19 @@ function previewFor(ad: AdCreativeCard): { url: string; aspect: CreativeAspect }
|
||||
function formatThumbs(ad: AdCreativeCard): { url: string; aspect: CreativeAspect; label: string }[] {
|
||||
if (ad.mediaType !== 'pmax' && ad.channel !== 'pmax') return [];
|
||||
const assets = adAssets(ad).filter(
|
||||
(a) => (a.kind === 'image' || a.kind === 'logo' || a.kind === 'youtube') && a.url
|
||||
(a) =>
|
||||
(a.kind === 'image' || a.kind === 'logo' || a.kind === 'youtube') && a.url
|
||||
);
|
||||
const order: CreativeAspect[] = ['1:1', '4:5', '16:9', '9:16'];
|
||||
const out: { url: string; aspect: CreativeAspect; label: string }[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const asp of order) {
|
||||
const hit = assets.find((a) => resolveAspect(a) === asp && !seen.has(a.url));
|
||||
if (!hit) continue;
|
||||
seen.add(hit.url);
|
||||
const out: { url: string; aspect: CreativeAspect; label: string }[] = [];
|
||||
for (const a of assets) {
|
||||
if (seen.has(a.url)) continue;
|
||||
seen.add(a.url);
|
||||
const asp = resolveAspect(a);
|
||||
out.push({
|
||||
url: proxiedMediaUrl(hit.url),
|
||||
aspect: asp,
|
||||
label: asp,
|
||||
url: proxiedMediaUrl(a.url),
|
||||
aspect: asp === 'unknown' ? '1:1' : asp,
|
||||
label: a.label || asp,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
@@ -451,15 +454,15 @@ function CreativeCard({
|
||||
<Maximize2 className="w-3.5 h-3.5" /> Zoom
|
||||
</span>
|
||||
</button>
|
||||
{pmaxFormats.length > 1 ? (
|
||||
{pmaxFormats.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1.5 justify-center">
|
||||
{pmaxFormats.map((f) => (
|
||||
{pmaxFormats.slice(0, 12).map((f, i) => (
|
||||
<button
|
||||
key={f.label}
|
||||
key={`${f.url}-${i}`}
|
||||
type="button"
|
||||
onClick={onZoom}
|
||||
className="w-12 overflow-hidden rounded-md border border-gray-200 bg-gray-50"
|
||||
title={`Formato ${f.label}`}
|
||||
title={f.label}
|
||||
>
|
||||
<div className={`${aspectClass(f.aspect)} w-full`}>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
@@ -475,6 +478,11 @@ function CreativeCard({
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
{pmaxFormats.length > 12 ? (
|
||||
<p className="w-full text-center text-[10px] text-gray-500">
|
||||
+{pmaxFormats.length - 12} en Zoom
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -585,21 +593,24 @@ export function CreativeZoomModal({
|
||||
const images = assets.filter((a) => a.kind === 'image' || a.kind === 'logo');
|
||||
const videos = assets.filter((a) => a.kind === 'youtube' || a.kind === 'video');
|
||||
const { url: main, aspect: mainAspect } = previewFor(ad);
|
||||
const previewAssetUrl = main.replace(/^\/api\/ad-tracker\/media\?url=/, '');
|
||||
const otherImages = images.filter((a) => {
|
||||
if (!a.url) return false;
|
||||
if (!previewAssetUrl) return true;
|
||||
try {
|
||||
return decodeURIComponent(previewAssetUrl) !== a.url && a.url !== main;
|
||||
} catch {
|
||||
return a.url !== main;
|
||||
}
|
||||
});
|
||||
// Prefer showing all format variants in zoom for Meta carousel / Google PMax
|
||||
const zoomImages =
|
||||
otherImages.length > 0
|
||||
? otherImages
|
||||
: images.filter((a) => a.url && proxiedMediaUrl(a.url) !== main);
|
||||
ad.platform === 'google'
|
||||
? images.filter((a) => a.url)
|
||||
: images.filter((a) => {
|
||||
if (!a.url) return false;
|
||||
const previewAssetUrl = main.replace(
|
||||
/^\/api\/ad-tracker\/media\?url=/,
|
||||
''
|
||||
);
|
||||
if (!previewAssetUrl) return true;
|
||||
try {
|
||||
return (
|
||||
decodeURIComponent(previewAssetUrl) !== a.url && a.url !== main
|
||||
);
|
||||
} catch {
|
||||
return a.url !== main;
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
@@ -714,7 +725,9 @@ export function CreativeZoomModal({
|
||||
{zoomImages.length > 0 ? (
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
||||
Otras piezas ({zoomImages.length})
|
||||
{ad.platform === 'google'
|
||||
? `Imágenes y logos (${zoomImages.length})`
|
||||
: `Otras piezas (${zoomImages.length})`}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{zoomImages.map((asset, i) => {
|
||||
@@ -810,7 +823,7 @@ export function CreativeZoomModal({
|
||||
Titulares largos ({longH.length})
|
||||
</h3>
|
||||
<ul className="space-y-1.5 text-sm text-gray-800">
|
||||
{longH.slice(0, 5).map((h) => (
|
||||
{longH.map((h) => (
|
||||
<li
|
||||
key={h}
|
||||
className="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2"
|
||||
@@ -857,6 +870,89 @@ export function CreativeZoomModal({
|
||||
</ul>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{ad.businessName ? (
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
||||
Nombre de negocio
|
||||
</h3>
|
||||
<p className="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm text-gray-800">
|
||||
{ad.businessName}
|
||||
</p>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{(ad.ctas || []).length > 0 ? (
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
||||
CTA ({ad.ctas!.length})
|
||||
</h3>
|
||||
<ul className="flex flex-wrap gap-1.5">
|
||||
{ad.ctas!.map((c) => (
|
||||
<li
|
||||
key={c}
|
||||
className="rounded-full border border-gray-200 bg-white px-3 py-1 text-xs text-gray-800"
|
||||
>
|
||||
{c}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{(ad.sitelinks || []).length > 0 ? (
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
||||
Sitelinks ({ad.sitelinks!.length})
|
||||
</h3>
|
||||
<ul className="space-y-1.5 text-sm text-gray-800">
|
||||
{ad.sitelinks!.map((s) => (
|
||||
<li
|
||||
key={s}
|
||||
className="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2"
|
||||
>
|
||||
{s}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{(ad.callouts || []).length > 0 ? (
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
||||
Callouts ({ad.callouts!.length})
|
||||
</h3>
|
||||
<ul className="space-y-1.5 text-sm text-gray-800">
|
||||
{ad.callouts!.map((s) => (
|
||||
<li
|
||||
key={s}
|
||||
className="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2"
|
||||
>
|
||||
{s}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{(ad.snippets || []).length > 0 ? (
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
||||
Structured snippets ({ad.snippets!.length})
|
||||
</h3>
|
||||
<ul className="space-y-1.5 text-sm text-gray-800">
|
||||
{ad.snippets!.map((s) => (
|
||||
<li
|
||||
key={s}
|
||||
className="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2"
|
||||
>
|
||||
{s}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,10 @@ import type {
|
||||
CreativeAsset,
|
||||
GoogleCreativeChannel,
|
||||
} from '@/lib/ad-creatives-meta';
|
||||
import { aggregateDailyByAd } from '@/lib/ad-creatives-meta';
|
||||
import {
|
||||
aggregateDailyByAd,
|
||||
inferCreativeAspect,
|
||||
} from '@/lib/ad-creatives-meta';
|
||||
import {
|
||||
friendlyGoogleCreativeWarning,
|
||||
resolveGoogleAdsAccessToken,
|
||||
@@ -58,13 +61,23 @@ type GaqlRow = {
|
||||
asset?: {
|
||||
id?: string;
|
||||
type?: string;
|
||||
name?: string;
|
||||
textAsset?: { text?: string };
|
||||
imageAsset?: { fullSize?: { url?: string } };
|
||||
imageAsset?: {
|
||||
fullSize?: { url?: string; widthPixels?: number; heightPixels?: number };
|
||||
};
|
||||
youtubeVideoAsset?: {
|
||||
youtubeVideoId?: string;
|
||||
youtubeVideoTitle?: string;
|
||||
};
|
||||
callToActionAsset?: { callToAction?: string };
|
||||
sitelinkAsset?: {
|
||||
linkText?: string;
|
||||
description1?: string;
|
||||
description2?: string;
|
||||
};
|
||||
calloutAsset?: { calloutText?: string };
|
||||
structuredSnippetAsset?: { header?: string; values?: string[] };
|
||||
};
|
||||
metrics?: {
|
||||
costMicros?: string;
|
||||
@@ -211,9 +224,14 @@ function ytThumb(youtubeId: string): string {
|
||||
|
||||
function googleAspectFromFieldType(ft: string): CreativeAspect {
|
||||
const u = ft.toUpperCase();
|
||||
if (u.includes('PORTRAIT') || u.includes('TALL')) return '4:5';
|
||||
if (u.includes('SQUARE') || u === 'LOGO') return '1:1';
|
||||
if (u.includes('YOUTUBE') || u.includes('LANDSCAPE') || u === 'MARKETING_IMAGE') {
|
||||
if (u.includes('TALL_PORTRAIT') || u.includes('VERTICAL')) return '9:16';
|
||||
if (u.includes('PORTRAIT')) return '4:5';
|
||||
if (u.includes('SQUARE') || u === 'LOGO' || u === 'BUSINESS_LOGO') return '1:1';
|
||||
if (
|
||||
u.includes('YOUTUBE') ||
|
||||
u.includes('LANDSCAPE') ||
|
||||
u === 'MARKETING_IMAGE'
|
||||
) {
|
||||
return '16:9';
|
||||
}
|
||||
return 'unknown';
|
||||
@@ -235,11 +253,6 @@ function pickVerticalGoogle(assets: CreativeAsset[]): string {
|
||||
return images[0]?.url || '';
|
||||
}
|
||||
|
||||
function withAspect(asset: CreativeAsset, ft: string): CreativeAsset {
|
||||
if (asset.aspect && asset.aspect !== 'unknown') return asset;
|
||||
return { ...asset, aspect: googleAspectFromFieldType(ft) };
|
||||
}
|
||||
|
||||
/** Keep Search / PMax / Display represented in the gallery, not only top spend. */
|
||||
function balanceGoogleCards(cards: AdCreativeCard[], limit: number): AdCreativeCard[] {
|
||||
const buckets: Record<string, AdCreativeCard[]> = {
|
||||
@@ -259,7 +272,7 @@ function balanceGoogleCards(cards: AdCreativeCard[], limit: number): AdCreativeC
|
||||
const quotas: Record<string, number> = {
|
||||
search_brand: Math.max(8, Math.floor(limit * 0.2)),
|
||||
search_generic: Math.max(8, Math.floor(limit * 0.2)),
|
||||
pmax: Math.max(12, Math.floor(limit * 0.35)),
|
||||
pmax: Math.max(24, Math.floor(limit * 0.4)),
|
||||
display: Math.max(8, Math.floor(limit * 0.2)),
|
||||
other: Math.max(4, Math.floor(limit * 0.1)),
|
||||
};
|
||||
@@ -295,9 +308,103 @@ type AgBucket = {
|
||||
longHeadlines: string[];
|
||||
descriptions: string[];
|
||||
ctas: string[];
|
||||
businessNames: string[];
|
||||
sitelinks: string[];
|
||||
callouts: string[];
|
||||
snippets: string[];
|
||||
assets: CreativeAsset[];
|
||||
};
|
||||
|
||||
function pushGoogleImage(
|
||||
list: CreativeAsset[],
|
||||
img: string,
|
||||
ft: string,
|
||||
kind: CreativeAsset['kind'],
|
||||
width?: number,
|
||||
height?: number
|
||||
) {
|
||||
if (!img || list.some((a) => a.url === img)) return;
|
||||
const fromPx = inferCreativeAspect(ft, width, height);
|
||||
const fromFt = googleAspectFromFieldType(ft);
|
||||
list.push({
|
||||
kind,
|
||||
url: img,
|
||||
label: ft || 'IMAGE',
|
||||
aspect: fromPx !== 'unknown' ? fromPx : fromFt,
|
||||
});
|
||||
}
|
||||
|
||||
function pushGoogleYoutube(
|
||||
list: CreativeAsset[],
|
||||
yt: string,
|
||||
title?: string
|
||||
) {
|
||||
if (!yt || list.some((a) => a.youtubeId === yt)) return;
|
||||
list.push({
|
||||
kind: 'youtube',
|
||||
url: ytThumb(yt),
|
||||
label: 'YOUTUBE_VIDEO',
|
||||
youtubeId: yt,
|
||||
title: title || yt,
|
||||
aspect: '16:9',
|
||||
});
|
||||
}
|
||||
|
||||
const PMAX_ASSET_QUERY = `
|
||||
SELECT
|
||||
campaign.id,
|
||||
campaign.name,
|
||||
campaign.advertising_channel_type,
|
||||
asset_group.id,
|
||||
asset_group.name,
|
||||
asset_group.status,
|
||||
asset_group.final_urls,
|
||||
asset_group_asset.field_type,
|
||||
asset.id,
|
||||
asset.type,
|
||||
asset.name,
|
||||
asset.text_asset.text,
|
||||
asset.image_asset.full_size.url,
|
||||
asset.image_asset.full_size.width_pixels,
|
||||
asset.image_asset.full_size.height_pixels,
|
||||
asset.youtube_video_asset.youtube_video_id,
|
||||
asset.youtube_video_asset.youtube_video_title,
|
||||
asset.call_to_action_asset.call_to_action,
|
||||
asset.sitelink_asset.link_text,
|
||||
asset.sitelink_asset.description1,
|
||||
asset.sitelink_asset.description2,
|
||||
asset.callout_asset.callout_text,
|
||||
asset.structured_snippet_asset.header,
|
||||
asset.structured_snippet_asset.values
|
||||
FROM asset_group_asset
|
||||
WHERE asset_group.status != 'REMOVED'
|
||||
AND asset_group_asset.status != 'REMOVED'
|
||||
`;
|
||||
|
||||
const PMAX_ASSET_QUERY_BASIC = `
|
||||
SELECT
|
||||
campaign.id,
|
||||
campaign.name,
|
||||
campaign.advertising_channel_type,
|
||||
asset_group.id,
|
||||
asset_group.name,
|
||||
asset_group.status,
|
||||
asset_group.final_urls,
|
||||
asset_group_asset.field_type,
|
||||
asset.id,
|
||||
asset.type,
|
||||
asset.text_asset.text,
|
||||
asset.image_asset.full_size.url,
|
||||
asset.image_asset.full_size.width_pixels,
|
||||
asset.image_asset.full_size.height_pixels,
|
||||
asset.youtube_video_asset.youtube_video_id,
|
||||
asset.youtube_video_asset.youtube_video_title,
|
||||
asset.call_to_action_asset.call_to_action
|
||||
FROM asset_group_asset
|
||||
WHERE asset_group.status != 'REMOVED'
|
||||
AND asset_group_asset.status != 'REMOVED'
|
||||
`;
|
||||
|
||||
function cardsFromDaily(
|
||||
dailyRows: Parameters<typeof aggregateDailyByAd>[0],
|
||||
since: string,
|
||||
@@ -398,30 +505,8 @@ export async function fetchGoogleCreativeCards(opts: {
|
||||
|
||||
try {
|
||||
const [agAssets, agMetrics, adRows, adMetrics, adAssets] = await Promise.all([
|
||||
searchGoogle(
|
||||
customerId,
|
||||
accessToken,
|
||||
`
|
||||
SELECT
|
||||
campaign.id,
|
||||
campaign.name,
|
||||
campaign.advertising_channel_type,
|
||||
asset_group.id,
|
||||
asset_group.name,
|
||||
asset_group.status,
|
||||
asset_group.final_urls,
|
||||
asset_group_asset.field_type,
|
||||
asset.id,
|
||||
asset.type,
|
||||
asset.text_asset.text,
|
||||
asset.image_asset.full_size.url,
|
||||
asset.youtube_video_asset.youtube_video_id,
|
||||
asset.youtube_video_asset.youtube_video_title,
|
||||
asset.call_to_action_asset.call_to_action
|
||||
FROM asset_group_asset
|
||||
WHERE asset_group.status != 'REMOVED'
|
||||
AND asset_group_asset.status != 'REMOVED'
|
||||
`
|
||||
searchGoogle(customerId, accessToken, PMAX_ASSET_QUERY).catch(() =>
|
||||
searchGoogle(customerId, accessToken, PMAX_ASSET_QUERY_BASIC)
|
||||
),
|
||||
searchGoogle(
|
||||
customerId,
|
||||
@@ -466,7 +551,6 @@ SELECT
|
||||
FROM ad_group_ad
|
||||
WHERE ad_group_ad.status != 'REMOVED'
|
||||
AND campaign.status != 'REMOVED'
|
||||
LIMIT 120
|
||||
`
|
||||
),
|
||||
searchGoogle(
|
||||
@@ -500,11 +584,13 @@ WHERE ad_group_ad.status != 'REMOVED'
|
||||
'MARKETING_IMAGE',
|
||||
'SQUARE_MARKETING_IMAGE',
|
||||
'PORTRAIT_MARKETING_IMAGE',
|
||||
'TALL_PORTRAIT_MARKETING_IMAGE',
|
||||
'LOGO',
|
||||
'LANDSCAPE_LOGO',
|
||||
'BUSINESS_LOGO',
|
||||
'AD_IMAGE',
|
||||
'YOUTUBE_VIDEO'
|
||||
)
|
||||
LIMIT 400
|
||||
`
|
||||
).catch(() => [] as GaqlRow[]),
|
||||
]);
|
||||
@@ -516,30 +602,21 @@ LIMIT 400
|
||||
const list = assetsByAdId.get(adId) || [];
|
||||
const ft = enumName(row.adGroupAdAssetView?.fieldType).toUpperCase();
|
||||
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;
|
||||
const yt = str(row.asset?.youtubeVideoAsset?.youtubeVideoId);
|
||||
const ytTitle = str(row.asset?.youtubeVideoAsset?.youtubeVideoTitle);
|
||||
if (img && !list.some((a) => a.url === img)) {
|
||||
list.push(
|
||||
withAspect(
|
||||
{
|
||||
kind: ft.includes('LOGO') ? 'logo' : 'image',
|
||||
url: img,
|
||||
label: ft || 'MARKETING_IMAGE',
|
||||
},
|
||||
ft || 'MARKETING_IMAGE'
|
||||
)
|
||||
if (img) {
|
||||
pushGoogleImage(
|
||||
list,
|
||||
img,
|
||||
ft || 'MARKETING_IMAGE',
|
||||
ft.includes('LOGO') ? 'logo' : 'image',
|
||||
w,
|
||||
h
|
||||
);
|
||||
}
|
||||
if (yt && !list.some((a) => a.youtubeId === yt)) {
|
||||
list.push({
|
||||
kind: 'youtube',
|
||||
url: ytThumb(yt),
|
||||
label: 'YOUTUBE_VIDEO',
|
||||
youtubeId: yt,
|
||||
title: ytTitle || yt,
|
||||
aspect: '16:9',
|
||||
});
|
||||
}
|
||||
if (yt) pushGoogleYoutube(list, yt, ytTitle);
|
||||
assetsByAdId.set(adId, list);
|
||||
}
|
||||
|
||||
@@ -613,6 +690,10 @@ LIMIT 400
|
||||
longHeadlines: [],
|
||||
descriptions: [],
|
||||
ctas: [],
|
||||
businessNames: [],
|
||||
sitelinks: [],
|
||||
callouts: [],
|
||||
snippets: [],
|
||||
assets: [],
|
||||
};
|
||||
agMap.set(agid, entry);
|
||||
@@ -620,37 +701,51 @@ LIMIT 400
|
||||
const ft = enumName(row.assetGroupAsset?.fieldType).toUpperCase();
|
||||
const text = str(row.asset?.textAsset?.text);
|
||||
const img = str(row.asset?.imageAsset?.fullSize?.url);
|
||||
const imgW = n(row.asset?.imageAsset?.fullSize?.widthPixels) || undefined;
|
||||
const imgH = n(row.asset?.imageAsset?.fullSize?.heightPixels) || undefined;
|
||||
const yt = str(row.asset?.youtubeVideoAsset?.youtubeVideoId);
|
||||
const ytTitle = str(row.asset?.youtubeVideoAsset?.youtubeVideoTitle);
|
||||
const cta = enumName(row.asset?.callToActionAsset?.callToAction);
|
||||
const sitelink = row.asset?.sitelinkAsset;
|
||||
const callout = str(row.asset?.calloutAsset?.calloutText);
|
||||
const snippet = row.asset?.structuredSnippetAsset;
|
||||
|
||||
if (ft === 'HEADLINE' && text) entry.headlines.push(text);
|
||||
else if (ft === 'LONG_HEADLINE' && text) entry.longHeadlines.push(text);
|
||||
else if (ft === 'DESCRIPTION' && text) entry.descriptions.push(text);
|
||||
else if (ft === 'CALL_TO_ACTION_SELECTION' && cta) entry.ctas.push(cta);
|
||||
else if (ft.includes('MARKETING_IMAGE') && img) {
|
||||
if (!entry.assets.some((a) => a.url === img)) {
|
||||
entry.assets.push(
|
||||
withAspect({ kind: 'image', url: img, label: ft }, ft)
|
||||
);
|
||||
}
|
||||
} else if ((ft === 'LOGO' || ft === 'LANDSCAPE_LOGO') && img) {
|
||||
if (!entry.assets.some((a) => a.url === img)) {
|
||||
entry.assets.push(
|
||||
withAspect({ kind: 'logo', url: img, label: ft }, ft)
|
||||
);
|
||||
}
|
||||
} else if (ft === 'YOUTUBE_VIDEO' && yt) {
|
||||
if (!entry.assets.some((a) => a.youtubeId === yt)) {
|
||||
entry.assets.push({
|
||||
kind: 'youtube',
|
||||
url: ytThumb(yt),
|
||||
label: 'YOUTUBE_VIDEO',
|
||||
youtubeId: yt,
|
||||
title: ytTitle || yt,
|
||||
aspect: '16:9',
|
||||
});
|
||||
}
|
||||
if ((ft === 'HEADLINE' || ft === 'MANDATORY_AD_TEXT') && text) {
|
||||
entry.headlines.push(text);
|
||||
} else if (ft === 'LONG_HEADLINE' && text) {
|
||||
entry.longHeadlines.push(text);
|
||||
} else if (ft === 'DESCRIPTION' && text) {
|
||||
entry.descriptions.push(text);
|
||||
} else if (ft === 'BUSINESS_NAME' && text) {
|
||||
entry.businessNames.push(text);
|
||||
} else if (ft === 'CALL_TO_ACTION_SELECTION' && cta) {
|
||||
entry.ctas.push(cta);
|
||||
} else if (ft === 'SITELINK' && sitelink) {
|
||||
const line = [
|
||||
str(sitelink.linkText),
|
||||
str(sitelink.description1),
|
||||
str(sitelink.description2),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ');
|
||||
if (line) entry.sitelinks.push(line);
|
||||
} else if (ft === 'CALLOUT' && callout) {
|
||||
entry.callouts.push(callout);
|
||||
} else if (ft === 'STRUCTURED_SNIPPET' && snippet) {
|
||||
const vals = (snippet.values || []).map(str).filter(Boolean).join(', ');
|
||||
const line = [str(snippet.header), vals].filter(Boolean).join(': ');
|
||||
if (line) entry.snippets.push(line);
|
||||
} else if (img && (/IMAGE|LOGO/.test(ft) || row.asset?.type === 'IMAGE')) {
|
||||
pushGoogleImage(
|
||||
entry.assets,
|
||||
img,
|
||||
ft || 'IMAGE',
|
||||
/LOGO/.test(ft) ? 'logo' : 'image',
|
||||
imgW,
|
||||
imgH
|
||||
);
|
||||
} else if (yt && (/VIDEO/.test(ft) || row.asset?.type === 'YOUTUBE_VIDEO')) {
|
||||
pushGoogleYoutube(entry.assets, yt, ytTitle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -665,11 +760,25 @@ LIMIT 400
|
||||
const impressions = m?.impressions ?? camp?.impressions ?? 0;
|
||||
const clicks = m?.clicks ?? camp?.clicks ?? 0;
|
||||
const conversions = m?.conversions ?? camp?.conversions ?? 0;
|
||||
if (spend <= 0 && impressions <= 0 && clicks <= 0 && !m) continue;
|
||||
const isPmax =
|
||||
/PERFORMANCE_MAX/i.test(entry.channelType) ||
|
||||
/P[\s_-]?MAX|PERFORMANCE[\s_-]?MAX/i.test(entry.campaignName);
|
||||
const hasPack =
|
||||
entry.assets.length > 0 ||
|
||||
entry.headlines.length > 0 ||
|
||||
entry.descriptions.length > 0;
|
||||
if (spend <= 0 && impressions <= 0 && clicks <= 0 && !m && !(isPmax && hasPack)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const headlines = uniq(entry.headlines);
|
||||
const longHeadlines = uniq(entry.longHeadlines);
|
||||
const descriptions = uniq(entry.descriptions);
|
||||
const sitelinks = uniq(entry.sitelinks);
|
||||
const callouts = uniq(entry.callouts);
|
||||
const snippets = uniq(entry.snippets);
|
||||
const ctas = uniq(entry.ctas);
|
||||
const businessName = uniq(entry.businessNames)[0];
|
||||
const preview =
|
||||
pickVerticalGoogle(entry.assets) ||
|
||||
entry.assets.find((a) => a.kind === 'youtube')?.url ||
|
||||
@@ -701,9 +810,14 @@ LIMIT 400
|
||||
bodies: descriptions,
|
||||
longHeadlines,
|
||||
descriptions: [],
|
||||
sitelinks,
|
||||
callouts,
|
||||
snippets,
|
||||
ctas,
|
||||
assets: entry.assets,
|
||||
linkUrl: entry.finalUrls[0],
|
||||
cta: uniq(entry.ctas)[0],
|
||||
cta: ctas[0],
|
||||
businessName,
|
||||
status: entry.status,
|
||||
youtubeId: firstYt,
|
||||
});
|
||||
@@ -827,6 +941,8 @@ LIMIT 400
|
||||
c.impressions > 0 ||
|
||||
c.clicks > 0 ||
|
||||
c.conversions > 0 ||
|
||||
(c.mediaType === 'pmax' &&
|
||||
((c.assets?.length || 0) > 0 || (c.headlines?.length || 0) > 0)) ||
|
||||
(c.mediaType === 'rsa' && (c.headlines?.length || 0) > 0) ||
|
||||
((c.mediaType === 'rda' || c.mediaType === 'image') &&
|
||||
(c.assets?.length || 0) > 0)
|
||||
|
||||
@@ -61,13 +61,17 @@ export type AdCreativeCard = {
|
||||
/** Joined for backwards compat; prefer headlines/bodies/descriptions arrays. */
|
||||
headline: string;
|
||||
primaryText: string;
|
||||
/** Titulares (Meta titles / Google headlines), máx. 5. */
|
||||
/** Titulares (Meta titles / Google headlines). */
|
||||
headlines: string[];
|
||||
/** Textos principales (Meta bodies), máx. 5. */
|
||||
/** Textos principales (Meta bodies / Google descriptions). */
|
||||
bodies: string[];
|
||||
/** Descripción del enlace (Meta descriptions), suele ser 1. */
|
||||
/** Descripción del enlace (Meta) u otros textos cortos. */
|
||||
descriptions: string[];
|
||||
longHeadlines?: string[];
|
||||
sitelinks?: string[];
|
||||
callouts?: string[];
|
||||
snippets?: string[];
|
||||
ctas?: string[];
|
||||
assets: CreativeAsset[];
|
||||
linkUrl?: string;
|
||||
cta?: string;
|
||||
|
||||
Reference in New Issue
Block a user