Una ruta de Webflow Cloud puede copiar una colección del CMS a Google Sheets cada vez que la ejecutes, para que quienes necesiten los datos dejen de pedirte exportaciones en CSV.
Exportar los datos del blog a Google Sheets ayuda a los usuarios que no usan Webflow, como los responsables de contenido y especialistas en SEO, a revisar títulos y URLs sin necesidad de acceso al Designer. Las exportaciones estáticas en CSV se quedan obsoletas enseguida. Una sincronización dedicada mantiene la hoja actualizada con los datos más recientes del CMS, incluyendo el estado de borrador y las fechas de publicación.
La API de datos de Webflow expone cada colección del CMS mediante REST, por lo que un servidor puede leer lo que editas visualmente. En esta guía, crearemos un Route Handler de Next.js en Webflow Cloud que copie una colección en una pestaña de hoja de cálculo con cada llamada autenticada.
¿Qué necesitas para sincronizar los datos del CMS de Webflow con Google Sheets en Webflow?
Necesitas cinco cosas antes de empezar:
- Sitio de Webflow: Un sitio con al menos una colección del CMS que ya contenga elementos, además de Webflow Cloud, disponible desde el plan Starter en adelante.
- Token de la API de Webflow: Un token de sitio con permiso de lectura del CMS, además del ID de la colección.
- Cuenta de servicio de Google: Un proyecto de Cloud con la API de Sheets habilitada, una cuenta de servicio con una clave JSON y una hoja de cálculo compartida con su dirección de correo.
- Entorno de desarrollo local: Node.js 22 o superior, Next.js 15 o superior y npm, el único gestor de paquetes que la documentación de "bring your own app" indica como compatible.
- Secreto compartido: Una cadena larga y aleatoria que protege la ruta; cualquiera que la tenga puede leer la colección y sobrescribir la hoja.
Con todo esto listo, la construcción consiste en un único Route Handler y un puñado de variables de entorno, y la hoja se llenará con la primera solicitud autenticada.
6 pasos para sincronizar datos del CMS de Webflow con Google Sheets en Webflow Cloud
La construcción es una POST ruta en una aplicación Next.js en Webflow Cloud que extrae los elementos de una colección y reescribe una hoja. La autenticación de Google utiliza Web Crypto porque la ruta desplegada se ejecuta en Cloudflare Workers.
Primero configuro las credenciales (Google y luego Webflow), sigo con la aplicación y su ruta, y termino con el despliegue y un disparador en vivo.
1. Crea la cuenta de servicio de Google y comparte la hoja
Crea una cuenta de servicio en lugar de un cliente OAuth, ya que la ruta se ejecuta de servidor a servidor sin nadie presente para hacer clic en una pantalla de consentimiento. En la consola de Google Cloud, elige o crea un proyecto, habilita la API de Google Sheets, crea una cuenta de servicio y descarga una clave JSON para ella. El archivo contiene dos valores que la ruta necesita: client_email y private_key.
Ahora abre la hoja de cálculo que quieres rellenar, haz clic en Compartir y pega la dirección client_email de la cuenta de servicio con el rol de Editor. La gente suele saltarse este paso y, sin él, cada escritura devuelve un error de permiso aunque el intercambio de tokens haya tenido éxito.
Copia el ID de la hoja de cálculo de la URL, la cadena larga entre /d/ y /edit, y anota el nombre de la pestaña que quieres escribir (una hoja de cálculo nueva usa Sheet1). Ahora deberías tener el correo de la cuenta de servicio, el texto de la clave privada, el ID de la hoja de cálculo y una pestaña que la cuenta pueda editar.
2. Genera el token de API de Webflow y copia el ID de la colección
Configura el acceso de lectura al contenido del CMS antes de tocar la colección en sí. Genera un token de API de Webflow y asígnale únicamente permisos de lectura del CMS; la ruta nunca escribe en Webflow, por lo que un token con permisos de escritura supone un riesgo innecesario. Copia el token en un gestor de contraseñas de inmediato.
A continuación, localiza el ID de la colección. Mientras estés en ella, anota los slugs de los campos que quieras incluir en la hoja. La ruta utiliza el ID de la colección para solicitar los datos del CMS y una lista de slugs de campos separados por comas para decidir qué columnas mostrar. Los slugs de campo comunes incluyen name y slug.
Mantén cada slug exactamente como lo define Webflow, ya que la ruta busca item.fieldData[f] mediante esa cadena y escribe una celda vacía cuando no hay un valor coincidente.
En este punto, ya tienes un token de solo lectura, un ID de colección y una lista breve de slugs de campos para exportar.
3. Prepara la aplicación Next.js en npm
Prepara una aplicación Next.js básica con TypeScript y el App Router, y utiliza npm en todo momento. Webflow Cloud solo admite npm y no es compatible con pasos de instalación escritos como pnpm add o yarn add, así que nunca permito que otro gestor de paquetes se acerque a estos proyectos, ni siquiera localmente.
Ejecuta la estructura desde un directorio vacío:
npx create-next-app@15 cms-sheets-sync --typescript --app --no-tailwind --eslint
cd cms-sheets-sync
npm run dev
La página predeterminada debería cargarse en http://localhost:3000. Deja next.config tranquilo: la documentación de configuración de Webflow Cloud indica que no necesitas añadir un adaptador, una ruta base ni un modo de salida, ya que la ruta de montaje se inyecta durante la compilación.
Tampoco necesitas instalar nada en el lado de Google. La ruta utiliza únicamente la API Fetch y la interfaz crypto.subtle de Web Crypto, ambas APIs web estándar en lugar de módulos de Node, lo que mantiene la lista de dependencias vacía y evita tener que adivinar qué módulos de Node podría requerir un cliente de terceros una vez desplegado.
Una línea que debes evitar, aunque una página de documentación en vivo todavía la sugiera: no añadas export const runtime = 'edge' a la ruta. Esa directiva apunta al entorno de ejecución edge de Next.js, que el adaptador OpenNext Cloudflare no puede compilar.
Ahora tienes una aplicación Next.js 15 (o posterior) en funcionamiento, sin dependencias adicionales y lista para el archivo de ruta.
4. Escribe el controlador de ruta de sincronización
Escribe un único controlador POST que verifique el secreto compartido, recorra la colección por páginas, intercambie un JWT (JSON Web Token) firmado por un token de acceso de Google y escriba las filas. Una actualización de valores solo escribe las celdas de su carga útil, por lo que una colección que se redujera de 40 a 30 elementos dejaría 10 filas obsoletas al final.
Por lo tanto, la ruta escribe primero y luego borra todo lo que hay debajo de la nueva última fila, de modo que, si la escritura falla, se mantiene la sincronización anterior en lugar de dejar una pestaña vacía.
La ruta autentica cada llamada con un secreto compartido, lo compara en tiempo constante y mantiene todas las credenciales en el lado del servidor. Añade una limitación de tasa (rate limiting) en el origen antes de compartir la URL ampliamente.
Crea con este contenido:app/api/sync/route.ts
// app/api/sync/route.ts
import { NextResponse } from "next/server";
const SHEETS_SCOPE = "https://www.googleapis.com/auth/spreadsheets";
function base64url(input: ArrayBuffer | string): string {
const bytes =
typeof input === "string"
? new TextEncoder().encode(input)
: new Uint8Array(input);
let binary = "";
for (const b of bytes) binary += String.fromCharCode(b);
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function pemToBuffer(pem: string): ArrayBuffer {
const body = pem
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replace(/\s+/g, "");
const binary = atob(body);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return bytes.buffer;
}
// Hand-rolled because timingSafeEqual exists on the deployed Workers
// runtime but not under local `next dev`.
function constantTimeEqual(a: string, b: string): boolean {
if (a.length !== b.length) return false;
let diff = 0;
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
return diff === 0;
}
async function getGoogleAccessToken(): Promise<string> {
const email = process.env.GOOGLE_CLIENT_EMAIL!;
const pem = process.env.GOOGLE_PRIVATE_KEY!.replace(/\\n/g, "\n");
const now = Math.floor(Date.now() / 1000);
const header = base64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
const claims = base64url(
JSON.stringify({
iss: email,
scope: SHEETS_SCOPE,
aud: "https://oauth2.googleapis.com/token",
iat: now,
exp: now + 3600,
})
);
const key = await crypto.subtle.importKey(
"pkcs8",
pemToBuffer(pem),
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
false,
["sign"]
);
const signature = await crypto.subtle.sign(
"RSASSA-PKCS1-v1_5",
key,
new TextEncoder().encode(`${header}.${claims}`)
);
const assertion = `${header}.${claims}.${base64url(signature)}`;
const res = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion,
}),
});
if (!res.ok) {
throw new Error(`Google token exchange failed: ${res.status} ${await res.text()}`);
}
const data = (await res.json()) as { access_token: string };
return data.access_token;
}
type WebflowItem = {
id: string;
lastPublished: string | null;
isDraft: boolean;
isArchived: boolean;
fieldData: Record<string, unknown>;
};
type ItemsPage = {
items: WebflowItem[];
pagination: { limit: number; offset: number; total: number };
};
async function fetchAllItems(collectionId: string, token: string): Promise<WebflowItem[]> {
const items: WebflowItem[] = [];
let offset = 0;
while (true) {
const res = await fetch(
`https://api.webflow.com/v2/collections/${collectionId}/items?limit=100&offset=${offset}`,
{ headers: { Authorization: `Bearer ${token}`, accept: "application/json" } }
);
if (!res.ok) {
throw new Error(`Webflow API failed: ${res.status} ${await res.text()}`);
}
const page = (await res.json()) as ItemsPage;
items.push(...page.items);
offset += page.pagination.limit;
if (page.items.length === 0 || offset >= page.pagination.total) break;
}
return items;
}
function toCell(v: unknown): string {
if (v === null || v === undefined) return "";
return typeof v === "object" ? JSON.stringify(v) : String(v);
}
export async function POST(request: Request) {
const provided = request.headers.get("x-sync-secret") ?? "";
const expected = process.env.SYNC_SECRET ?? "";
if (!expected || !constantTimeEqual(provided, expected)) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const required = [
"WEBFLOW_API_TOKEN",
"WEBFLOW_COLLECTION_ID",
"GOOGLE_CLIENT_EMAIL",
"GOOGLE_PRIVATE_KEY",
"GOOGLE_SPREADSHEET_ID",
];
const missing = required.filter((name) => !process.env[name]);
if (missing.length) {
return NextResponse.json({ error: "Missing variables", missing }, { status: 500 });
}
const items = await fetchAllItems(
process.env.WEBFLOW_COLLECTION_ID!,
process.env.WEBFLOW_API_TOKEN!
);
const fields = (process.env.SHEET_FIELDS ?? "name,slug")
.split(",")
.map((f) => f.trim())
.filter(Boolean);
const rows: string[][] = [
["id", "lastPublished", "isDraft", "isArchived", ...fields],
...items.map((item) => [
item.id,
item.lastPublished ?? "",
String(item.isDraft),
String(item.isArchived),
...fields.map((f) => toCell(item.fieldData[f])),
]),
];
const accessToken = await getGoogleAccessToken();
const spreadsheetId = process.env.GOOGLE_SPREADSHEET_ID!;
const sheetName = process.env.GOOGLE_SHEET_NAME ?? "Sheet1";
// Quote the tab name so names with spaces, or names like "A1", still parse.
const tab = `'${sheetName.replace(/'/g, "''")}'`;
const values = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values`;
const authHeaders = {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
};
// Write first, so a failed write leaves the previous sync in place.
const written = await fetch(
`${values}/${encodeURIComponent(`${tab}!A1`)}?valueInputOption=RAW`,
{
method: "PUT",
headers: authHeaders,
body: JSON.stringify({ range: `${tab}!A1`, majorDimension: "ROWS", values: rows }),
}
);
if (!written.ok) {
throw new Error(`Sheets update failed: ${written.status} ${await written.text()}`);
}
// Then clear any rows left over from a larger earlier sync.
const leftover = `${tab}!A${rows.length + 1}:ZZ`;
const cleared = await fetch(`${values}/${encodeURIComponent(leftover)}:clear`, {
method: "POST",
headers: authHeaders,
body: "{}",
});
if (!cleared.ok) {
throw new Error(`Sheets clear failed: ${cleared.status} ${await cleared.text()}`);
}
return NextResponse.json({ synced: items.length, columns: rows[0].length });
}
La clave privada llega desde una variable de entorno y pasa por .replace(/\\n/g, "\n"), porque pegar un PEM (el formato de clave codificado en texto) en un formulario de configuración puede convertir sus saltos de línea en pares literales de barra invertida-n, y una clave en ese estado ya no se decodifica como PEM.
Los campos de imagen y archivo llegan como objetos, así que toCell los escribe como texto JSON en lugar de "[object Object]"; los campos de referencia múltiple llegan como matrices de IDs de elementos y reciben el mismo tratamiento.
Solicitar 100 elementos por página, el máximo, mantiene una colección grande dentro del límite de tasa de la API de Webflow por minuto y reduce los viajes de ida y vuelta dentro del tiempo de espera de la solicitud. El nombre de la pestaña se pone entre comillas para que los nombres con espacios se sigan analizando como un rango.
Inicia npm run dev y envía esta solicitud para confirmar que la ruta se compila y rechaza un secreto incorrecto:
curl -i -X POST http://localhost:3000/api/sync -H "x-sync-secret: wrong"
Un 401 con {"error":"Unauthorized"} es el resultado correcto aquí; la ejecución completa requiere que las variables de entorno estén configuradas.
5. Añade las variables de entorno y despliega en Webflow Cloud
Configura las ocho variables de entorno que aparecen a continuación en .env.local para que next dev las detecte, y haz lo mismo en el entorno de Webflow Cloud, marcando los tres valores confidenciales como secretos.
En Webflow Cloud, tanto las variables secretas como las que no lo son están disponibles para el proceso de compilación y para la aplicación desplegada en tiempo de ejecución, y los secretos se ocultan en los registros de compilación.
Ese último detalle es la razón por la que la clave privada debe ir en una variable secreta y nunca en un archivo incluido en el repositorio, aunque la ruta no pudiera leer archivos en Workers de todos modos.
<>
Usa estos nombres, marcando los tres confidenciales como secretos:
| Variable | Valor | ¿Secreto? |
|---|---|---|
WEBFLOW_API_TOKEN |
El token de sitio de solo lectura | Sí |
WEBFLOW_COLLECTION_ID |
El ID de la colección desde la configuración de la misma | No |
SHEET_FIELDS |
Slugs de campo separados por comas, por ejemplo name,slug,post-summary |
No |
GOOGLE_CLIENT_EMAIL |
client_email del JSON de la cuenta de servicio |
No |
GOOGLE_PRIVATE_KEY |
private_key del JSON de la cuenta de servicio, incluyendo las líneas BEGIN y END |
Sí |
GOOGLE_SPREADSHEET_ID |
El ID de la URL de la hoja de cálculo | No |
GOOGLE_SHEET_NAME |
El nombre de la pestaña, Sheet1 si no se ha cambiado |
No |
SYNC_SECRET |
Una cadena larga y aleatoria que tú generes | Sí |
| Variable → Valor → ¿Secreto? |
|---|
WEBFLOW_API_TOKEN |
| El token de sitio de solo lectura |
| Sí |
WEBFLOW_COLLECTION_ID |
| El ID de la colección desde la configuración de la misma |
| No |
SHEET_FIELDS |
Slugs de campo separados por comas, por ejemplo name,slug,post-summary |
| No |
GOOGLE_CLIENT_EMAIL |
client_email del JSON de la cuenta de servicio |
| No |
GOOGLE_PRIVATE_KEY |
private_key del JSON de la cuenta de servicio, incluyendo las líneas BEGIN y END |
| Sí |
GOOGLE_SPREADSHEET_ID |
| El ID de la URL de la hoja de cálculo |
| No |
GOOGLE_SHEET_NAME |
El nombre de la pestaña, Sheet1 si no se ha cambiado |
| No |
SYNC_SECRET |
| Una cadena larga y aleatoria que tú generes |
| Sí |
Esta ruta no necesita NEXT_PUBLIC_BASE_PATH, porque nada en el navegador la llama. Webflow Cloud no crea esa variable, así que si más adelante añades una página que llame a la ruta desde el lado del cliente, añádela tú mismo con la ruta de montaje como valor.
La tabla también sirve como documento de entrega en un proyecto para clientes: indica exactamente qué credenciales existen y quién emite cada una.
Envía el repositorio a GitHub, conéctalo a Webflow Cloud y crea un entorno para tu rama principal, siguiendo la documentación de Webflow sobre cómo traer tu propia aplicación. Elige una ruta de montaje como /sync; la aplicación residirá bajo esa ruta en tu sitio, por lo que la URL pública de la ruta será https://your-domain.com/sync/api/sync.
Si no tienes un dominio personalizado, usa la propia URL del entorno con la misma ruta de montaje. Añade las variables, inicia el primer despliegue y observa el registro de compilación. Una compilación exitosa y una URL de entorno que responda 401 a una solicitud POST no autenticada indican que la ruta está activa y rechazando llamadas sin autorización.
6. Inicia la sincronización y verifica la hoja
Llama a la ruta desplegada una vez con el secreto real y abre la hoja de cálculo. La ruta es un endpoint HTTP sencillo, así que cualquier cosa que pueda enviar un POST con una cabecera puede activarla, desde una terminal hasta un programador de tareas que ya utilices en otro lugar. Cada ejecución exitosa sobrescribe la pestaña desde el principio, por lo que una segunda ejecución reemplaza el resultado de la primera.
Reemplaza el dominio, la ruta de montaje y el secreto, y ejecuta:
curl -s -X POST https://your-domain.com/sync/api/sync \
-H "x-sync-secret: YOUR_SYNC_SECRET"
La respuesta es un objeto JSON corto donde es el número de elementos devueltos para la colección y syncedcolumns es el recuento de cabeceras (cuatro columnas fijas más los slugs de tus campos).
Cambia a la hoja: la fila 1 contiene las cabeceras id, lastPublished, isDraft, isArchived y los nombres de tus campos, y cada fila siguiente es un elemento de la colección. Filtra isDraft y isArchived a false para ver solo los elementos a los que un visitante podría acceder.
Si más adelante eliminas un slug de , limpia la pestaña manualmente, ya que la ruta elimina las filas sobrantes pero no las columnas. Mantén SHEET_FIELDSSYNC_SECRET en el código del lado del servidor; un botón del navegador necesita su propia verificación en el servidor, como una sesión.
Cada llamada exitosa ahora deja la hoja coincidiendo con los datos de la colección más recientes devueltos a la ruta, con el estado de borrador y las marcas de tiempo de la última publicación junto a los campos que elegiste.
¿Qué causa que falle una sincronización de Webflow CMS a Google Sheets?
Mantengo la sincronización fiable comprobando cuatro categorías recurrentes de fallos en Webflow Cloud: una directiva de tiempo de ejecución edge, una URL a la que le falta la ruta de montaje, una clave leída desde el disco y una verificación de secreto que difiere entre next dev y Workers.
Rastreo cada entrada desde el síntoma que observas hasta el detalle del tiempo de ejecución de Webflow Cloud que hay detrás.
La compilación de Webflow Cloud falla en la ruta de sincronización, pero la compilación de next pasa localmente
Causa: Webflow Cloud despliega Next.js a través del adaptador OpenNext Cloudflare, y ese adaptador no admite el objetivo de tiempo de ejecución edge de Next.js. La palabra "edge" significa dos cosas diferentes aquí. Webflow Cloud se ejecuta en Cloudflare Workers, que es una plataforma edge, y la documentación describe el despliegue de esa manera.
El tiempo de ejecución de Next.js es un objetivo de compilación independiente que edgeexport const runtime = 'edge' permite activar en una ruta, y OpenNext no puede compilarlo. La página de documentación de "bring-your-own-app" todavía te dice que añadas esa directiva a las rutas de la API, por lo que un desarrollador que siga la documentación al pie de la letra puede producir una compilación que el adaptador no puede completar.
Solución: Elimina la directiva de app/api/sync/route.ts y vuelve a desplegar. La ruta ya se ejecuta en la plataforma Workers sin ella; no hay ninguna línea de reemplazo que añadir.
Si más adelante añades un al proyecto, mantén ese nombre de archivo: la documentación de personalización del framework establece que el middleware de tiempo de ejecución de Node.js no es compatible y solo funciona el middleware de tiempo de ejecución Edge en el entorno de ejecución de Workers, y el cambio de nombre de middleware.tsproxy.ts en Next.js 16 se ejecuta en el tiempo de ejecución de Node, por lo que un archivo renombrado no puede ejecutarse en absoluto.
La ruta da un error 404 en el sitio en vivo pero responde en next dev
Causa: La ruta en vivo responde en una URL pública que incluye la ruta de montaje de la aplicación, mientras que la URL local no. Localmente, sirve el manejador en . En Webflow Cloud, con una ruta de montaje de , el mismo manejador responde en next devhttp://localhost:3000/api/sync/synchttps://your-domain.com/sync/api/sync.
Un comando curl o una tarea programada copiada de las pruebas locales apunta a en el dominio en vivo, lo cual omite la ruta de montaje y devuelve un 404. La ruta de montaje se inyecta en el momento de la compilación y no es algo que configures en /api/syncnext.config, por lo que nada en el repositorio sugiere que la ruta exista.
Solución: Antepón a la URL de cada llamador externo la ruta de montaje que se muestra en la configuración del entorno. Para una página en la aplicación que llame a la ruta desde el navegador, añade una variable configurada con la ruta de montaje y construye la URL como NEXT_PUBLIC_BASE_PATH${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/api/sync.
Esto se debe a que las llamadas fetch del lado del cliente deben incluir manualmente la ruta base.
Una comprobación rápida: si la raíz de la URL del entorno carga la página de inicio de tu aplicación, añade /api/sync a esa URL exacta y haz un POST a ella.
The deployed sync route returns 500 while next dev runs the same code
La ruta de sincronización desplegada devuelve un error 500, mientras que next dev ejecuta el mismo código sin problemas.fs.readFileSyncnode:fs
It is a semantic runtime limit, so nothing in the editor flags it: the import type-checks and local next dev runs on Node, so the problem can surface only once the code runs on Webflow Cloud. node:path, by contrast, is available, which is why a project can import one Node built-in without trouble and break on the next.
Causa: La ruta desplegada devuelve un error 500 cuando el código de la cuenta de servicio de Google intenta leer el archivo de clave desde el disco con , ya que ese patrón no puede ejecutarse aquí. La ruta desplegada se ejecuta en Cloudflare Workers, donde no está disponible en la fecha de compatibilidad que utiliza Webflow Cloud.fs.readFileSync
Put client_email and private_key into the GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY environment variables, with the key marked as a secret, and let getGoogleAccessToken read them from process.env in app/api/sync/route.ts.
Se trata de un límite semántico del entorno de ejecución, por lo que nada en el editor lo detecta: la importación pasa las comprobaciones de tipo y la ejecución local funciona en Node, así que el problema solo aparece cuando el código se ejecuta en Webflow Cloud. , por el contrario, sí está disponible, razón por la cual un proyecto puede importar un módulo nativo de Node sin problemas y fallar con el siguiente.crypto.subtle.importKey\n.replace(/\\n/g, "\n")
Remove the JSON file from the repository history as well, since a committed key stays in git even after the file is gone.
Solución: Elimina la llamada a , que no puede ejecutarse en Workers. Descarta también cualquier importación JSON del archivo de clave, ya que incluir una clave en el repositorio expone credenciales.
Cause: Switching to crypto.subtle.timingSafeEqual creates an environment mismatch because Workers exposes it as an extension on crypto.subtle, while the method does not exist under local next dev.
Coloca y en las variables de entorno y , marcando la clave como secreta, y permite que las lea desde en .crypto.subtle.timingSafeEqualconstantTimeEqual
Fix: Keep the comparison in plain JavaScript, as in constantTimeEqual, which runs in both places. After checking that lengths match, the function XORs every character instead of exiting on the first difference.
Si genera un error, la causa más probable es una clave pegada con secuencias literales en lugar de saltos de línea; la llamada a soluciona el caso común, y volver a pegar la clave con sus saltos de línea reales resuelve el resto.typeof fn === "function"next dev
What you can build next with Google Sheets and Webflow
Elimina también el archivo JSON del historial del repositorio, ya que una clave confirmada permanece en git incluso después de que el archivo haya desaparecido.
For anything past a one-way mirror, Webflow's developer docs cover the CMS endpoints a write route needs and the Webflow Cloud limits that apply to it.
Preguntas frecuentes
La comprobación del secreto falla en next dev tras cambiar a timingSafeEqual.
Not from inside Webflow Cloud, which has no built-in scheduler; the route runs only when called. Any external scheduler, such as a cron job or a CI workflow, can call it, sending the x-sync-secret header to the full mount-path URL. Each run rereads the whole collection and must finish inside Webflow Cloud's 20-second request timeout, so time a run on your largest collection before scheduling it often.
Causa: Cambiar a crea una discrepancia en el entorno porque Workers lo expone como una extensión de , mientras que el método no existe en el local.
A 403 after a successful token exchange often means the service account cannot edit the spreadsheet; token exchange does not confirm spreadsheet access. Open the sheet, click Share, and confirm the service account's client_email is listed with the Editor role. Also check that the Google Sheets API is enabled in the same Cloud project that issued the key.
Una ruta que llama a funciona en producción pero falla en tu máquina. Ambos entornos discrepan sobre una función que parece un estándar. La implementación manual de en la ruta evita el problema, pero una refactorización posterior hacia el asistente reintroduce la división.
Una ruta independiente puede leer filas de la hoja a través de la API de Sheets y enviarlas a la API REST de Webflow usando un token con los permisos de CMS necesarios. Define cómo gestiona la ruta el estado de los elementos y mantén la columna id como clave para asociar las filas con sus elementos correspondientes.
¿Qué plan de Webflow necesito para esta sincronización?
Cualquier plan de sitio, desde el nivel gratuito Starter en adelante, incluye Webflow Cloud, por lo que la ruta se despliega sin necesidad de un plan de pago. Para montar la aplicación bajo tu propio dominio personalizado, se requiere un plan Premium o superior.
¿Aparecen los borradores en la hoja y puedo excluirlos?
Sí, los borradores aparecen porque la ruta copia cada elemento que devuelve el endpoint junto con su valor de isDraft , y los elementos archivados también se incluyen. Para excluirlos, añade .filter((item) => !item.isDraft && !item.isArchived) al array de elementos antes de que la ruta cree las filas. Mantener los borradores en la hoja es ideal para planificar contenidos.





