Las listas filtradas del CMS de Webflow son muy útiles para organizar y mostrar contenido, pero en cuanto un usuario escribe una palabra parcial o busca en varios campos a la vez, se quedan cortas. Elasticsearch soluciona este problema y Webflow Cloud te ofrece el entorno necesario para ejecutar el proxy sin necesidad de un servidor independiente.
Las listas filtradas nativas del CMS de Webflow son excelentes para ordenar y filtrar, pero no admiten búsquedas de texto completo en varios campos ni gestionan variaciones ortográficas. Si necesitas que una consulta coincida simultáneamente con títulos de publicaciones, contenido del cuerpo, etiquetas y nombres de autor, necesitas algo más potente funcionando por detrás.
Puedes montar un clúster de Elasticsearch. La arquitectura es sencilla: el contenido del CMS de Webflow se indexa en Elasticsearch una vez (y se vuelve a indexar al publicar).
Un controlador de rutas de una aplicación de Webflow Cloud recibe las consultas de búsqueda desde el frontend, las reenvía a Elasticsearch mediante la API HTTP y devuelve resultados estructurados. El sitio de Webflow envía una fetch() llamada a ese controlador de rutas y muestra la respuesta.
En esta guía, veremos paso a paso cómo configurar un clúster de Elasticsearch en Elastic Cloud, crear un controlador de rutas en tu aplicación de Webflow Cloud para gestionar y estructurar las consultas de búsqueda, indexar el contenido de tu CMS de Webflow con un script local y conectar el campo de búsqueda en tu sitio de Webflow.
¿Qué necesitas para crear una búsqueda de texto completo con Elasticsearch en Webflow Cloud?
Necesitas un clúster de Elasticsearch, una aplicación de Webflow Cloud y un token de la API de datos de Webflow. No hace falta ningún paquete npm adicional más allá de lo que ya incluye la estructura de la aplicación Cloud.
Estos son los requisitos previos:
- Una cuenta de Elastic Cloud: La prueba gratuita de 14 días te ofrece un clúster de Elasticsearch alojado con un endpoint HTTPS público. No requiere configuración propia.
- Aplicación de Webflow Cloud: Ejecuta
webflow auth login && webflow cloud initsi aún no lo has hecho. - Untoken de la API de datos de Webflow: Para obtener los elementos del CMS que quieres indexar. Genera uno en Configuración del sitio > Aplicaciones e integraciones > Acceso a la API. Al seleccionar los alcances, habilita
cms:read. Eso es todo lo que necesita el script de indexación. Usa un token independiente con acceso de solo escritura en tu script de indexación local, nunca en el controlador de rutas desplegado. - El ID de la colección del CMS que quieres hacer buscable. Lo encontrarás en el Diseñador de Webflow en CMS → Configuración de la colección.
Una vez que tengas todo esto, la configuración te llevará entre 30 y 45 minutos. Aquí tienes cómo hacerlo.
Algo importante que debes saber de antemano: el cliente oficial de @elastic/elasticsearch Node.js no funciona en Cloudflare Workers. Depende de APIs de Node.js que el entorno de ejecución V8 isolate no expone.
Existe un ticket de seguimiento en elastic/elasticsearch-js #2032 sin solución definitiva. Yo uso la API REST HTTP de Elasticsearch directamente mediante fetch(). Funciona perfectamente, la interfaz de la API es estable y, de hecho, es más fácil de leer que el código del SDK.
5 pasos para crear una búsqueda de texto completo con Elasticsearch y Webflow Cloud
Elasticsearch se encarga del índice y del motor de consultas. La aplicación de Webflow Cloud gestiona el enrutamiento de las consultas y la estructuración de las respuestas. Estos pasos construyen cada pieza en orden: primero la configuración del clúster, luego el controlador de rutas, después el script de indexación y, finalmente, la conexión con el frontend.
1. Crea tu clúster de Elastic Cloud y define el mapeo del índice
Inicia sesión en Elastic Cloud y crea un nuevo despliegue. Yo uso el ajuste preestablecido de Elasticsearch (el nivel de hardware predeterminado es suficiente para el desarrollo).
Una vez que el despliegue esté listo, obtén estos dos valores del panel de control:
- El endpoint de Elasticsearch (una URL que termina en
.elastic-cloud.com) y crea una clave de API en Management → API Keys → Create API Key. - La clave de API que te proporciona Elastic Cloud ya está codificada. Ponla directamente en el
Authorization: ApiKey {key}header sin codificación adicional. No se necesita ninguna codificación extra.
Con el clúster en funcionamiento, crea el índice con mapeos de campo explícitos.
Ejecuta esto en Dev Tools → Console dentro de Kibana, o envíalo mediante curl desde tu terminal:
PUT /webflow-content
{
"mappings": {
"properties": {
"title": { "type": "text", "boost": 3 },
"slug": { "type": "keyword" },
"body": { "type": "text" },
"category": { "type": "keyword" },
"tags": { "type": "keyword" },
"published_at": { "type": "date" },
"webflow_item_id": { "type": "keyword" }
}
}
}
Siempre guardo webflow_item_id como un campo de nivel superior. Cuando reindexo contenido, uso el ID del elemento de Webflow como el _id del documento de Elasticsearch, lo que hace que la reindexación sea idempotente. Ejecutar el script dos veces no crea duplicados; simplemente sobrescribe el documento existente.
Las coincidencias en el título tienen un rango más alto que las del cuerpo porque la multi_match consulta en el Route Handler especifica title^3; un aumento de relevancia (boost) en tiempo de consulta que hace que una coincidencia en el título valga tres veces más que una en el cuerpo.
El aumento de relevancia en tiempo de consulta es el enfoque correcto en Elasticsearch 8.x; los aumentos de campo en tiempo de indexación se eliminaron en la versión 8.0 y arrojarán un MapperParsingException si se incluyen en un mapeo.
Punto de control: En Kibana Dev Tools, ejecuta GET /webflow-content y confirma que la respuesta muestra tu índice con la estructura de mapeo anterior.
2. Añade las credenciales de Elasticsearch a tu entorno de Webflow Cloud
Tu Cloud App necesita tres variables de entorno para comunicarse con Elastic Cloud.
Añádelas a .env.local para el desarrollo local:
# .env.local
ELASTIC_URL=https://your-cluster-id.es.us-east-1.aws.elastic-cloud.com
ELASTIC_API_KEY=your_encoded_api_key_from_elastic_cloud
ELASTIC_INDEX=webflow-content
En la configuración de tu sitio Webflow, abre Webflow Cloud, selecciona tu entorno y añade las mismas tres variables en Environment Variables.
ELASTIC_API_KEY tiene acceso de escritura a tu índice. Mantenla fuera del código del lado del cliente y de tu historial de git. Yo uso una clave de API de solo lectura independiente para el Route Handler de búsqueda y una clave con capacidad de escritura solo en el script de indexación que se ejecuta localmente. Dos claves significan que una clave de búsqueda filtrada no puede corromper tu índice.
Punto de control: Inicia tu servidor de desarrollo local (next dev para Next.js, astro dev para Astro) y confirma que process.env.ELASTIC_URL se resuelve en la URL de tu clúster. Despliega y confirma que la misma variable está disponible en el entorno de producción.
3. Crea el Route Handler de búsqueda de Elasticsearch
Crea app/api/search/route.ts en tu Cloud App. Este manejador recibe una cadena de consulta y filtros opcionales desde el frontend, construye una consulta de Elasticsearch y devuelve resultados estructurados con destacados y facetas de categoría.
Así es como se ve:
// app/api/search/route.ts
export const runtime = 'edge'
const ELASTIC_URL = process.env.ELASTIC_URL!
const ELASTIC_API_KEY = process.env.ELASTIC_API_KEY!
const INDEX = process.env.ELASTIC_INDEX ?? 'webflow-content'
interface ElasticHit {
_id: string
_score: number
_source: {
title: string
slug: string
body: string
category: string
tags: string[]
published_at: string
}
highlight?: { body?: string[] }
}
interface ElasticResponse {
hits: {
total: { value: number }
hits: ElasticHit[]
}
aggregations?: {
categories: { buckets: Array<{ key: string; doc_count: number }> }
}
}
export async function GET(request: Request) {
const url = new URL(request.url)
const q = url.searchParams.get('q')?.trim() ?? ''
const category = url.searchParams.get('category')
const page = Math.max(1, parseInt(url.searchParams.get('page') ?? '1'))
const size = 10
const from = (page - 1) * size
if (!q) {
return Response.json({ results: [], total: 0, facets: {}, page })
}
// Build the query — multi_match across title, body, and tags with typo tolerance
const must: unknown[] = [
{
multi_match: {
query: q,
fields: ['title^3', 'body', 'tags'],
fuzziness: 'AUTO',
minimum_should_match: '70%',
},
},
]
// Apply optional category filter
if (category) {
must.push({ term: { category } })
}
const esQuery = {
query: { bool: { must } },
highlight: {
fields: {
body: { fragment_size: 150, number_of_fragments: 1 },
},
pre_tags: ['<mark>'],
post_tags: ['</mark>'],
},
aggs: {
categories: { terms: { field: 'category', size: 20 } },
},
size,
from,
}
const esRes = await fetch(`${ELASTIC_URL}/${INDEX}/_search`, {
method: 'POST',
headers: {
Authorization: `ApiKey ${ELASTIC_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(esQuery),
})
if (!esRes.ok) {
const error = await esRes.text()
console.error('Elasticsearch error:', error)
return Response.json({ error: 'Search unavailable' }, { status: 502 })
}
const data = (await esRes.json()) as ElasticResponse
const results = data.hits.hits.map((hit) => ({
id: hit._id,
score: hit._score,
...hit._source,
highlight: hit.highlight?.body?.[0] ?? null,
}))
return Response.json({
results,
total: data.hits.total.value,
facets: {
categories: data.aggregations?.categories?.buckets ?? [],
},
page,
})
}
Configuré minimum_should_match: '70%' porque las coincidencias fuzziness: AUTO puras son demasiado amplias en consultas cortas. Una consulta de dos caracteres con la opción AUTO fuzziness coincidirá con casi todo en el índice. 70% significa que al menos el 70% de los términos de búsqueda deben aparecer en un documento, lo que arroja resultados mucho más útiles para búsquedas reales.
El bloque highlight devuelve un extracto de 150 caracteres del cuerpo con el término coincidente envuelto en etiquetas <mark> . Siempre lo incluyo. Mostrar a los usuarios dónde apareció su término de búsqueda en el contenido es la diferencia entre un resultado de búsqueda útil y una lista de títulos.
Punto de control: Inicia el servidor de desarrollo y ejecuta:
curl "http://localhost:3000/app/api/search?q=your-search-term"
Deberías obtener una respuesta JSON con los campos results, total, facets, y page . Los resultados estarán vacíos en esta etapa ya que el índice aún no tiene contenido. El paso 4 soluciona eso.
4. Indexa tu contenido de Webflow CMS en Elasticsearch
Ejecuto un script local para indexar contenido del CMS de forma masiva. Crea scripts/index-webflow-content.ts en la raíz de tu proyecto y ejecútalo desde la línea de comandos cuando necesites actualizar el índice:
// scripts/index-webflow-content.ts
// Run with: npx tsx scripts/index-webflow-content.ts
const WEBFLOW_TOKEN = process.env.WEBFLOW_API_TOKEN!
const COLLECTION_ID = process.env.WEBFLOW_COLLECTION_ID!
const ELASTIC_URL = process.env.ELASTIC_URL!
const ELASTIC_WRITE_KEY = process.env.ELASTIC_WRITE_API_KEY!
const INDEX = process.env.ELASTIC_INDEX ?? 'webflow-content'
interface WebflowItem {
id: string
lastPublished: string
fieldData: {
name: string
slug: string
'post-body'?: string
category?: string
tags?: string[]
}
}
async function fetchAllItems(): Promise<WebflowItem[]> {
const items: WebflowItem[] = []
let offset = 0
while (true) {
const res = await fetch(
`https://api.webflow.com/v2/collections/${COLLECTION_ID}/items?offset=${offset}&limit=100`,
{
headers: {
Authorization: `Bearer ${WEBFLOW_TOKEN}`,
},
}
)
const data = await res.json()
items.push(...data.items)
if (items.length >= data.pagination.total) break
offset += 100
}
return items
}
async function bulkIndex(items: WebflowItem[]): Promise<void> {
// Elasticsearch bulk API requires alternating action + document lines
const ndjson = items
.flatMap((item) => [
JSON.stringify({ index: { _index: INDEX, _id: item.id } }),
JSON.stringify({
title: item.fieldData.name,
slug: item.fieldData.slug,
body: item.fieldData['post-body'] ?? '',
category: item.fieldData.category ?? '',
tags: item.fieldData.tags ?? [],
published_at: item.lastPublished,
webflow_item_id: item.id,
}),
])
.join('\n') + '\n'
const res = await fetch(`${ELASTIC_URL}/_bulk`, {
method: 'POST',
headers: {
Authorization: `ApiKey ${ELASTIC_WRITE_KEY}`,
'Content-Type': 'application/x-ndjson',
},
body: ndjson,
})
const result = await res.json()
const indexed = result.items.length
const errors = result.items.filter((i: any) => i.index?.error).length
console.log(`Indexed ${indexed} items. Errors: ${errors}`)
if (errors > 0) {
console.error('First error:', result.items.find((i: any) => i.index?.error)?.index?.error)
}
}
const items = await fetchAllItems()
console.log(`Fetched ${items.length} items from Webflow`)
await bulkIndex(items)
La API masiva requiere Content-Type: application/x-ndjson (JSON delimitado por saltos de línea), no application/json estándar. Cada solicitud alterna entre una línea de acción ({ index: { _index, _id } }) y una línea de documento fuente. Si falta el salto de línea al final del cuerpo, se producirá un error de análisis silencioso. El + '\n' al final de la unión es obligatorio.
Mantengo un ELASTIC_WRITE_API_KEY independiente en .env.local para este script. Nunca se incluye en el entorno de Webflow Cloud. El controlador de búsqueda desplegado utiliza una clave de solo lectura.
Punto de control: Ejecuta el script y vuelve a consultar el controlador de ruta de búsqueda. Esta vez, los resultados deberían aparecer.
Paso 5: Conectar el formulario de búsqueda del frontend de Webflow
Añade esto a un bloque de Código personalizado (Código del pie de página: Antes de </body>) en cualquier página de Webflow que tenga un formulario de búsqueda. El formulario necesita un campo de entrada de texto con el atributo data-search-input y un contenedor de resultados con el data-search-results attribute.
(function () {
const input = document.querySelector('[data-search-input]')
const results = document.querySelector('[data-search-results]')
const facetsEl = document.querySelector('[data-search-facets]')
if (!input || !results) return
let debounceTimer
input.addEventListener('input', function () {
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => search(this.value.trim()), 300)
})
async function search(q, category = '') {
if (!q) {
results.innerHTML = ''
return
}
const params = new URLSearchParams({ q })
if (category) params.set('category', category)
const res = await fetch(`/app/api/search?${params}`)
const data = await res.json()
renderResults(data.results, data.total)
renderFacets(data.facets.categories ?? [])
}
function renderResults(items, total) {
if (!items.length) {
results.innerHTML = '<p>No results found.</p>'
return
}
results.innerHTML = `
<p>${total} result${total !== 1 ? 's' : ''}</p>
${items.map(item => `
<div class="search-result">
<a href="/${item.slug}"><strong>${item.title}</strong></a>
${item.highlight ? `<p>${item.highlight}</p>` : ''}
${item.category ? `<span class="category">${item.category}</span>` : ''}
</div>
`).join('')}
`
}
function renderFacets(categories) {
if (!facetsEl || !categories.length) return
facetsEl.innerHTML = categories.map(cat => `
<button data-category="${cat.key}" class="facet-btn">
${cat.key} (${cat.doc_count})
</button>
`).join('')
facetsEl.querySelectorAll('.facet-btn').forEach(btn => {
btn.addEventListener('click', function () {
search(input.value.trim(), this.dataset.category)
})
})
}
})()
El debounce de 300ms en el evento de entrada significa que la búsqueda se dispara cuando el usuario deja de escribir, no con cada pulsación de tecla. Siempre lo añado. Sin esto, una búsqueda de siete caracteres genera siete llamadas a la API y los resultados parpadean a medida que cada respuesta llega en momentos diferentes.
Punto de control: Publica tu sitio de Webflow, escribe en el campo de búsqueda y confirma que los resultados aparecen dentro del intervalo de debounce.
¿Qué causa que las búsquedas de Elasticsearch fallen en Webflow Cloud?
La mayoría de los fallos se dividen en tres categorías: importar el cliente de Node.js cuando no debería, el bloqueo de CORS en la petición del frontend y discrepancias en el mapeo del índice entre el script de indexación y la consulta.
El cliente @elastic/elasticsearch falla al importar
Si ves "Cannot find module 'net'" o "TypeError: net.createConnection is not a function", significa que tienes instalado el cliente oficial de Node.js. Elimínalo con npm uninstall @elastic/elasticsearch y utiliza el enfoque basado en fetch() del Paso 3.
El problema de GitHub que rastrea el soporte para Workers (elastic/elasticsearch-js #2032) no tiene fecha de resolución. La API HTTP cubre todas las operaciones de búsqueda e indexación que necesitas.
La búsqueda devuelve resultados, pero faltan los resaltados
Los resaltados requieren que los campos se almacenen como text en el mapeo del índice. Si creaste el índice antes de configurar el mapeo, es posible que Elasticsearch haya detectado automáticamente tu campo de cuerpo como keyword (que no admite operaciones de texto completo).
Ejecuta GET /webflow-content/_mapping en Kibana Dev Tools y verifica que body y title muestren "type": "text". Si no es así, elimina el índice (DELETE /webflow-content), vuelve a crearlo con el mapeo del Paso 1 y ejecuta de nuevo el script de indexación.
El script de indexación masiva se ejecuta pero no produce resultados
La causa más común es una discrepancia entre el nombre del campo en el script de indexación y el slug del campo del CMS de Webflow. Webflow genera slugs de campo a partir del nombre del campo. Un campo llamado "Post Body" usa el slug "post-body", no "body". Registra item.fieldData para el primer elemento en fetchAllItems() para inspeccionar los nombres reales de los campos, luego actualiza el mapeo del campo del cuerpo en el script.
Qué construir después de que Elasticsearch esté funcionando en tu Webflow Cloud App
Con un endpoint de búsqueda funcional, lo siguiente que añado es la reindexación automática. Webflow envía un webhook al publicar un elemento. Conéctalo a un controlador de ruta que llame al endpoint de indexación de un solo documento de Elasticsearch (PUT /{index}/_doc/{id}) para actualizar el elemento. De esta forma, el índice se mantiene actualizado sin tener que ejecutar el script masivo manualmente después de cada actualización de contenido.
Explora Webflow + Elasticsearch para obtener una visión general completa de la integración, incluyendo opciones de autenticación y notas sobre la compatibilidad de la biblioteca cliente.
Preguntas frecuentes
¿Puedo usar Elasticsearch para realizar búsquedas en todo el sitio a través de múltiples colecciones de Webflow CMS?
Sí. Indexa cada colección en el mismo índice de Elasticsearch con un collection_type campo (por ejemplo, "blog-post", "product", "help-article"). Añade collection_type al mapeo del índice como un campo keyword, y luego añádelo como una faceta en la consulta de agregación. El gestor de rutas de búsqueda devuelve una lista de resultados que abarca todos los tipos de contenido, con facetas para filtrar por tipo.
¿Cómo gestiono la paginación de Webflow CMS al indexar bibliotecas de contenido extensas?
La fetchAllItems() función del paso 4 se encarga de esto con un bucle de desplazamiento. La API de datos de Webflow devuelve hasta 100 elementos por solicitud e incluye pagination.total en la respuesta. El bucle continúa hasta items.length >= pagination.total. Para colecciones con varios miles de elementos, esto puede llevar un minuto. Ejecuta el script en segundo plano y monitorízalo con la salida console.log.
¿La versión gratuita de Elasticsearch admite todo lo que se describe en esta guía?
Sí. La prueba gratuita de 14 días de Elastic Cloud incluye toda la funcionalidad de Elasticsearch: consultas multi-match, agregaciones, resaltados y la API de indexación masiva están disponibles. Tras la prueba, el plan de pago más económico es suficiente para búsquedas en producción en un sitio Webflow típico. Consulta los precios de Elastic Cloud para ver los detalles actuales de los planes.
¿Debería usar Elasticsearch o Algolia para las búsquedas en Webflow?
Elijo Elasticsearch cuando los clientes necesitan control total sobre el ajuste de relevancia, puntuación personalizada o estructuras de consulta complejas, o cuando ya utilizan Elastic Stack para otros fines. Opto por Algolia cuando la prioridad es un tiempo de configuración mínimo y un motor de relevancia gestionado listo para usar. Elasticsearch ofrece más potencia a cambio de más configuración. Ambos se integran de la misma forma con Webflow Cloud: un gestor de rutas procesa las consultas y devuelve JSON.




