Uno de los aspectos de Webflow que me resulta más interesante es el CMS.
Te permite crear colecciones de contenido que se adaptan a cualquier necesidad y mostrarlas en tu sitio. Aún más interesantes son las API que te permiten realizar operaciones CRUD completas (crear, leer, actualizar y eliminar) sobre tu contenido mediante el lenguaje de programación que prefieras. Los casos de uso para esto son evidentes y numerosos: desde sincronizar contenido con proveedores externos hasta automatizar tu contenido de Webflow.
Hoy voy a tratar otro uso interesante de las API: crear más API. Concretamente, quiero tomar el fantástico contenido de mi sitio en Webflow y ponerlo a disposición de otros mediante una API pública alojada en mi sitio. En mi caso, una API sencilla de "solo lectura", pero que permite a otros sitios y desarrolladores de aplicaciones aprovechar mi contenido. Esto podría integrarse mediante JavaScript, utilizarse en plataformas low-code/no-code y mucho más. Lo importante es que los datos estarán disponibles y serán útiles para todo el mundo. Así es como lo logré.
¿Cuál es nuestro contenido?
Para mi demostración, creé una nueva colección de CMS llamada Gatos. Esta colección consta de unas pocas propiedades sencillas:
- Nombre (una cadena de texto)
- Slug (una cadena de texto)
- Género (un conjunto de opciones: Macho, Hembra)
- Raza (una cadena de texto)
- Imagen (una imagen)
También utilicé Webflow AI para generar algo de contenido de muestra.

Dado este conjunto de contenido, mi objetivo ahora es exponerlo públicamente en formato JSON sin procesar. Para lograrlo, recurriré a otra función de Webflow: Webflow Cloud.
¡A la nube!
Webflow Cloud te permite alojar una aplicación full-stack de Next.js o Astro en la plataforma de Webflow, estrechamente vinculada a tu sitio. Esto es increíblemente potente, ya que te permite acceder a la flexibilidad de Astro o Next.js. Además, obtienes una cohesión automática con tu sitio, específicamente en lo que respecta al sistema de diseño. Así que, si has diseñado un sitio web atractivo en Webflow, puedes usar el mismo diseño en tu aplicación.
Seguí las instrucciones de Primeros pasos y seleccioné Astro como mi framework. También configuré un nuevo repositorio de GitHub para alojar el código de mi aplicación como parte del proceso. El flujo de despliegue de Webflow Cloud facilita la integración y entrega continuas (CI/CD), ya que solo tengo que confirmar mis cambios y Webflow los detectará y volverá a desplegarlos automáticamente.
Para mi API, utilicé la función de Endpoints de Astro. Esto te permite generar una ruta de API personalizada con cualquier nombre o extensión que se cree de forma dinámica. Con esto en mente, dentro del directorio pages de mi proyecto Astro, creé cats.json.js. Este archivo se encarga de varias cosas:
- Obtener una lista de elementos
Gatousando la API de CMS Listar elementos en vivo. - Reformatear el resultado para que sea más comprensible para consumidores externos.
- Y, por diversión, permitir el filtrado por nombre mediante parámetros de consulta.
Vamos a desglosarlo paso a paso. Aquí está la versión inicial que construí, y explicaré qué hace cada parte en detalle:
const CATS = "691b3bd2bb9b164290a66b91";
export const config = {
runtime: "edge",
};
export async function GET({ params, request, locals }) {
let url = new URL(request.url);
let qs = Object.fromEntries(url.searchParams);
let name = qs.name || "";
let KEY = locals.runtime.env.CMSKEY;
let filter = '';
if(name !== '') filter = '?name=' + encodeURIComponent(name);
const catsReq = await fetch(`https://api.webflow.com/v2/collections/${CATS}/items/live${filter}`, {
headers: {
"Authorization": `Bearer ${KEY}`,
}
});
const catsData = await catsReq.json();
const cats = catsData.items.map(c => {
return {
...c.fieldData,
id:c.id
}
});
return new Response(JSON.stringify(cats), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
});
}La primera línea es simplemente el ID de mi colección de CMS. Puedes obtenerlo mediante una llamada a la API (Listar colecciones), pero es aún más fácil abrir tu colección en el Diseñador de Webflow:

La siguiente parte, la exportación config, proviene de nuestra documentación sobre aplicaciones Astro en Webflow Cloud y rutas de API. Simplemente la copié tal cual.
Ahora pasemos a la función GET, el núcleo de una API. Empiezo comprobando la solicitud actual para ver si quien llama a la API quiere filtrar por nombre. Lo harían añadiendo ?name=X a la URL. Unas líneas más abajo, comprobamos si el valor contiene algo y, de ser así, lo añadimos a una variable filter que modificará nuestra llamada a la API de CMS.
Toma nota de esta línea: let KEY = locals.runtime.env.CMSKEY; Para trabajar con la API de CMS, necesitas una clave (se puede generar en la configuración de tu sitio), y en lugar de incrustarla directamente en el código (lo cual casi siempre es una mala idea), utilicé una variable de entorno. La documentación para desarrolladores también explica las diferentes formas de usar variables de entorno en tu código Astro. Como recordatorio, al crear claves, tienes la opción de limitar el alcance de lo que esa clave puede hacer. Te sugiero encarecidamente que lo hagas y crees una clave que solo pueda leer datos del CMS.
Muy bien, llegado a este punto, podemos acceder a la API. En la documentación de la API Listar elementos en vivo, puedes ver que el ID de la colección es el único valor obligatorio y está incrustado en la URL. También añadí el filtro de nombre, que devolverá los elementos que coincidan exactamente. (¡La búsqueda aproximada está en nuestra lista de mejoras!)
Cabe señalar que, aunque las API oficiales del CMS admiten la paginación, el endpoint de la API personalizada que usamos aquí puede gestionar una implementación mejor con ciertos compromisos. Podría encargarse de la paginación por sí mismo (usando un bucle para seguir llamando a la API hasta obtener todos los datos) o devolver la información al emisor. Es muy posible que la persona o el sitio que utilice nuestra API solo quiera un subconjunto. Del mismo modo, podríamos admitir la ordenación y el criterio de orden. Para esta implementación sencilla, me he limitado a lo mínimo indispensable. ¡Pero no olvides que el poder está en tus manos!
Antes de devolver los datos, hago un pequeño ajuste. Así es como la API suele devolver los elementos:
{
"items": [
{
"id": "62b720ef280c7a7a3be8cabe",
"lastPublished": "2022-06-30T13:35:20.878Z",
"lastUpdated": "2022-06-25T14:51:27.809Z",
"createdOn": "2022-06-25T14:51:27.809Z",
"fieldData": {
"name": "Senior Data Analyst",
"slug": "senior-data-analyst",
"url": "https://boards.greenhouse.io/webflow/jobs/26567701",
"department": "Data"
},
"cmsLocaleId": "66f6e966c9e1dc700a857ca3",
"isArchived": false,
"isDraft": false
},
{
"id": "62c880ef281c7b7b4cf9dabc",
"lastPublished": "2023-04-15T10:25:18.123Z",
"lastUpdated": "2023-04-10T11:45:30.567Z",
"createdOn": "2023-04-10T11:45:30.567Z",
"fieldData": {
"name": "Product Manager",
"slug": "product-manager",
"url": "https://boards.greenhouse.io/webflow/jobs/31234567",
"department": "Product"
},
"cmsLocaleId": "66f6e966c9e1dc700a857ca3",
"isArchived": false,
"isDraft": false
}
],
"pagination": {
"limit": 25,
"offset": 0,
"total": 2
}
}Quiero devolver solo los items, y como fieldData es la información relevante, quiero devolver solo eso, junto con el ID. Mi llamada map se encarga de ello. Y, de nuevo, podría optar por devolver más, por ejemplo, lastUpdated o createdOn; simplemente decidí no hacerlo para este ejemplo.
La última parte simplemente devuelve todo usando el tipo de contenido application/JSON.
No es tan difícil, ¿verdad? Una vez que la aplicación en la nube se despliega en mi sitio, puedo acceder a este endpoint en: mysite.com/app/cats.json. Filtrar por nombre es igual de sencillo: mysite.com/app/cats.json?name=Luna. Cuando lo ejecuté, ¡funcionó de maravilla! Pero noté algo extraño en mis resultados:
{
"gender": "e0331b12f028facd7e22b5e4246a882c",
"name": "Bella",
"breed": "Abyssinian",
"slug": "bella",
"id": "69208bfc6563afc5d003d48f"
},
{
"gender": "18e9905214ba268d86ae1222acd66ad8",
"name": "Oscar",
"breed": "American Shorthair",
"slug": "oscar",
"id": "69208bfc6563afc5d003d48d"
},¿Ves cómo el valor de gender es un ID? Si recuerdas, configuré el esquema de la colección con una opción de dos valores y, entre bastidores, esto se almacena como un valor de ID. De esa forma, si cambias el valor, todos los elementos de la colección tendrán el valor correcto. (Lo típico de la normalización de datos). Entonces, ¿qué hacemos?
El problema de las opciones
Hay un par de formas de resolver este entuerto, y cada una difiere en cuanto a lo sencilla y lo preparada para el futuro que queramos que sea la solución.
La solución más fácil sería identificar los dos valores de nuestras opciones, Male y Female, determinar los valores de ID y simplemente sustituirlos en los datos. Esto estaría codificado de forma rígida y no estaría preparado para el futuro si se añadiera otra opción. Dicho esto, puedes estar bastante seguro de estas cosas si conoces bien tus datos.
Una solución más compleja podría ser obtener los detalles de la colección mediante una llamada a la API. Esto devuelve información sobre cada campo de tu colección. Según nuestra documentación, puedes verlo en uno de los campos:
{
"id": "6f7e8d9c0b1a2e3d4c5b6a8f",
"isRequired": false,
"type": "Option",
"displayName": "Item Type",
"isEditable": true,
"slug": "item-type",
"helpText": "The type of item.",
"validations": {
"options": [
{
"name": "Survival Gear",
"id": "66f6e966c9e1dc700a857ca3"
},
{
"name": "Gadget",
"id": "66f6e966c9e1dc700a857ca4"
},
{
"name": "Other",
"id": "66f6e966c9e1dc700a857ca5"
}
]
}
}Sin embargo, esta solución requeriría, o al menos sugeriría encarecidamente, algo de almacenamiento en caché para no estar obteniendo constantemente los detalles de la colección en cada llamada.
Opté por un enfoque algo híbrido. Primero, necesitaba obtener los detalles usando la API de detalles de la colección mencionada anteriormente. Como ya he dicho, puedes obtener el ID de la colección a través del Diseñador, pero los detalles de los campos solo están disponibles a través de la API. Eso hice para mi colección de gatos.
A continuación, añadí esto a mi código:
// I take an array of raw CMS items and attempt to correct options
// This is a work in progress and could be improved. For now, I use a hard coded
// set of mappings
const optionMappings = {
"gender": [
{
"name": "Male",
"id": "18e9905214ba268d86ae1222acd66ad8"
},
{
"name": "Female",
"id": "e0331b12f028facd7e22b5e4246a882c"
}
]
}
const mapOptions = item => {
for(let k in item) {
if(optionMappings[k]) {
for(let o of optionMappings[k]) {
if(item[k] === o.id) {
item[k] = o.name;
}
}
}
}
return item;
}Empiezo con un objeto optionMappings que tiene una clave basada en el nombre del campo y, a continuación, un conjunto de ID y valores. Luego creé mapOptions, que toma el resultado "en bruto" de mi lista de elementos de la colección, comprueba si un campo está en optionMappings y, si es así, sustituye el valor id por el valor name. Aquí tienes el código completo actualizado:
const CATS = "691b3bd2bb9b164290a66b91";
export const config = {
runtime: "edge",
};
// I take an array of raw CMS items and attempt to correct options
// This is a work in progress and could be improved. For now, I use a hard coded
// set of mappings
const optionMappings = {
"gender": [
{
"name": "Male",
"id": "18e9905214ba268d86ae1222acd66ad8"
},
{
"name": "Female",
"id": "e0331b12f028facd7e22b5e4246a882c"
}
]
}
const mapOptions = item => {
for(let k in item) {
if(optionMappings[k]) {
for(let o of optionMappings[k]) {
if(item[k] === o.id) {
item[k] = o.name;
}
}
}
}
return item;
}
export async function GET({ params, request, locals }) {
let url = new URL(request.url);
let qs = Object.fromEntries(url.searchParams);
let name = qs.name || "";
let KEY = locals.runtime.env.CMSKEY;
let filter = '';
if(name !== '') filter = '?name=' + encodeURIComponent(name);
const catsReq = await fetch(`https://api.webflow.com/v2/collections/${CATS}/items/live${filter}`, {
headers: {
"Authorization": `Bearer ${KEY}`,
}
});
const catsData = await catsReq.json();
const cats = catsData.items.map(c => {
return {
...mapOptions(c.fieldData),
id:c.id
}
});
return new Response(JSON.stringify(cats), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
});
}Cabe destacar que ahora llamo a mapOptions en mi map para gestionar la reescritura. Una vez desplegado, ahora obtengo valores más claros:
{
"gender": "Female",
"name": "Bella",
"breed": "Abyssinian",
"slug": "bella",
"id": "69208bfc6563afc5d003d48f"
},
{
"gender": "Male",
"name": "Oscar",
"breed": "American Shorthair",
"slug": "oscar",
"id": "69208bfc6563afc5d003d48d"
},Puedes comprobarlo tú mismo aquí: https://raymonds-cool-site-b78797.webflow.io/app/cats.json
Una cosa más
En teoría, ya hemos terminado, ¿verdad? Como he mencionado antes, hay varias áreas de mejora para convertir esto en una API con más funciones, pero ya captas la idea. Sin embargo, quiero añadir un pequeño ajuste más al código.
Hace poco, Webflow lanzó las API de entrega de contenido CMS para todos los usuarios. Esta API se centra en la recuperación de contenido en tiempo real y cuenta con una caché y un rendimiento mejorados. Se trata de una API de solo lectura que solo admite obtener una lista de elementos de la colección o un solo elemento. Las solicitudes duplicadas se almacenan en caché durante 2 minutos para los planes Enterprise y 5 minutos para el resto de niveles. Y lo que es mejor, las respuestas almacenadas en caché no cuentan para los límites de tasa de la API de tu plan de sitio, lo que hace que este cambio sea aún mejor.
Lo mejor de todo es que el cambio completo consiste en pasar de esto:
const catsReq = await fetch(`https://api.webflow.com/v2/collections/${CATS}/items/live${filter}`, {
a esto:
const catsReq = await fetch(`https://api-cdn.webflow.com/v2/collections/${CATS}/items/live${filter}`, {
En concreto, solo tienes que cambiar el host y nada más. Puedes encontrar la versión final del código en el repositorio de la aplicación.
Próximos pasos
Webflow Cloud es una incorporación increíble a Webflow, ya que prácticamente te abre las puertas a cualquier cosa que quieras hacer en tu sitio. Como mencioné, mi uso de una sencilla ruta de API personalizada es solo un pequeño ejemplo de lo que es posible. ¡Explora la función tú mismo y trae tus dudas a nuestro Foro si necesitas ayuda!





