Un cuestionario con puntuación necesita un estado de respuesta fiable, y una aplicación de Webflow Cloud te lo ofrece sin tener que configurar decenas de reglas de mostrar y ocultar.
Crear un cuestionario con puntuación es una duda recurrente en la comunidad de Webflow, y la mayoría de los tutoriales dependen de interacciones apiladas de mostrar y ocultar para revelar respuestas y llevar la cuenta. Esa configuración está bien para un par de preguntas, pero cuando necesitas una puntuación real y un estado de respuesta fiable, es mucho más fácil gestionar esa lógica en el código de la aplicación.
El mejor patrón está un nivel por encima: un componente de cliente de React en una aplicación Next.js de Webflow Cloud, con las preguntas del cuestionario gestionadas en Webflow CMS para que los editores puedan añadir y actualizar contenido sin tocar una sola línea de código.
En esta guía, exploramos esa configuración de principio a fin. Las preguntas residen en una colección de Webflow CMS. El estado del cuestionario se gestiona mediante un useReducer hook. La puntuación y las explicaciones de cada pregunta se muestran en una pantalla de resultados. Opcionalmente, las finalizaciones se registran en un almacén de clave-valor de Webflow Cloud para obtener estadísticas.
¿Qué necesitas para crear un cuestionario interactivo con puntuación en Webflow Cloud?
Necesitas una aplicación Next.js de Webflow Cloud y un sitio de Webflow con acceso a CMS. No se requieren servicios de terceros.
Estos son los requisitos previos:
- Un proyecto de Webflow Cloud con una aplicación Next.js desplegada o ejecutándose localmente
- Un sitio de Webflow con un plan que incluya colecciones CMS (el plan Premium Site o superior)
- Un token de API de Webflow con
cms:readalcance (generado en Configuración del sitio > Aplicaciones e integraciones > Acceso a la API) - Node.js 22 o superior para el desarrollo local
El contenido de las preguntas del cuestionario reside en Webflow CMS, lo que significa que tus editores pueden actualizar las preguntas, añadir otras nuevas y editar las explicaciones de las respuestas desde el panel de control de Webflow sin tocar la aplicación ni tener que volver a desplegarla.
Según mi experiencia, esta es la razón principal para usar Webflow CMS como fuente en lugar de codificarlo directamente en JSON.
6 pasos para crear un cuestionario interactivo en Webflow Cloud
La arquitectura se divide claramente:
- Webflow CMS almacena las preguntas
- El componente de servidor de Next.js las obtiene en tiempo de compilación con ISR
- El componente de cliente de React gestiona todo el estado del cuestionario
No hay viajes de ida y vuelta entre el cliente y el servidor durante el cuestionario; toda la máquina de estados se ejecuta en el navegador una vez que se cargan las preguntas.
Vamos con los pasos.
1. Crea la colección CMS de Webflow para las preguntas del cuestionario
En Webflow, ve a CMS > Collections y crea una nueva colección. Yo la llamo "Quiz Questions".
Luego, añade los siguientes campos:
Una vez creada la colección, añade tus preguntas. El campo Order te permite controlar la secuencia sin depender del orden de creación de los elementos del CMS, el cual puede cambiar al duplicar o importar elementos.
2. Obtén las preguntas desde la API de datos de Webflow
En la aplicación Next.js, obtén las preguntas del CMS de Webflow durante el tiempo de compilación usando ISR.
Crea una utilidad lib/questions.ts:
// lib/questions.ts
export type Question = {
id: string
question: string
options: { key: string; text: string }[]
correctAnswer: string
explanation: string
order: number
}
export async function getQuizQuestions(): Promise<Question[]> {
const res = await fetch(
`https://api.webflow.com/v2/collections/${process.env.WEBFLOW_COLLECTION_ID}/items?limit=100`,
{
headers: {
Authorization: `Bearer ${process.env.WEBFLOW_API_TOKEN}`,
accept: 'application/json',
},
// Revalidate at most every 5 minutes. ISR is experimental on
// Webflow Cloud, so treat this interval as best effort.
next: { revalidate: 300 },
}
)
if (!res.ok) throw new Error(`Webflow CMS fetch failed: ${res.status}`)
const { items } = await res.json() as {
items: {
id: string
fieldData: {
name: string
'option-a': string
'option-b': string
'option-c': string
'option-d': string
'correct-answer': string
explanation: string
order: number
}
}[]
}
return items
.map((item) => ({
id: item.id,
question: item.fieldData.name,
options: [
{ key: 'a', text: item.fieldData['option-a'] },
{ key: 'b', text: item.fieldData['option-b'] },
{ key: 'c', text: item.fieldData['option-c'] },
{ key: 'd', text: item.fieldData['option-d'] },
],
correctAnswer: item.fieldData['correct-answer'].toLowerCase(),
explanation: item.fieldData['explanation'],
order: item.fieldData['order'] ?? 0,
}))
.sort((a, b) => a.order - b.order)
}
Añade las variables de entorno a .env.local y al panel de entorno de Webflow Cloud:
WEBFLOW_API_TOKEN=your_token_here
WEBFLOW_COLLECTION_ID=your_quiz_questions_collection_id
Con la utilidad y las variables de entorno configuradas, getQuizQuestions() devuelve una matriz ordenada de objetos Question tipados que cualquier componente de servidor puede invocar, manteniendo el token de la API en el servidor en lugar de en el paquete del navegador.
Un detalle importante a tener en cuenta desde el principio es que el ISR en Webflow Cloud es actualmente experimental; por lo tanto, considera el intervalo de revalidación como una estimación y vuelve a desplegar cuando necesites que un cambio aparezca de inmediato. Una vez resuelta la obtención de datos, el siguiente paso es construir la máquina de estados que gestiona el cuestionario en el cliente.
3. Construye la máquina de estados del cuestionario
Recurro a useReducer en cuanto un componente tiene más de tres estados interdependientes.
Un cuestionario tiene al menos cinco:
- El índice de la pregunta actual
- La respuesta seleccionada para la pregunta actual
- Si la respuesta ya se ha revelado
- El historial de respuestas
- El estado de finalización
Usar useState para cada uno de ellos genera ese tipo de lógica de actualización dispersa que falla de formas sutiles cuando los usuarios hacen clic rápidamente.
La máquina de estados tiene cuatro estados:
idle(antes de empezar)active(pregunta visible, sin respuesta seleccionada)answered(respuesta seleccionada, explicación visible)complete(todas las preguntas completadas, resultados visibles)
Crear app/quiz/quizReducer.ts:
// app/quiz/quizReducer.ts
export type AnswerRecord = {
questionId: string
selected: string
correct: boolean
}
export type QuizState = {
status: 'idle' | 'active' | 'answered' | 'complete'
currentIndex: number
selectedAnswer: string | null
answers: AnswerRecord[]
}
export type QuizAction =
| { type: 'START' }
| { type: 'SELECT'; answer: string; questionId: string; isCorrect: boolean }
| { type: 'NEXT'; total: number }
| { type: 'RESTART' }
export const initialState: QuizState = {
status: 'idle',
currentIndex: 0,
selectedAnswer: null,
answers: [],
}
export function quizReducer(state: QuizState, action: QuizAction): QuizState {
switch (action.type) {
case 'START':
return { ...initialState, status: 'active' }
case 'SELECT':
if (state.status !== 'active') return state
return {
...state,
status: 'answered',
selectedAnswer: action.answer,
answers: [
...state.answers,
{
questionId: action.questionId,
selected: action.answer,
correct: action.isCorrect,
},
],
}
case 'NEXT': {
if (state.status !== 'answered') return state
const isLast = state.currentIndex >= action.total - 1
return {
...state,
status: isLast ? 'complete' : 'active',
currentIndex: state.currentIndex + 1,
selectedAnswer: null,
}
}
case 'RESTART':
return initialState
default:
return state
}
}
La SELECT acción es idempotente dentro de un turno: una vez que status pasa a 'answered', las acciones SELECT posteriores se ignoran. Esto evita que se puntúe doble si el usuario hace clic rápidamente.
4. Renderizar la interfaz del cuestionario
Crea el componente de cliente que gestiona el cuestionario. Recibe las preguntas como props (obtenidas desde el servidor, sin llamadas a la API del cliente durante el cuestionario en sí):
// app/quiz/QuizClient.tsx
'use client'
import { useReducer } from 'react'
import { quizReducer, initialState, type AnswerRecord } from './quizReducer'
import type { Question } from '@/lib/questions'
function QuestionScreen({
question,
questionNumber,
total,
selectedAnswer,
onSelect,
onNext,
isAnswered,
}: {
question: Question
questionNumber: number
total: number
selectedAnswer: string | null
onSelect: (key: string) => void
onNext: () => void
isAnswered: boolean
}) {
return (
<div className="quiz-question">
<p className="quiz-progress">Question {questionNumber} of {total}</p>
<h2>{question.question}</h2>
<div className="quiz-options">
{question.options.map((opt) => {
let state = 'default'
if (isAnswered) {
if (opt.key === question.correctAnswer) state = 'correct'
else if (opt.key === selectedAnswer) state = 'wrong'
}
return (
<button
key={opt.key}
className={`quiz-option quiz-option--${state}`}
onClick={() => !isAnswered && onSelect(opt.key)}
disabled={isAnswered}
>
<span className="quiz-option-key">{opt.key.toUpperCase()}.</span>
{opt.text}
</button>
)
})}
</div>
{isAnswered && (
<div className="quiz-explanation">
<p>
<strong>
{selectedAnswer === question.correctAnswer ? 'Correct!' : 'Not quite.'}
</strong>{' '}
{question.explanation}
</p>
<button className="quiz-btn" onClick={onNext}>
Next question
</button>
</div>
)}
</div>
)
}
function ResultsScreen({
answers,
questions,
onRestart,
}: {
answers: AnswerRecord[]
questions: Question[]
onRestart: () => void
}) {
const score = answers.filter((a) => a.correct).length
const pct = Math.round((score / questions.length) * 100)
return (
<div className="quiz-results">
<h2>You scored {score}/{questions.length} ({pct}%)</h2>
<div className="quiz-breakdown">
{questions.map((q, i) => {
const record = answers.find((a) => a.questionId === q.id)
return (
<div key={q.id} className={`quiz-result-item ${record?.correct ? 'correct' : 'wrong'}`}>
<p><strong>Q{i + 1}:</strong> {q.question}</p>
<p>Your answer: {q.options.find((o) => o.key === record?.selected)?.text ?? 'None'}</p>
{!record?.correct && (
<p>Correct answer: {q.options.find((o) => o.key === q.correctAnswer)?.text}</p>
)}
<p className="quiz-explanation-text">{q.explanation}</p>
</div>
)
})}
</div>
<button className="quiz-btn" onClick={onRestart}>Try again</button>
</div>
)
}
export default function QuizClient({ questions }: { questions: Question[] }) {
const [state, dispatch] = useReducer(quizReducer, initialState)
const currentQuestion = questions[state.currentIndex]
if (state.status === 'idle') {
return (
<div className="quiz-start">
<h2>{questions.length}-question quiz</h2>
<p>Select the best answer for each question. Your score and explanations appear at the end.</p>
<button className="quiz-btn" onClick={() => dispatch({ type: 'START' })}>
Start quiz
</button>
</div>
)
}
if (state.status === 'complete') {
return (
<ResultsScreen
answers={state.answers}
questions={questions}
onRestart={() => dispatch({ type: 'RESTART' })}
/>
)
}
return (
<QuestionScreen
question={currentQuestion}
questionNumber={state.currentIndex + 1}
total={questions.length}
selectedAnswer={state.selectedAnswer}
isAnswered={state.status === 'answered'}
onSelect={(key) =>
dispatch({
type: 'SELECT',
answer: key,
questionId: currentQuestion.id,
isCorrect: key === currentQuestion.correctAnswer,
})
}
onNext={() => dispatch({ type: 'NEXT', total: questions.length })}
/>
)
}
Ese único componente de cliente cubre ahora las tres pantallas: el aviso de inicio, la pregunta activa con respuesta y el desglose de resultados, cada uno controlado por el campo status del reducer. Como las preguntas llegan como props, nada aquí hace llamadas a la red durante el cuestionario. La última pieza es el componente de servidor que obtiene las preguntas y las entrega.
5. Configurar la página del componente de servidor
Obtén las preguntas en el servidor y pásalas como props. El estado del cuestionario permanece totalmente en el lado del cliente; el servidor solo proporciona los datos de las preguntas.
Aquí tienes el fragmento para la configuración:
// app/quiz/page.tsx
import { getQuizQuestions } from '@/lib/questions'
import QuizClient from './QuizClient'
export default async function QuizPage() {
const questions = await getQuizQuestions()
if (questions.length === 0) {
return <main><p>No quiz questions found. Add some in Webflow CMS.</p></main>
}
return (
<main className="quiz-container">
<QuizClient questions={questions} />
</main>
)
}
Como las preguntas se obtienen con una ventana de revalidate, las ediciones en el CMS de Webflow deberían aparecer en el cuestionario en vivo en unos minutos. ISR es actualmente experimental en Webflow Cloud, así que cuando una edición deba publicarse de inmediato, vuelve a desplegar la aplicación.
He utilizado este patrón de CMS como fuente para cuestionarios de generación de leads, donde el equipo de marketing actualiza las preguntas según el rendimiento de la campaña sin tener que involucrar a un desarrollador en cada cambio.
6. Registrar las finalizaciones del cuestionario en Webflow Cloud KV (opcional)
Si quieres hacer un seguimiento de cuántas personas completan el cuestionario y sus puntuaciones, el almacén de clave-valor integrado de Webflow Cloud es la opción de almacenamiento más ligera disponible. No requiere configuración de Drizzle ni migración de esquemas, solo una declaración de enlace y una llamada put .
Añade el enlace KV a wrangler.json:
{
"kv_namespaces": [
{
"binding": "QUIZ_KV",
"id": "your-kv-namespace-id"
}
]
}
Luego, crea un manejador de rutas que el cliente llame cuando el cuestionario termine:
// app/api/quiz/complete/route.ts
import { getCloudflareContext } from '@opennextjs/cloudflare'
import { NextResponse, type NextRequest } from 'next/server'
export async function POST(request: NextRequest) {
const { score, total, sessionId } = await request.json() as {
score: number
total: number
sessionId: string
}
const { env } = getCloudflareContext()
const kv = (env as { QUIZ_KV: KVNamespace }).QUIZ_KV
const entry = JSON.stringify({
score,
total,
percentage: Math.round((score / total) * 100),
completedAt: new Date().toISOString(),
})
// Store result keyed by session ID. Expires after 90 days.
await kv.put(`result:${sessionId}`, entry, { expirationTtl: 60 * 60 * 24 * 90 })
return NextResponse.json({ ok: true })
}
En QuizClient.tsx, dispara esta llamada cuando el reducer alcance el estado 'complete' usando un useEffect:
useEffect(() => {
if (state.status !== 'complete') return
const score = state.answers.filter((a) => a.correct).length
const sessionId = crypto.randomUUID()
fetch('/api/quiz/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ score, total: questions.length, sessionId }),
})
}, [state.status])
Esto se ejecuta una vez cuando el cuestionario pasa a complete. Los resultados almacenados se pueden ver en el panel de control de Webflow Cloud, en la sección de almacenamiento de tu entorno.
¿Qué hace que la creación de un cuestionario interactivo en Webflow Cloud falle?
Estos tres problemas aparecen constantemente al pasar del desarrollo local a producción.
Las preguntas aparecen en el orden incorrecto
El campo Order existe precisamente por esta razón, pero solo funciona si el .sort() en getQuizQuestions() se ejecuta correctamente. Si las preguntas siguen apareciendo desordenadas, verifica que el campo Order esté completado en el CMS de Webflow para cada elemento. Los elementos del CMS sin un valor de Order toman por defecto 0 en la ordenación, lo que los agrupa al principio.
Las respuestas de la sesión anterior aparecen al recargar
El estado del cuestionario reside en useReducer, que se restablece a initialState en cada carga de página. Si ves respuestas antiguas, algo está restaurando el estado desde localStorage o un hook personalizado. He visto que esto sucede cuando un componente de cuestionario se envuelve accidentalmente en una biblioteca de persistencia de estado. Busca cualquier envoltorio de almacenamiento persistente en el árbol de componentes.
ISR muestra preguntas antiguas tras una actualización del CMS de Webflow
El ajuste revalidate: 300 significa que la caché se actualiza cada cinco minutos. Si necesitas una propagación más rápida (por ejemplo, acabas de corregir una errata en una respuesta correcta), vuelve a desplegar la aplicación Cloud o reduce revalidate a 60 segundos.
La revalidación bajo demanda a través de revalidatePath de Next.js conectada a un webhook de Webflow es otra opción, aunque tanto ISR como la revalidación bajo demanda siguen siendo experimentales en Webflow Cloud, por lo que un nuevo despliegue es la forma más fiable de forzar contenido fresco.
Crea más cosas con este patrón
La máquina de estados del cuestionario en esta guía se adapta fácilmente a formatos más complejos: preguntas cronometradas (añade un campo timeRemaining al estado y una cuenta atrás de useEffect ), lógica de ramificación (envía NEXT con un índice de destino basado en la respuesta) y cuestionarios de personalidad (acumula puntuaciones de rasgos en los registros de respuesta en lugar de un sistema binario de correcto/incorrecto).
Para una integración más profunda entre tu aplicación Webflow Cloud y el contenido del CMS de Webflow, la documentación para desarrolladores de Webflow cubre la API de datos, los enlaces de almacenamiento y la configuración del entorno.
Preguntas frecuentes
¿Puedo usar las interacciones de Webflow para el cuestionario en lugar de una aplicación Cloud?
Puedes, pero no te lo recomiendo para nada que requiera puntuación o validación de respuestas. Los cuestionarios basados en interacciones exponen las respuestas correctas en el DOM (cualquier visitante puede abrir las herramientas de desarrollo y verlas), no pueden calcular puntuaciones en el servidor y fallan cuando aumenta el número de preguntas. El enfoque de Webflow Cloud mantiene la validación de respuestas en el estado de JavaScript, donde debe estar.
¿Cómo evito que los visitantes vean las respuestas correctas en el código fuente?
El campo correctAnswer se incluye en la carga útil de preguntas enviada al navegador, por lo que es visible en las herramientas de desarrollo de React y en las respuestas de red. Para cuestionarios informales (pruebas de conocimiento, imanes de leads), esto es aceptable. Para evaluaciones donde hacer trampas es un problema, traslada la validación de respuestas a un Route Handler: envía solo el texto de la pregunta y las opciones al cliente, envía las respuestas al servidor y haz que el servidor valide contra un campo que nunca salga del backend.
¿Puedo usar más de cuatro opciones de respuesta?
Sí. La estructura de la colección CMS en esta guía utiliza cuatro campos de opción fijos por simplicidad. Para opciones de longitud variable, utiliza un campo de referencia múltiple que apunte a una colección de "Opciones de respuesta", o almacena las opciones como una cadena JSON en un campo de texto plano y analízala en la función de mapeo.
¿Qué plan requiere Webflow Cloud?
El alojamiento de aplicaciones Webflow Cloud está disponible en los planes de sitio de pago de Webflow. Las colecciones CMS utilizadas en esta guía requieren un plan que incluya el CMS, que es el plan Premium o superior, ya que el plan Básico no incluye colecciones CMS. Consulta los precios de Webflow para conocer los detalles actuales de los niveles.





