Require 9:16 and 1.91:1 adaptations alongside every static 4:5 ad creative.
Generation briefs and Canva webhooks now declare all three Meta placements so WF3/WF5 deliver feed, Stories, and landscape from one Freepik base. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
76
src/lib/creatives-aspect-ratios.ts
Normal file
76
src/lib/creatives-aspect-ratios.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Aspect ratios for Meta static ads.
|
||||
* Primary design is always 4:5; every estatico request must also adapt to Stories (9:16)
|
||||
* and landscape feed / right column (1.91:1).
|
||||
*/
|
||||
|
||||
export const STATIC_PRIMARY_ASPECT = '4:5' as const;
|
||||
|
||||
export const STATIC_AD_ASPECT_RATIOS = ['4:5', '9:16', '1.91:1'] as const;
|
||||
|
||||
export type StaticAdAspectRatio = (typeof STATIC_AD_ASPECT_RATIOS)[number];
|
||||
|
||||
export type StaticAdPlacement = {
|
||||
aspect_ratio: StaticAdAspectRatio;
|
||||
role: 'feed' | 'stories' | 'landscape';
|
||||
size: string;
|
||||
/** Where n8n should store the final URL on the creative */
|
||||
meta_key: 'asset_url' | 'stories_asset_url' | 'landscape_asset_url';
|
||||
canva_template_key?: string;
|
||||
};
|
||||
|
||||
export const STATIC_AD_PLACEMENTS: StaticAdPlacement[] = [
|
||||
{
|
||||
aspect_ratio: '4:5',
|
||||
role: 'feed',
|
||||
size: '1080x1350',
|
||||
meta_key: 'asset_url',
|
||||
},
|
||||
{
|
||||
aspect_ratio: '9:16',
|
||||
role: 'stories',
|
||||
size: '1080x1920',
|
||||
meta_key: 'stories_asset_url',
|
||||
canva_template_key: 'stories_9_16',
|
||||
},
|
||||
{
|
||||
aspect_ratio: '1.91:1',
|
||||
role: 'landscape',
|
||||
size: '1200x628',
|
||||
meta_key: 'landscape_asset_url',
|
||||
canva_template_key: 'landscape_1_91',
|
||||
},
|
||||
];
|
||||
|
||||
export function isStaticAdFormat(format?: string | null): boolean {
|
||||
const f = String(format || 'estatico').toLowerCase().trim();
|
||||
return f === 'estatico' || f === 'image' || f === 'static';
|
||||
}
|
||||
|
||||
/** Fields to merge into generation_request.brief for static ads. */
|
||||
export function staticAdBriefAspectFields(format?: string | null): {
|
||||
aspect_ratio?: string;
|
||||
aspect_ratios?: string[];
|
||||
required_placements?: StaticAdPlacement[];
|
||||
} {
|
||||
if (!isStaticAdFormat(format)) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
aspect_ratio: STATIC_PRIMARY_ASPECT,
|
||||
aspect_ratios: [...STATIC_AD_ASPECT_RATIOS],
|
||||
required_placements: STATIC_AD_PLACEMENTS,
|
||||
};
|
||||
}
|
||||
|
||||
/** Extra payload for creative.created / recompose so WF5 composes all placements. */
|
||||
export function staticAdWebhookAspectFields(format?: string | null) {
|
||||
if (!isStaticAdFormat(format)) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
aspect_ratio: STATIC_PRIMARY_ASPECT,
|
||||
aspect_ratios: [...STATIC_AD_ASPECT_RATIOS],
|
||||
required_placements: STATIC_AD_PLACEMENTS,
|
||||
};
|
||||
}
|
||||
@@ -110,7 +110,17 @@ export interface GenerationRequestBrief {
|
||||
reason: GenerationRequestReason;
|
||||
source_creative_id?: string | null;
|
||||
format: 'estatico' | 'carrusel' | 'video';
|
||||
/** Primary design ratio (always 4:5 for estático). */
|
||||
aspect_ratio?: string;
|
||||
/** All placements to produce for estático: 4:5, 9:16, 1.91:1. */
|
||||
aspect_ratios?: string[];
|
||||
required_placements?: Array<{
|
||||
aspect_ratio: string;
|
||||
role: string;
|
||||
size: string;
|
||||
meta_key: string;
|
||||
canva_template_key?: string;
|
||||
}>;
|
||||
variants_requested: number;
|
||||
avoid_angles?: string[];
|
||||
campaign_name?: string | null;
|
||||
|
||||
@@ -14,6 +14,10 @@ import {
|
||||
buildPartialRegenBriefNotes,
|
||||
type CreativeRegenSelection,
|
||||
} from './creatives-regen';
|
||||
import {
|
||||
staticAdBriefAspectFields,
|
||||
staticAdWebhookAspectFields,
|
||||
} from './creatives-aspect-ratios';
|
||||
|
||||
export interface ActionActor {
|
||||
role: 'agency' | 'client' | string;
|
||||
@@ -346,14 +350,15 @@ export async function rejectCreative(
|
||||
const landingUrl =
|
||||
prodEntry?.landing_url || creative.landing_url || tenant?.default_landing_url || null;
|
||||
|
||||
const format = creative.format || 'estatico';
|
||||
const briefPayload = {
|
||||
client_id: tenantId,
|
||||
tenant_id: tenantId,
|
||||
client_slug: tenant?.slug || 'bps',
|
||||
reason: 'manual',
|
||||
source_creative_id: sourceId,
|
||||
format: creative.format || 'estatico',
|
||||
aspect_ratio: '4:5',
|
||||
format,
|
||||
...staticAdBriefAspectFields(format),
|
||||
variants_requested: options?.variantsRequested || 1,
|
||||
avoid_angles: avoidAngles,
|
||||
campaign_name: campaignName,
|
||||
@@ -639,14 +644,15 @@ export async function requestCreativePartialV2(
|
||||
.filter(Boolean)
|
||||
.join('\n\n');
|
||||
|
||||
const format = creative.format || 'estatico';
|
||||
const briefPayload = {
|
||||
client_id: tenantId,
|
||||
tenant_id: tenantId,
|
||||
client_slug: tenant?.slug || 'bps',
|
||||
reason: 'manual',
|
||||
source_creative_id: creative.id,
|
||||
format: creative.format || 'estatico',
|
||||
aspect_ratio: '4:5',
|
||||
format,
|
||||
...staticAdBriefAspectFields(format),
|
||||
variants_requested: 1,
|
||||
avoid_angles: plan.regenerate.image && creative.angle ? [creative.angle] : [],
|
||||
campaign_name: campaignName,
|
||||
@@ -824,7 +830,7 @@ export async function recomposeCreativeInCanva(
|
||||
visibility: 'agency',
|
||||
});
|
||||
|
||||
// Disparar WF5 pasando la imagen limpia original (raw_image_url) como asset_url para Canva
|
||||
// Disparar WF5: componer 4:5 + adaptar 9:16 y 1.91:1
|
||||
dispatchN8nWebhookAsync('creative.created', {
|
||||
creative: {
|
||||
...updated,
|
||||
@@ -833,6 +839,7 @@ export async function recomposeCreativeInCanva(
|
||||
meta: existingMeta,
|
||||
client: clientInfo,
|
||||
},
|
||||
...staticAdWebhookAspectFields(updated.format),
|
||||
});
|
||||
|
||||
return updated;
|
||||
|
||||
Reference in New Issue
Block a user