Webflow Cloud te permite colocar un panel de reservas seguro junto a tus páginas de marketing, utilizando middleware.ts y una cookie firmada para mantener sincronizados el acceso a las páginas y los datos del cliente.
Un sitio de marketing en Webflow ahora puede llevar a los clientes directamente a sus propias reservas sin tener que enviarlos a una aplicación independiente en un subdominio con su propio alojamiento, canal de despliegue y pantalla de inicio de sesión desconectada.
Webflow Cloud te permite montar una aplicación Next.js en una ruta del mismo sitio, de modo que /app/dashboard se ubique junto a las páginas de marketing y se publique desde el mismo proyecto.
Un panel de control fiable mantiene sus dos mitades alineadas en cuanto a identidad. La puerta de enlace decide si se debe renderizar la página y la ruta de datos decide qué reservas devolver.
Cuando ambas verifican cosas distintas, el usuario ve una lista vacía o, peor aún, una lista que pertenece a otra persona. Una cookie de sesión firmada con una clave secreta, y verificada por ambas partes con esa misma clave, es lo que hace que coincidan.
Eso es lo que produce esta compilación: una aplicación Next.js donde middleware.ts es la puerta de enlace, un gestor de rutas de inicio de sesión emite la cookie, los gestores de rutas de reserva confían únicamente en el ID de usuario que contiene, y una página de panel en el lado del cliente realiza las peticiones a través de la ruta de montaje que asigna Webflow Cloud.
¿Qué necesitas para crear un panel de inicio de sesión y reservas en Next.js dentro de Webflow?
Necesitas un sitio de Webflow, Node.js y npm instalados localmente, un proyecto de Next.js compatible, un endpoint de identidad y un endpoint de reservas. Webflow Cloud está disponible desde el plan gratuito Starter en adelante, mientras que montar una aplicación en un dominio personalizado requiere el plan Premium o superior.
En conjunto, estos requisitos previos proporcionan la base del proyecto, la verificación de identidad y la conexión privada con el almacén de reservas que el panel necesita.
Prepara estos cinco elementos antes de crear la aplicación:
- Sitio de Webflow: Utiliza el plan gratuito Starter o superior, y actualiza a Premium cuando montes el panel en tu dominio personalizado.
- Herramientas locales: Instala Node.js y npm localmente para poder estructurar, ejecutar y probar la aplicación antes de desplegarla.
- Proyecto Next.js: Utiliza un proyecto Next.js 15, que el comando de estructura crea con la estructura compatible de App Router. Fija la versión principal. Next.js 16 deja de usar
middleware.tsy lo renombra comoproxy.ts, y la documentación de Next.js indica que "Proxy utiliza por defecto el entorno de ejecución de Node.js" y que "la opción de configuración no está disponible en los archivos Proxy", por lo que no puede optar por Edge. Webflow Cloud solo ejecuta middleware con el entorno de ejecución Edge, por lo que en la versión 16 la puerta de enlace del paso 3 deja de funcionar. - Endpoint de identidad: Proporciona un endpoint HTTP que acepte un correo electrónico y una contraseña y devuelva un campo JSON
idsi tiene éxito. - Endpoint de reservas: Proporciona un endpoint HTTP que liste y cree reservas, protegido por un token de portador (bearer token) almacenado como secreto. Este es tu almacén de reservas.
Con estas piezas listas, puedes crear primero el límite de la sesión, conectar ambos endpoints del lado del servidor y, a continuación, desplegar el panel terminado a través de Webflow Cloud.
7 pasos para crear un panel de inicio de sesión y reservas en Next.js con Webflow Cloud
Crea el panel estructurando una aplicación Next.js compatible, compartiendo una sesión firmada entre el middleware y los gestores de rutas, conectando el almacén de reservas y añadiendo la ruta de montaje de Webflow Cloud como prefijo a las peticiones del navegador.
Mantén todos los archivos en un único proyecto de Next.js.
1. Prepara la aplicación Next.js con npm
Crea la aplicación con el App Router y TypeScript, y elige npm. El archivo de bloqueo (lockfile) de npm debe coincidir con el gestor de paquetes que utiliza Webflow Cloud. La documentación de Webflow es clara: "Actualmente, Webflow Cloud solo admite el gestor de paquetes npm". Mantén los archivos de bloqueo de otros gestores de paquetes fuera del repositorio.create-next-app
Ejecuta esto desde el directorio donde guardas tus proyectos:
npx create-next-app@15 booking-dashboard --typescript --app --eslint --no-src-dir --no-tailwind --import-alias "@/*" --use-npm
cd booking-dashboard
npm run dev
La versión importa y mucho. Next.js 16 marca como obsoleta la función que necesitas y la renombra, lo cual no funciona en Webflow Cloud, así que debes mantenerte en la versión 15. Dentro de la serie 15, asegúrate de estar por encima de la 15.2.3: las versiones desde la 15.0.0 hasta esa incluyen la vulnerabilidad GHSA-f82v-jwr5-mffw, un fallo crítico de omisión de autorización que permite que una solicitud manipulada se salte el middleware por completo.@15middleware.tsproxy.ts
Dado que el filtro de esta compilación es el middleware, una versión anterior te dejaría un panel de control abierto a cualquiera. La versión 15.x más reciente soluciona esto y es totalmente segura.@15
Ahora tienes un proyecto de Next.js con un directorio app y un archivo de bloqueo de npm. Deja next.config.js sin cambios, sin rutas base ni modos de salida; la página de configuración de Webflow lo define como "Sin adaptador, sin ruta base, sin wrangler.json", ya que la ruta de montaje se inyecta durante la compilación. No añadas export const runtime a los archivos de ruta.app/next.configexport const runtime = 'edge'
Abre localhost:3000 y deberías ver la página de bienvenida predeterminada de Next.js, lo que confirma que la cadena de herramientas funciona correctamente antes de añadir tu propio código.http://localhost:3000
2. Escribe el asistente de sesiones firmadas
Crea un único módulo compartido que mantenga sincronizados el filtro y las rutas de datos convirtiendo un ID de usuario en un token firmado y verificando ese token de vuelta a un ID de usuario. La firma es un HMAC sobre una carga útil en base64url, generada con crypto.subtle, la Web Crypto API que existe en el middleware de Next.js y en el entorno de ejecución Cloudflare Workers donde se despliega Webflow Cloud.crypto.subtle
Aquí utilizo crypto.subtle.digest para un comportamiento consistente entre entornos. subtle.verify es una extensión de Cloudflare para Web Crypto: existe en el entorno de ejecución de los Workers desplegados, pero no está definida en el entorno local de Node, donde el SubtleCrypto de Node no tiene ese método. Una comparación manual de la firma realiza la verificación y se comporta igual en ambos entornos.crypto.subtle.verifycrypto.subtle.timingSafeEqualnext devverify
Crea con este contenido:lib/session.ts
// lib/session.ts
const encoder = new TextEncoder();
const decoder = new TextDecoder();
export type SessionPayload = { sub: string; exp: number };
async function hmacKey(secret: string) {
return crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign', 'verify']
);
}
function toBase64Url(bytes: Uint8Array) {
return btoa(String.fromCharCode(...bytes))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
function fromBase64Url(text: string) {
const padded = text.replace(/-/g, '+').replace(/_/g, '/');
return Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
}
export async function createSession(
userId: string,
secret: string,
ttlSeconds = 60 * 60 * 24 * 7
) {
const payload: SessionPayload = { sub: userId, exp: Date.now() + ttlSeconds * 1000 };
const body = toBase64Url(encoder.encode(JSON.stringify(payload)));
const key = await hmacKey(secret);
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(body));
return `${body}.${toBase64Url(new Uint8Array(signature))}`;
}
export async function verifySession(
token: string,
secret: string
): Promise<SessionPayload | null> {
try {
const [body, signature] = token.split('.');
if (!body || !signature) return null;
const key = await hmacKey(secret);
const valid = await crypto.subtle.verify(
'HMAC',
key,
fromBase64Url(signature),
encoder.encode(body)
);
if (!valid) return null;
const payload = JSON.parse(decoder.decode(fromBase64Url(body))) as SessionPayload;
return payload.exp > Date.now() ? payload : null;
} catch {
return null;
}
}
Ahora tienes dos funciones que solo comparten un secreto. El bloque try/catch alrededor de la verificación es más importante de lo que parece: una cookie manipulada o truncada provoca un error en la función, y sin el catch, ese error se convertiría en un error de servidor en el middleware en lugar de una redirección al inicio de sesión.try-atob
Un token firmado con un secreto y verificado con otro devuelve false, que es exactamente el comportamiento que necesita el filtro.null
3. Protege el panel de control con middleware.ts
Añade un archivo middleware.ts en la raíz del proyecto para redirigir las solicitudes de página no autenticadas a /login y devolver una respuesta JSON de no autorizado para las solicitudes de API no autenticadas. El archivo verifica la cookie antes de gestionar cualquiera de los dos resultados.middleware.ts/login
El nombre del archivo es fundamental en Webflow Cloud. La documentación de personalización de frameworks de Webflow indica: "El middleware del entorno de ejecución de Node.js no es compatible. Solo el middleware del entorno Edge funciona en el entorno de ejecución de Workers". El middleware de Next.js en un archivo llamado middleware.ts se ejecuta en el entorno Edge de forma predeterminada, por eso funciona aquí sin ninguna directiva adicional.middleware.ts
Clona el objeto request para construir la redirección. La clonación mantiene la redirección vinculada a la URL de la solicitud entrante mientras cambias su ruta a /login.request.nextUrl/login
Guarda esto en la raíz del proyecto, junto a next.config.js:package.json
// middleware.ts
import { NextResponse, type NextRequest } from 'next/server';
import { verifySession } from '@/lib/session';
export async function middleware(request: NextRequest) {
const secret = process.env.SESSION_SECRET;
if (!secret) {
return new NextResponse('SESSION_SECRET is not configured', { status: 500 });
}
const token = request.cookies.get('session')?.value;
const session = token ? await verifySession(token, secret) : null;
if (session) {
return NextResponse.next();
}
if (request.nextUrl.pathname.startsWith('/api/')) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const loginUrl = request.nextUrl.clone();
loginUrl.pathname = '/login';
loginUrl.search = '';
loginUrl.searchParams.set('next', request.nextUrl.pathname);
return NextResponse.redirect(loginUrl);
}
export const config = {
matcher: ['/dashboard/:path*', '/api/bookings/:path*'],
};
El matcher cubre tanto la página como la API, por lo que una solicitud no autenticada a /api/data recibe una respuesta JSON de no autorizado. Añade el código a middleware.ts, reinicia el servidor de desarrollo y visita localhost:3000.fetchSESSION_SECRET=any-long-random-string.env.localnext devhttp://localhost:3000/dashboard
Deberías ser redirigido a /login, y una solicitud a /api/data debería devolver una respuesta JSON de no autorizado. La página del panel de control aún no existe; la redirección demuestra que el filtro está funcionando./login?next=%2Fdashboard/api/bookings
4. Crea la ruta y la página de inicio de sesión
Convierte una respuesta exitosa de tu endpoint de identidad en una cookie de sesión firmada. El manejador de rutas reenvía el correo electrónico y la contraseña a ese endpoint, y luego confía en una respuesta exitosa con una cadena válida id. El almacenamiento y la comparación de contraseñas permanecen en el AUTH_API_URL.
Esa es la única línea que un desarrollador de agencia cambia cuando un cliente modifica sus proveedores de identidad.
Coloca el controlador en :app/api/login/route.ts
// app/api/login/route.ts
import { NextResponse, type NextRequest } from 'next/server';
import { createSession } from '@/lib/session';
export async function POST(request: NextRequest) {
const authApiUrl = process.env.AUTH_API_URL;
const sessionSecret = process.env.SESSION_SECRET;
if (!authApiUrl || !sessionSecret) {
return NextResponse.json({ error: 'Server configuration is incomplete' }, { status: 500 });
}
let identityUrl: URL;
try {
identityUrl = new URL(authApiUrl);
} catch {
return NextResponse.json({ error: 'AUTH_API_URL is invalid' }, { status: 500 });
}
const { email, password } = (await request.json()) as {
email?: string;
password?: string;
};
if (!email || !password) {
return NextResponse.json({ error: 'Email and password are required' }, { status: 400 });
}
let identity: Response;
try {
identity = await fetch(identityUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ email, password }),
});
} catch {
return NextResponse.json({ error: 'Identity service is unavailable' }, { status: 502 });
}
if (!identity.ok) {
return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
}
let identityBody: unknown;
try {
identityBody = await identity.json();
} catch {
return NextResponse.json({ error: 'Identity service returned invalid JSON' }, { status: 502 });
}
if (
typeof identityBody !== 'object' ||
identityBody === null ||
!('id' in identityBody) ||
typeof identityBody.id !== 'string' ||
!identityBody.id
) {
return NextResponse.json({ error: 'Identity service returned an invalid user id' }, { status: 502 });
}
const token = await createSession(identityBody.id, sessionSecret);
const response = NextResponse.json({ ok: true });
response.cookies.set('session', token, {
httpOnly: true,
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
path: '/',
maxAge: 60 * 60 * 24 * 7,
});
return response;
}
La cookie es httpOnly para que los scripts de la página no puedan leerla, y secure solo en producción para que el HTTP local siga funcionando. La página que envía datos a esta ruta es un componente de cliente y necesita su propia protección: el next parámetro proviene de la URL, así que acéptalo solo cuando sea una ruta del mismo sitio.
Sin esa comprobación, ?next=//evil.example se convierte en una redirección abierta tras un inicio de sesión exitoso.
El formulario de inicio de sesión va en app/login/page.tsx:
// app/login/page.tsx
'use client';
import { useState, type FormEvent } from 'react';
const base = process.env.NEXT_PUBLIC_BASE_PATH ?? '';
export default function LoginPage() {
const [error, setError] = useState<string | null>(null);
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const form = new FormData(event.currentTarget);
const response = await fetch(`${base}/api/login`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
email: form.get('email'),
password: form.get('password'),
}),
});
if (!response.ok) {
setError('Check your email and password.');
return;
}
const requested = new URLSearchParams(window.location.search).get('next');
const next =
requested && new URL(requested, window.location.origin).origin === window.location.origin
? requested
: '/dashboard';
window.location.assign(`${base}${next}`);
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" placeholder="Email" required />
<input name="password" type="password" placeholder="Password" required />
<button type="submit">Log in</button>
{error && <p>{error}</p>}
</form>
);
}
Añade AUTH_API_URL a .env.local apuntando a tu endpoint de identidad, luego envía credenciales válidas en /login. El tarro de cookies del navegador debería contener ahora una session cookie con un punto en medio, y la página debería navegar a /dashboard, que actualmente devuelve una respuesta de no encontrado porque aún no se ha escrito.
Esa respuesta confirma que la cookie fue verificada.
5. Añadir las rutas de reserva
Limita las solicitudes de listado y creación con el valor sub de la cookie verificada. Un archivo de manejador de rutas puede exponer GET y POST, donde ambos métodos toman el ID de usuario de esa cookie. Esto evita que un cliente que ha iniciado sesión lea o cree las reservas de otro: el navegador puede enviar cualquier userId que desee en JSON, y la ruta lo ignorará.
La ruta también contiene el token de portador para tu almacén de reservas. Esa clave reside en una variable de entorno del lado del servidor y se adjunta a la solicitud ascendente dentro del manejador, por lo que el navegador solo ve tu propia /api/bookings ruta.
Para un desarrollador de agencia, ese límite es la diferencia entre que el almacén de reservas de un cliente sea público o privado.
Crea app/api/bookings/route.ts con ambos métodos:
// app/api/bookings/route.ts
import { NextResponse, type NextRequest } from 'next/server';
import { verifySession } from '@/lib/session';
async function currentUserId(request: NextRequest, sessionSecret: string) {
const token = request.cookies.get('session')?.value;
if (!token) return null;
const session = await verifySession(token, sessionSecret);
return session?.sub ?? null;
}
function upstreamHeaders(bookingApiKey: string) {
return {
'content-type': 'application/json',
authorization: `Bearer ${bookingApiKey}`,
};
}
async function upstreamJson(upstream: Response) {
try {
return await upstream.json();
} catch {
return null;
}
}
export async function GET(request: NextRequest) {
const sessionSecret = process.env.SESSION_SECRET;
const bookingApiUrl = process.env.BOOKING_API_URL;
const bookingApiKey = process.env.BOOKING_API_KEY;
if (!sessionSecret || !bookingApiUrl || !bookingApiKey) {
return NextResponse.json({ error: 'Server configuration is incomplete' }, { status: 500 });
}
const userId = await currentUserId(request, sessionSecret);
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
let url: URL;
try {
url = new URL(bookingApiUrl);
} catch {
return NextResponse.json({ error: 'BOOKING_API_URL is invalid' }, { status: 500 });
}
url.searchParams.set('userId', userId);
let upstream: Response;
try {
upstream = await fetch(url, { headers: upstreamHeaders(bookingApiKey) });
} catch {
return NextResponse.json({ error: 'Booking store is unavailable' }, { status: 502 });
}
const body = await upstreamJson(upstream);
if (body === null) {
return NextResponse.json({ error: 'Booking store returned invalid JSON' }, { status: 502 });
}
return NextResponse.json(body, { status: upstream.status });
}
export async function POST(request: NextRequest) {
const sessionSecret = process.env.SESSION_SECRET;
const bookingApiUrl = process.env.BOOKING_API_URL;
const bookingApiKey = process.env.BOOKING_API_KEY;
if (!sessionSecret || !bookingApiUrl || !bookingApiKey) {
return NextResponse.json({ error: 'Server configuration is incomplete' }, { status: 500 });
}
const userId = await currentUserId(request, sessionSecret);
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { service, date } = (await request.json()) as { service?: string; date?: string };
if (!service || !date) {
return NextResponse.json({ error: 'service and date are required' }, { status: 400 });
}
let url: URL;
try {
url = new URL(bookingApiUrl);
} catch {
return NextResponse.json({ error: 'BOOKING_API_URL is invalid' }, { status: 500 });
}
let upstream: Response;
try {
upstream = await fetch(url, {
method: 'POST',
headers: upstreamHeaders(bookingApiKey),
body: JSON.stringify({ userId, service, date }),
});
} catch {
return NextResponse.json({ error: 'Booking store is unavailable' }, { status: 502 });
}
const body = await upstreamJson(upstream);
if (body === null) {
return NextResponse.json({ error: 'Booking store returned invalid JSON' }, { status: 502 });
}
return NextResponse.json(body, { status: upstream.status });
}
Estado de seguridad por ruta: POST /api/login no requiere autenticación de forma intencionada; la autorización no se aplica antes de establecer la identidad, y la ruta mantiene las credenciales y los secretos de firma en el lado del servidor; sin embargo, este ejemplo no implementa límites de tasa a nivel de aplicación ni controles contra ataques de fuerza bruta, y no realiza una comprobación explícita de origen o CSRF antes de emitir una cookie de sesión.
Ambos son requisitos de producción pendientes. GET /api/bookings autentica la cookie firmada y autoriza el acceso derivando el ID de usuario de su sub verificado; la protección CSRF no se aplica a esta operación de solo lectura, pero un límite de tasa sigue siendo un requisito de producción pendiente.
POST /api/bookings utiliza la misma autenticación y autorización basada en el usuario. Aun así, no tiene límite de tasa a nivel de aplicación ni comprobación explícita de origen o CSRF para la solicitud que cambia el estado, por lo que ambos siguen siendo requisitos de producción pendientes.
Requisitos de seguridad de producción y límites de gasto
Ninguna de estas rutas aplica un límite de gasto a nivel de aplicación para las llamadas al endpoint de identidad o al almacén de reservas, por lo que los límites de gasto también son un requisito de producción pendiente cuando cualquiera de los sistemas ascendentes se factura por uso.
El BOOKING_API_KEY y SESSION_SECRET permanecen en el lado del servidor, mientras que NEXT_PUBLIC_BASE_PATH es visible para el cliente de forma intencionada y no contiene secretos. Esta compilación no tiene flujo OAuth ni webhook, por lo que la validación de estado OAuth, la verificación de firma de webhook y la defensa contra repetición de webhook no se aplican.
La ruta verifica la cookie por segunda vez porque necesita el valor sub real para limitar la llamada ascendente. La verificación mantiene la identidad vinculada a la carga útil firmada y evita que los clientes la falsifiquen a través de una cabecera.
Añade BOOKING_API_URL y BOOKING_API_KEY a .env.local, luego, mientras hayas iniciado sesión, abre /api/bookings en el navegador. Deberías recibir lo que sea que tu almacén de reservas devuelva para tu usuario, en formato JSON, con el código de estado del propio almacén transmitido.
6. Crea la página del panel de control con el prefijo de la ruta base
Añade un prefijo a cada solicitud del navegador desde la página con para que un panel de control alojado en envíe las solicitudes a través de la misma ruta de montaje. El componente cliente enumera las reservas y envía otras nuevas.NEXT_PUBLIC_BASE_PATH/app/dashboard
Webflow Cloud inyecta la ruta de montaje en la compilación del lado del servidor, pero la documentación de "bring-your-own-app" es explícita sobre el lado del navegador: "Las llamadas fetch del lado del cliente deben incluir manualmente la ruta base para llegar correctamente a tus endpoints". Una simple le pide al sitio de Webflow , que el sitio no tiene.fetch('/api/bookings')/api/bookings
Localmente, sin ruta de montaje, la variable no está definida y el prefijo se resuelve como una cadena vacía, por lo que el mismo código funciona en ambos lugares. Un detalle de React: captura antes del , porque React lo anula una vez que el controlador cede, y llamar a después provocaría un error.event.currentTargetawaitreset()
Guarda la página como :app/dashboard/page.tsx
// app/dashboard/page.tsx
'use client';
import { useEffect, useState, type FormEvent } from 'react';
const base = process.env.NEXT_PUBLIC_BASE_PATH ?? '';
type Booking = { id: string; service: string; date: string };
export default function DashboardPage() {
const [bookings, setBookings] = useState<Booking[]>([]);
const [error, setError] = useState<string | null>(null);
async function loadBookings() {
const response = await fetch(`${base}/api/bookings`);
if (response.ok) {
setBookings(await response.json());
setError(null);
} else {
setError('Could not load bookings.');
}
}
useEffect(() => {
loadBookings();
}, []);
async function createBooking(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const formElement = event.currentTarget;
const form = new FormData(formElement);
const response = await fetch(`${base}/api/bookings`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ service: form.get('service'), date: form.get('date') }),
});
if (!response.ok) {
setError('Could not create the booking.');
return;
}
setError(null);
formElement.reset();
await loadBookings();
}
return (
<main>
<h1>Your bookings</h1>
{error && <p>{error}</p>}
<ul>
{bookings.map((booking) => (
<li key={booking.id}>
{booking.service} on {booking.date}
</li>
))}
</ul>
<form onSubmit={createBooking}>
<input name="service" placeholder="Service" required />
<input name="date" type="date" required />
<button type="submit">Book</button>
</form>
</main>
);
}
Inicia sesión localmente y abre . La lista debería completarse desde tu almacén de reservas, y al enviar el formulario deberías añadir una fila y actualizar la lista sin recargar la página./dashboard
Cierra la sesión eliminando la cookie en las herramientas de desarrollo y recarga; deberías volver a con en la cadena de consulta.session/loginnext=%2Fdashboard
7. Configura las variables de entorno y despliega en Webflow Cloud
Configura los secretos del servidor, los endpoints ascendentes y la ruta de montaje pública en Webflow Cloud antes de activar el despliegue de producción.
Añade estas cinco variables de entorno:
- SESSION_SECRET: Una cadena larga y aleatoria que firma y verifica la cookie; márcala como secreta para que Webflow la oculte en los registros de compilación.
- AUTH_API_URL: El endpoint que recibe un correo electrónico y una contraseña y responde correctamente con un campo JSON cuando las credenciales son válidas.
id - BOOKING_API_URL: El endpoint que enumera las reservas para un parámetro de consulta y acepta un cuerpo POST para crear una nueva.
userId - BOOKING_API_KEY: El token de portador que espera tu almacén de reservas; márcalo como secreto, ya que solo sale del servidor dentro de un Route Handler.
- NEXT_PUBLIC_BASE_PATH: La ruta de montaje que le diste a la aplicación, como , sin barra diagonal al final; Next.js la compila en el paquete del navegador durante la compilación.
/app
Ahora tienes la configuración de tiempo de ejecución completa para firmar sesiones, acceder a ambos servicios ascendentes y enrutar las solicitudes del navegador a través de la ruta de la aplicación montada.
La página de "bring-your-own-app" de Webflow indica: "Tanto las variables de entorno secretas como las no secretas están disponibles para el proceso de compilación de tu aplicación... y permanecen disponibles para la aplicación desplegada en tiempo de ejecución".
Esa única frase es la razón por la que funciona aquí: Next.js la lee durante la compilación para integrarla en el paquete del navegador, y Webflow Cloud la pone a disposición en esa etapa.NEXT_PUBLIC_BASE_PATH
Gestión de secretos de sesión entre entornos
El valor de en Webflow Cloud y el de pueden diferir; cada entorno firma y verifica sus propias cookies. Dentro de un mismo entorno, el valor debe permanecer idéntico entre la compilación y el tiempo de ejecución. Webflow Cloud lo garantiza exponiendo el mismo conjunto a ambos.SESSION_SECRET.env.local
El panel de entorno debería mostrar ahora las cinco variables, con y marcadas como secretas y sus valores ocultos.SESSION_SECRETBOOKING_API_KEY
Apunta el entorno al repositorio de tu aplicación, establece la ruta de montaje para que coincida con y activa un despliegue.NEXT_PUBLIC_BASE_PATH
Una vez finalizada la compilación, abre la ruta montada seguida de en el sitio activo. Deberías ser redirigido al montado y, tras iniciar sesión, ver tus reservas renderizadas en el montado, con la pestaña de red mostrando las solicitudes a ./dashboard/login/dashboard/app/api/bookings
¿Qué hace que un inicio de sesión y un panel de reservas de Next.js fallen en Webflow Cloud?
La mayoría de los fallos se deben a un tiempo de ejecución de Route Handler no compatible, un archivo de middleware renombrado, una ruta base pública incorrecta o un secreto de sesión faltante o inconsistente en el entorno desplegado.
Comprueba el tiempo de ejecución, el enrutamiento, la ruta de montaje o la configuración de verificación de cookies correspondiente para cada síntoma.
La compilación falla con un error de tiempo de ejecución de Route Handler
Causa: Webflow Cloud despliega Next.js a través del adaptador OpenNext Cloudflare, y dicho adaptador no es compatible con el objetivo de tiempo de ejecución de Next.js. Los desarrolladores añaden la línea de todos modos porque la palabra "edge" significa dos cosas distintas en la misma página de documentación.edge
Webflow Cloud se ejecuta en Cloudflare Workers, que es una plataforma edge, y la página de "bring-your-own-app" lo describe de esa manera. La misma página también difumina esa distinción al sugerir el objetivo de tiempo de ejecución de Next.js no compatible para las rutas de API.
Ese objetivo de tiempo de ejecución es independiente, y seguir la guía rompe la compilación.
Solución: Elimina , deja sin una ruta base o modo de salida, y vuelve a desplegar. Comprueba y si copiaste la directiva de la documentación.export const runtime = 'edge'next.configapp/api/login/route.tsapp/api/bookings/route.ts
Visitar se renderiza sin una redirección de inicio de sesión tras actualizar Next.js/dashboard
Causa: La ruta de actualización para las versiones más recientes de Next.js renombra a , y es posible que un codemod o un compañero con buena intención haya realizado ese cambio. se ejecuta en el tiempo de ejecución de Node.js y no puede optar por el tiempo de ejecución Edge, y el tiempo de ejecución de Workers de Webflow Cloud no ejecuta middleware de Node.js.middleware.tsproxy.tsproxy
El archivo se compila pero nunca se ejecuta en el sitio desplegado, por lo que la puerta desaparece y cualquier ruta bajo se renderiza para cualquiera. Las rutas de API siguen devolviendo una respuesta de no autorizado sin una cookie porque verifican la sesión por sí mismas, por lo que el panel permanece vacío y los datos siguen protegidos./dashboard
Solución: Mantén el archivo con el nombre y mantén la función exportada con el nombre . Si el cambio de nombre ya se ha aplicado, vuelve a cambiarlo y confirma en el entorno desplegado que una solicitud no autenticada a la ruta montada redirige a .middleware.tsmiddleware/dashboard/login
Comprueba en el archivo antes de asumir que la puerta está intacta después de cualquier actualización de versión de Next.js, y añade una comprobación en tiempo de despliegue que solicite el panel sin una cookie y haga fallar la canalización si la página se renderiza correctamente.git log
El panel se renderiza, pero la lista de reservas está vacía y las herramientas de desarrollo muestran una respuesta de no encontrado en/api/bookings
Causa: La falta de envía las solicitudes del lado del cliente a la raíz del sitio de Webflow en lugar de a la ruta de montaje de la aplicación. Una barra diagonal al final también produce una ruta de solicitud incorrecta. Si la variable se añadió después de la última compilación, el paquete del navegador sigue conteniendo el valor anterior porque Next.js integra los valores de en tiempo de compilación.NEXT_PUBLIC_BASE_PATHNEXT_PUBLIC_
Solución: Establece en la ruta de montaje de la aplicación sin una barra diagonal al final, luego activa una nueva compilación. Confirma en la pestaña de red de la página desplegada que las solicitudes van a (o la ruta de montaje que utilices) y devuelven JSON.NEXT_PUBLIC_BASE_PATH/app/api/bookings
Si el formulario de inicio de sesión funciona pero el panel no, compara ambas solicitudes; ambas leen la misma constante , por lo que una discrepancia significa que una página se compiló antes de que la variable existiera y quedó almacenada en caché.base
El inicio de sesión funciona en el sitio desplegado, pero el navegador vuelve inmediatamente a /login
Causa: La cookie se emitió y luego fue rechazada durante la verificación. Primero, comprueba que SESSION_SECRET esté configurado de forma coherente en el entorno desplegado. El middleware de esta compilación devuelve un error de servidor cuando falta el secreto, y los controladores de inicio de sesión y reservas rechazan una configuración incompleta antes de firmar o verificar una sesión.
Los secretos se ocultan en los registros de compilación, por lo que no puedes confirmar el valor leyéndolos; debes confirmarlo mediante el comportamiento.
Solución: Restaura la configuración de SESSION_SECRET , vuelve a desplegar y prueba de nuevo. Mantén crypto.subtle.verify para una verificación coherente: crypto.subtle.timingSafeEqual existe en el entorno de ejecución de Workers desplegado, pero no está definido en el next dev local. Si el bucle persiste, comprueba que el path de la cookie sea /.
Qué puedes construir a continuación con Next.js y Webflow
Una vez que la puerta de enlace, la sesión y las dos rutas de reserva estén activas, cada función adicional que pida un cliente será un controlador de ruta más que importe verifySession y reenvíe una solicitud con ámbito.
Prefiero dejar de codificar la lista de servicios reservables aquí. Se puede acceder al CMS de Webflow a través de la Data API, con relaciones y configuraciones regionales, para que un especialista en marketing pueda mantener el catálogo de servicios en una colección. Al mismo tiempo, un controlador de ruta lo lee con un token guardado como secreto en Webflow Cloud.
Preguntas frecuentes
¿Puedes usar Astro o Vite en lugar de Next.js para este panel?
Sí. Webflow Cloud acepta Astro 6 o 7, Next.js 15 o superior, y Vite 6.1 o superior con React, Vue, Svelte o JavaScript nativo. Puedes migrar el asistente de sesión Web Crypto sin cambios, pero debes mover la puerta de enlace de solicitud de middleware.ts al mecanismo de interceptación que proporcione el framework elegido para las rutas protegidas.
¿Puedes conservar los parámetros de consulta al redirigir a través del inicio de sesión?
No. Esta implementación solo puede conservar la ruta, ya que el middleware borra la cadena de búsqueda entrante antes de añadir el parámetro next . Para restaurar filtros o parámetros de campaña después de iniciar sesión, amplía la lógica de redirección manteniendo la validación de ruta del mismo sitio que bloquea los destinos relativos al protocolo tras la autenticación.
¿Cuándo deja de funcionar tu sesión firmada?
Tu sesión deja de funcionar cuando expira la carga útil firmada, la cookie alcanza su edad máxima o eliminas la cookie. Esta implementación establece ambos límites al iniciar sesión y no los actualiza en solicitudes posteriores, por lo que la actividad normal en el panel no extiende la sesión. Mantener una cookie caducada no restaura el acceso.
¿Puedes enviar campos adicionales o cambiar el propietario de la reserva desde el navegador?
No. No puedes cambiar el propietario de la reserva ni reenviar campos arbitrarios con este controlador. Solo lee el service y el date, luego construye el cuerpo ascendente con esos valores y el usuario de sesión verificado. Para admitir otro campo, añádelo explícitamente al tipo de solicitud, valídalo e inclúyelo en el cuerpo ascendente que construyas.





