Una ruta de Webflow Cloud puede añadir fotos a una galería del CMS fusionándolas con la lista almacenada y volviendo a escribir la lista completa.
Al actualizar campos de imágenes múltiples del CMS mediante la API de Webflow, enviar solo los nuevos archivos sobrescribe la galería existente. Un campo de imágenes múltiples almacena una lista ordenada de hasta 25 imágenes, y la API de Webflow reemplaza toda esa lista en cada escritura. El código que envía solo fotos nuevas borra todo lo que los editores colocaron manualmente.
He creado la solución como una pequeña ruta de Next.js en Webflow Cloud que lee la galería, añade solo las imágenes que faltan y vuelve a escribir la lista completa. Un secreto compartido mantiene la ruta privada para tus propios sistemas, y la misma llamada funciona para un DAM o un proceso nocturno de proveedores.
¿Qué necesitas para actualizar campos de imágenes múltiples del CMS con la API de Webflow?
Necesitas cinco cosas:
- Un sitio con el CMS: El plan Starter incluye 50 elementos del CMS para pruebas y el plan Basic no tiene CMS, mientras que el plan Premium aumenta el límite a 20,000 elementos y 40 colecciones, según la página de precios de Webflow.
- Colección con un campo de imágenes múltiples: Anota el ID de la colección, el ID de un elemento para pruebas y el slug del campo, que puede ser diferente de la etiqueta que ven los editores en el CMS.
- Token de API: Un token de sitio con los alcances
CMS:readyCMS:write, mantenido fuera del repositorio y almacenado como un secreto de Webflow Cloud. - Next.js 15 en npm: Webflow Cloud es compatible con Next.js 15 y solo con el gestor de paquetes npm, así que crea la estructura con una versión fijada e instala las dependencias con npm.
- Node.js 22 o superior: Instala Node.js 22 o una versión posterior en tu equipo, tal como indica la documentación de Webflow Cloud como requisito mínimo general.
Todo lo que sigue depende de una regla en lib/webflow.ts: la ruta siempre vuelve a escribir la galería completa y nunca envía una lista parcial.
6 pasos para actualizar campos de imágenes múltiples del CMS en Webflow Cloud
La compilación es una aplicación de Next.js con un módulo auxiliar de API y una ruta POST protegida. La fusión es el núcleo de la ruta: lee la galería antes de cada escritura, por lo que las imágenes existentes sobreviven a cada actualización.
1. Crea la aplicación de Next.js para Webflow Cloud
Crea la estructura de un nuevo proyecto de Next.js o abre el que ya despliegas en Webflow Cloud. Instala las dependencias con npm, el único gestor de paquetes que admite Webflow Cloud, y elimina cualquier pnpm-lock.yaml o yarn.lock que haya quedado en el repositorio.
Mantengo la ruta de la galería en su propia aplicación pequeña montada en una ruta como /gallery-sync. Eso mantiene el token y la ruta alejados de las páginas de marketing que los editores tocan a diario.
Crea la estructura del proyecto con una versión 15 fijada para que la aplicación coincida con lo que admite Webflow Cloud:
npx create-next-app@15 gallery-sync --typescript --app --no-src-dir --import-alias "@/*" --use-npm
cd gallery-sync
npm run dev
La --import-alias bandera permite que el archivo de ruta resuelva @/lib/webflow. No añadas export const runtime = 'edge' a ningún archivo.
Webflow Cloud despliega Next.js a través del adaptador OpenNext Cloudflare, que no admite ese destino de tiempo de ejecución, así que deja la exportación del tiempo de ejecución fuera de cada manejador de ruta. Con el servidor de desarrollo en marcha, la página de inicio se carga en localhost:3000.
2. Añade el token y los ID como variables de entorno
Añade cuatro variables: el token de API y el ID de la colección, además del slug del campo de galería y un secreto compartido que envíen tus llamadas. Para el trabajo local, usa , que create-next-app .env.local.gitignore ya excluye.
La página de variables de entorno de Webflow indica que tanto las variables secretas como las normales están disponibles para la compilación y para la aplicación desplegada en tiempo de ejecución, y que los valores secretos se ocultan en los registros de compilación.
El archivo local tiene este aspecto, con tus propios valores en lugar de los marcadores de posición:
WEBFLOW_API_TOKEN=your-site-api-token
WEBFLOW_COLLECTION_ID=your-collection-id
WEBFLOW_GALLERY_FIELD=gallery
GALLERY_SYNC_SECRET=replace-with-a-long-random-string
Mantén el token en el lado del servidor omitiendo NEXT_PUBLIC_ de su nombre. Next.js inserta las variables con ese prefijo en el JavaScript que envía al navegador, por lo que un token de escritura del CMS ahí permitiría que cualquiera editara tu colección.
Los cuatro nombres ahora residen en , y se resuelve para .env.localprocess.env.WEBFLOW_API_TOKENlib/webflow.ts.
3. Inspecciona el campo de galería en un elemento real
Solicita un elemento real a la API de Webflow y lee cómo se devuelve el campo de galería antes de escribir cualquier lógica de combinación. Usa esa respuesta para inspeccionar las entradas almacenadas y, a continuación, consulta la documentación de la API para ver la estructura que acepta una escritura.
Cada llamada en esta compilación utiliza la API de datos v2: y en GETPATCH/v2/collections/{collection_id}/items/{item_id} con un token de portador.
Webflow documenta el formato: una lectura devuelve , y para cada imagen, y una escritura requiere y un fileIdurlalturlalt opcional. Cada imagen debe estar en una URL accesible públicamente y no superar los 4 MB, y un campo de imágenes múltiples admite un máximo de 25 imágenes.
Solicita el elemento con curl:
curl -s "https://api.webflow.com/v2/collections/YOUR_COLLECTION_ID/items/ITEM_ID" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "accept: application/json"
Busca el slug de tu galería en la respuesta, bajo fieldData, con una entrada por imagen. Si la matriz está vacía, añade manualmente dos imágenes a ese elemento en el CMS y vuelve a ejecutar la solicitud, ya que una lista vacía no te dice nada sobre la estructura. Si la solicitud falla, confirma que tu token puede acceder a la colección antes de continuar.
4. Escribe los ayudantes de la API de Webflow
Crea un módulo con las tres operaciones que necesita la ruta. Las separé en (leer la galería), (añadir nuevas entradas) y getGallerymergeImagessetGallery (volver a escribirla).
setGalleryenvía un PATCH cuyo cuerpo contiene solo el campo de galería; el código nunca envía el nombre, el slug u otros campos del elemento. Debido a que una escritura en un campo de lista puede reemplazar la lista almacenada, el ayudante siempre envía la lista combinada completa, nunca solo las nuevas entradas.
La combinación trata una imagen como duplicada si su URL o su texto alternativo ya existen en el elemento, y mantiene las entradas existentes exactamente como las devolvió la lectura, por lo que los identificadores y el texto alternativo que establecen los editores sobreviven al viaje de ida y vuelta.
El texto alternativo es un valor que tu sistema envía y controla, por lo que si el URL en una lectura posterior difiere del que enviaste, la coincidencia del texto alternativo sigue detectando la repetición. La otra cara de la moneda: dos fotos diferentes enviadas con el mismo texto alternativo, como "Foto de producto", cuentan como una, y la segunda se omite. Envía un texto alternativo que nombre el producto y la vista, por ejemplo, "Silla de roble, vista lateral".
El módulo llama al punto final de actualización de elementos con . Envía las entradas existentes exactamente como se leyeron, con su y , lo que coincide con el ejemplo de solicitud en la referencia de la API de Webflow, y envía las nuevas entradas como más PATCHfileIdurlurlalt.
La escritura establece skipInvalidFiles=false, porque, por defecto, Webflow descarta silenciosamente una imagen que no puede recuperar y aun así informa de éxito.
Guárdalo como lib/webflow.ts:
// lib/webflow.ts
const API_BASE = 'https://api.webflow.com/v2';
export type GalleryImage = Record<string, unknown> & { url?: string; alt?: string };
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing environment variable: ${name}`);
return value;
}
function itemUrl(itemId: string): string {
const collectionId = requireEnv('WEBFLOW_COLLECTION_ID');
return `${API_BASE}/collections/${collectionId}/items/${encodeURIComponent(itemId)}`;
}
function headers(): HeadersInit {
return {
Authorization: `Bearer ${requireEnv('WEBFLOW_API_TOKEN')}`,
'Content-Type': 'application/json',
Accept: 'application/json',
};
}
export type Gallery = { images: GalleryImage[]; lastUpdated: string | null };
export async function getGallery(itemId: string): Promise<Gallery> {
const res = await fetch(itemUrl(itemId), { headers: headers() });
if (!res.ok) {
throw new Error(`Webflow read failed: ${res.status} ${await res.text()}`);
}
const item = (await res.json()) as {
lastUpdated?: string;
fieldData?: Record<string, unknown>;
};
const field = item.fieldData?.[requireEnv('WEBFLOW_GALLERY_FIELD')];
return {
images: Array.isArray(field) ? (field as GalleryImage[]) : [],
lastUpdated: item.lastUpdated ?? null,
};
}
export function mergeImages(current: GalleryImage[], incoming: GalleryImage[]): GalleryImage[] {
const urls = new Set(current.map((img) => img.url).filter(Boolean));
const alts = new Set(current.map((img) => img.alt).filter(Boolean));
const additions: GalleryImage[] = [];
for (const img of incoming) {
if ((img.url && urls.has(img.url)) || (img.alt && alts.has(img.alt))) continue;
if (img.url) urls.add(img.url);
if (img.alt) alts.add(img.alt);
additions.push(img);
}
return [...current, ...additions];
}
export async function setGallery(itemId: string, images: GalleryImage[]): Promise<number> {
// skipInvalidFiles=false makes one bad URL fail the whole write instead of vanishing.
const res = await fetch(`${itemUrl(itemId)}?skipInvalidFiles=false`, {
method: 'PATCH',
headers: headers(),
body: JSON.stringify({
fieldData: { [requireEnv('WEBFLOW_GALLERY_FIELD')]: images },
}),
});
if (!res.ok) {
throw new Error(`Webflow update failed: ${res.status} ${await res.text()}`);
}
const item = (await res.json()) as { fieldData?: Record<string, unknown> };
const saved = item.fieldData?.[requireEnv('WEBFLOW_GALLERY_FIELD')];
return Array.isArray(saved) ? saved.length : 0;
}
Estos asistentes lanzan el código de estado y el texto de respuesta de Webflow ante cualquier fallo, por lo que una llamada fallida informa exactamente de qué salió mal, por ejemplo, cuando un token caduca. setGallery devuelve el número de imágenes que Webflow almacenó realmente, leído de su respuesta en lugar de la lista que envió.
5. Crea el manejador de rutas protegido
Añade un manejador de rutas POST que verifique el secreto compartido, rechace las entradas incorrectas y luego llame a los asistentes en orden. Utilizo una pequeña comparación de tiempo constante escrita en JavaScript simple en lugar de timingSafeEqual para mantener la verificación del secreto compatible con ambos entornos.
Esa función existe en el entorno de ejecución de Workers desplegado, pero no bajo next dev, por lo que el código que depende de ella puede funcionar en producción y fallar en tu portátil, o viceversa.
La ruta no tiene su propio límite de velocidad, así que mantén el secreto largo y aleatorio y llámalo solo desde tus propios sistemas. Cada solicitud consume tres llamadas a la API de datos frente a un límite de velocidad de 60 o 120 por minuto, dependiendo del plan.
Implementación de la ruta
Crea :app/api/gallery/[itemId]/route.ts
// app/api/gallery/[itemId]/route.ts
import { NextResponse } from 'next/server';
import { getGallery, mergeImages, setGallery, type GalleryImage } from '@/lib/webflow';
// No runtime export: Webflow Cloud does not support the Next.js edge runtime target.
const MAX_IMAGES = 25;
function safeEqual(a: string, b: string): boolean {
const enc = new TextEncoder();
const x = enc.encode(a);
const y = enc.encode(b);
let diff = x.length ^ y.length;
const len = Math.max(x.length, y.length);
for (let i = 0; i < len; i++) {
diff |= (x[i] ?? 0) ^ (y[i] ?? 0);
}
return diff === 0;
}
export async function POST(
request: Request,
{ params }: { params: Promise<{ itemId: string }> }
) {
const secret = process.env.GALLERY_SYNC_SECRET;
const provided = request.headers.get('x-gallery-secret') ?? '';
if (!secret || !safeEqual(provided, secret)) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { itemId } = await params;
const body = (await request.json().catch(() => null)) as { images?: unknown } | null;
const raw = Array.isArray(body?.images) ? (body.images as GalleryImage[]) : [];
const incoming = raw.filter(
(img) =>
typeof img?.url === 'string' &&
img.url.startsWith('https://') &&
typeof img?.alt === 'string' &&
img.alt.length > 0
);
if (incoming.length === 0) {
return NextResponse.json(
{ error: 'Send at least one image with an https url and alt text' },
{ status: 400 }
);
}
try {
const first = await getGallery(itemId);
const merged = mergeImages(first.images, incoming);
if (merged.length > MAX_IMAGES) {
return NextResponse.json(
{ error: `A multi-image field holds at most ${MAX_IMAGES} images` },
{ status: 422 }
);
}
// Webflow has no conditional write, so re-read just before writing to narrow the race.
const check = await getGallery(itemId);
if (check.lastUpdated !== first.lastUpdated) {
return NextResponse.json({ error: 'Item changed during the update, retry' }, { status: 409 });
}
const after = await setGallery(itemId, merged);
return NextResponse.json({ itemId, before: first.images.length, after });
} catch (error) {
return NextResponse.json({ error: (error as Error).message }, { status: 502 });
}
}
Si más adelante añades un formulario de navegador que llame a esta ruta, mantén el secreto en un servidor que controles, porque todo lo que se envía al navegador es público.
El manejador devuelve el recuento de la galería antes de la fusión y el recuento que Webflow almacenó después, por lo que es fácil detectar un envío duplicado porque ambos coinciden. Cualquier imagen sin una https URL y texto alternativo no vacío se descarta, y una solicitud sin ninguna imagen restante recibe un 400.
Una fusión que supere las 25 imágenes devuelve un 422, y un elemento que cambió entre la lectura y la escritura devuelve un 409 para que el emisor pueda reintentarlo. Las llamadas fallidas a Webflow se devuelven como un 502 con el mensaje de Webflow.
Prueba de la ruta local
Con npm run dev en ejecución, envía una prueba POST a tu ruta local:
curl -s -X POST "http://localhost:3000/api/gallery/ITEM_ID" \
-H "Content-Type: application/json" \
-H "x-gallery-secret: replace-with-a-long-random-string" \
-d '{"images":[{"url":"https://cdn.example.com/products/chair-side.jpg","alt":"Oak chair, side view"}]}'
Esta llamada local cambia el elemento real del CMS, así que usa tu elemento de prueba. Después de una escritura exitosa, elimina la nueva imagen en el CMS para que la prueba en vivo comience desde la misma galería.
6. Despliega y añade una imagen a través de la ruta en vivo
Sigue las instrucciones de despliegue de Webflow Cloud para tu repositorio y monta la aplicación en /gallery-sync en la configuración del entorno. Añade las cuatro variables de .env.local al entorno de despliegue, almacenando WEBFLOW_API_TOKEN y GALLERY_SYNC_SECRET como variables secretas, luego despliega y espera hasta que el despliegue aparezca como activo.
La aplicación está montada bajo la ruta que elegiste, por lo que esa ruta va antes de /api en la URL. La ruta solo acepta URLs de imágenes, porque Webflow Cloud no tiene acceso al sistema de archivos; sube los archivos al almacenamiento o a tu DAM primero y envía sus HTTPS URLs públicas.
Si los archivos están en Google Drive, la guía de URLs de imágenes de Drive muestra cómo servirlos desde tu propio dominio. Tu DAM o tarea programada hará esta misma llamada más tarde, así que probarla manualmente ahora también prueba la integración.
Verificación del punto final en vivo
Envía el POST en vivo, intercambiando tus propios valores:
curl -s -X POST "https://www.example.com/gallery-sync/api/gallery/ITEM_ID" \
-H "Content-Type: application/json" \
-H "x-gallery-secret: replace-with-a-long-random-string" \
-d '{"images":[{"url":"https://cdn.example.com/products/chair-side.jpg","alt":"Oak chair, side view"}]}'
La primera llamada devuelve un JSON como {"itemId":"...","before":2,"after":3}, y el elemento del CMS muestra la nueva imagen al final de su galería. Lee el elemento de nuevo con curl y confirma que las dos entradas originales mantuvieron sus fileId valores, lo que demuestra que la escritura los referenció en lugar de subir copias.
Luego vuelve a ejecutar el mismo POST: after se mantiene en 3 porque el texto alternativo coincidió con la entrada almacenada.
¿Por qué fallan las actualizaciones de imágenes múltiples en la API de Webflow?
Las actualizaciones de imágenes múltiples suelen fallar en cuatro puntos: la compilación, la escritura de la galería en sí, las sincronizaciones simultáneas y la publicación.
El despliegue falla en un Route Handler que se compila localmente
Causa: La directiva de tiempo de ejecución de Next.js provoca que la compilación falle porque el adaptador OpenNext Cloudflare que utiliza Webflow Cloud para desplegar Next.js no es compatible con ese destino. La página de "bring-your-own-app" utiliza "edge" para dos cosas distintas.edge
Webflow Cloud se ejecuta en Cloudflare Workers, una plataforma edge, y la página lo describe así: "Webflow Cloud despliega tu aplicación utilizando el tiempo de ejecución Edge, lo que permite un alojamiento rápido y distribuido globalmente". El destino de tiempo de ejecución de Next.js es una configuración independiente. Esa página sigue indicando que se añada la directiva a las rutas de la API.edge
Solución: Mantén las rutas compatibles con Workers eliminando la directiva de cada Route Handler y página. Yo omito esa exportación porque las rutas ya se ejecutan en la plataforma Workers sin ella.export const runtime = 'edge'
Si una plantilla de inicio o un asistente de programación por IA sigue reinsertando la línea, añade una regla de lint o una comprobación de revisión de código que marque la cadena antes de que llegue a un despliegue.
La ruta devuelve un 502 al escribir la galería
Causa: El 502 proviene del bloque try-catch del controlador. Al escribir, las causas habituales son una URL de imagen que Webflow no puede obtener, un archivo de más de 4 MB o un campo que contiene la etiqueta del campo (el nombre que ven los editores en el CMS) en lugar de su slug. Con un error, una URL incorrecta hace que toda la solicitud falle con un 400 de Webflow, que la ruta reporta como un 502.catchWEBFLOW_GALLERY_FIELDskipInvalidFiles=false
Solución: Lee el texto del error dentro del cuerpo del 502. Si comienza con "Webflow update failed", contiene el código de estado y el texto de respuesta de Webflow, que indican el campo o archivo que falló. Abre cada nueva URL de imagen en una ventana privada del navegador para confirmar que es pública y pesa menos de 4 MB.
Si una respuesta anterior reportó un recuento de 0 en un elemento que visiblemente tiene imágenes, corrige el campo al slug correspondiente en el entorno de Webflow Cloud y vuelve a desplegar.beforeWEBFLOW_GALLERY_FIELD
La galería solo contiene las imágenes más recientes después de una sincronización
Causa: Dos solicitudes para el mismo elemento se solaparon, o bien otro script escribió en el campo solo con sus nuevas entradas. El solapamiento ocurre así con esta ruta: las solicitudes A y B llegan casi al mismo tiempo, y ambas llaman a la lectura mientras el elemento contiene 2 imágenes.getGallery
A fusiona su nueva imagen y hace un PATCH de una lista de 3 imágenes. B, que sigue trabajando con las 2 imágenes que leyó, fusiona su propia imagen nueva y hace un PATCH de una lista de 3 imágenes diferente. Como el PATCH envía una lista completa cada vez, la lista de B puede reemplazar a la de A, y el elemento termina con 3 imágenes en lugar de 4. La imagen de A desaparece, aunque ambas respuestas reportaron un recuento inicial de 2 y uno final de 3.setGallerybeforeafter
Solución: Mantén las adiciones de cada elemento juntas en un solo POST, ya que la matriz de imágenes acepta varias entradas y el PATCH las añade en una sola escritura.imagesmergeImages
La API de Webflow no tiene escritura condicional, por lo que la ruta solo puede reducir el margen de error: vuelve a leer el elemento justo antes del PATCH y devuelve un 409 si ha cambiado. Cuando un emisor tenga que enviar varias solicitudes, haz que espere a cada una en lugar de lanzarlas todas a la vez, y reintenta si recibes un 409. Dirige toda escritura de galería a través de esta ruta para que ningún otro script envíe una lista parcial.lastUpdated
El CMS muestra las nuevas imágenes pero la página en vivo sigue mostrando la galería antigua
Causa: Un PATCH en el elemento escribe la versión en borrador, y el cambio llega al sitio en vivo solo después de que el elemento o el sitio se publiquen.PATCH
Solución: Repite el curl GET en el elemento: si muestra la nueva entrada, la escritura fue exitosa y solo queda publicar. Publica el sitio desde Webflow o, para automatizarlo, llama a la publicación después de la escritura, dentro del mismo bloque, para que un fallo en la publicación aparezca como un 502. Recarga la página en vivo en una ventana privada, ya que una pestaña en caché puede seguir mostrando la galería antigua.POST /v2/collections/{collection_id}/items/publish{"itemIds":[itemId]}setGallerytry
Qué puedes construir a continuación con la API de Webflow y Webflow Cloud
Una sincronización nocturna de fotos de proveedores es el siguiente paso lógico: mantienes el DAM como fuente de verdad mientras los editores conservan el control de la galería en Webflow. El endpoint de actualización masiva de elementos acepta hasta 100 elementos por solicitud, así que puedes actualizar muchas galerías con unas pocas llamadas.
Para sincronizar colecciones completas con una base de datos propia, la guía de base de datos externa utiliza la misma API de datos.
Para el paso de publicación y el resto de los endpoints de elementos, la documentación para desarrolladores de Webflow detalla cada solicitud que esta ruta podría añadir.
Preguntas frecuentes
¿Cómo elimino o reordeno imágenes en la galería?
Lee la galería con getGallery, filtra o reordena las entradas y luego pasa el array completo a setGallery. Envía el array al completo, no solo las imágenes modificadas, ya que cada escritura reemplaza la lista almacenada. Como la ruta de la galería solo añade elementos, gestiona las eliminaciones en una segunda ruta protegida por la misma clave secreta.
¿Cómo omito las imágenes que no han cambiado desde la última sincronización?
Compara sumas de verificación antes de enviar las imágenes. Webflow Cloud admite crypto.subtle.digest('MD5', ...) como extensión de Cloudflare, así que tu sincronización puede calcular el hash de los bytes de la imagen y saltarse las que ya se enviaron. Úsalo solo para detectar cambios: Cloudflare advierte que "MD5 se considera un algoritmo débil. No confíes en MD5 para la seguridad".
¿Puedo proteger la ruta con middleware en lugar de una comprobación de cabecera?
Sí, usa middleware.ts, pero recomiendo mantener la comprobación de cabecera como una segunda capa de autenticación. Webflow Cloud solo admite middleware de tiempo de ejecución Edge en Workers. Aunque Next.js 16 renombra el middleware a proxy, mantén middleware.ts: proxy.ts utiliza el tiempo de ejecución de Node, no puede optar por Edge y no se ejecutará en Webflow Cloud.
¿Esta compilación funciona en un sitio que aún muestra un nombre de plan antiguo?
Sí. La compilación necesita un sitio con CMS y un token de API de datos, independientemente de cómo se llame el plan en tu panel. Montar la aplicación en un dominio personalizado es lo único que requiere un plan Premium o superior.
¿Cuántas solicitudes puede gestionar esta ruta en mi plan?
Cada solicitud tiene un límite de 20 segundos y 30 segundos de CPU en todos los planes, margen suficiente para esta ruta. El límite más estricto es la tasa por minuto de la API de datos, ya que cada solicitud consume tres llamadas; la página de límites de Webflow detalla las cifras de Webflow Cloud.





