Contents
Introducción
Este artículo explica paso a paso cómo implementar una búsqueda instantánea en WordPress usando la REST API, fetch y un debounce para evitar llamadas excesivas. Cubriremos la creación del endpoint REST personalizado, el encolado del script, la implementación en JavaScript con cancelación de peticiones (AbortController) y debounce, un pequeño CSS para presentar resultados, y recomendaciones de rendimiento y seguridad. Todo pensado para integrarlo en tu theme o plugin sin dependencias externas.
Requisitos previos
- WordPress 4.7 (REST API integrada).
- Acceso para editar functions.php del theme o crear un plugin.
- Conocimientos básicos de PHP, JS y CSS.
Visión general del flujo
- Registrar un endpoint REST personalizado que reciba la consulta y devuelva resultados en JSON.
- Encolar e inicializar un script que use fetch para consultar ese endpoint.
- Aplicar un debounce al evento input para espaciar peticiones.
- Usar AbortController para cancelar peticiones anteriores si llega una nueva.
- Renderizar los resultados en un contenedor accesible y estilizado.
1) Endpoint REST en PHP (functions.php o plugin)
Registraremos una ruta REST en namespace instant-search/v1 y un endpoint /search. El callback realizará una consulta segura y devolverá un array de resultados con id, título, extracto, enlace y URL de imagen destacada si existe. También añadiremos un caché con transients por consultas para mejorar rendimiento en picos de tráfico.
// functions.php (o archivo principal del plugin) add_action(rest_api_init, function() { register_rest_route(instant-search/v1, /search, array( methods => GET, callback => is_search_endpoint_callback, args => array( s => array( required => true, sanitize_callback => sanitize_text_field, ), per_page => array( required => false, default => 6, sanitize_callback => absint, ), post_type => array( required => false, default => post, sanitize_callback => sanitize_text_field, ), ), permission_callback => __return_true, )) }) function is_search_endpoint_callback(WP_REST_Request request) { search = request->get_param(s) per_page = request->get_param(per_page) post_type = request->get_param(post_type) // Clave de caché por query cache_key = is_search_ . md5(search . . per_page . . post_type) cached = get_transient(cache_key) if (cached !== false) { return rest_ensure_response(cached) } args = array( s => search, post_type => post_type, post_status => publish, posts_per_page => per_page, no_found_rows => true, ) q = new WP_Query(args) results = array() if (q->have_posts()) { foreach (q->posts as post) { thumbnail = if (has_post_thumbnail(post->ID)) { src = wp_get_attachment_image_src(get_post_thumbnail_id(post->ID), thumbnail) if (!empty(src[0])) { thumbnail = esc_url_raw(src[0]) } } results[] = array( id => post->ID, title => wp_strip_all_tags(get_the_title(post)), excerpt => wp_strip_all_tags(get_the_excerpt(post)), link => get_permalink(post), thumbnail => thumbnail, ) } } // Guardar en caché por 60 segundos (ajustable) set_transient(cache_key, results, 60) return rest_ensure_response(results) }
Notas sobre seguridad y rendimiento
- El endpoint está públicamente accesible (permission_callback => __return_true). Si tu búsqueda requiere datos privados, cambia la comprobación y usa nonces/roles.
- Se usa set_transient para cachear resultados por consulta ajusta la duración según tu tráfico y frecuencia de cambios de contenido.
- La consulta está limitada con no_found_rows para reducir carga si no necesitas paginación.
2) Encolar el script y pasar configuración desde PHP
Encolaremos un JS que usará fetch y pasaremos la URL del endpoint y el nonce (si lo deseas) mediante una variable localizada.
// functions.php - encolar script add_action(wp_enqueue_scripts, function() { wp_enqueue_script( instant-search, get_template_directory_uri() . /js/instant-search.js, array(), 1.0, true ) wp_localize_script(instant-search, instantSearchSettings, array( rest_url => esc_url_raw(rest_url(instant-search/v1/search)), // nonce => wp_create_nonce(wp_rest), // opcional si ocupas cabeceras para autenticación )) })
3) HTML mínimo necesario en la plantilla
Coloca en la plantilla el campo de búsqueda y el contenedor donde se mostrarán los resultados. Importante usar atributos ARIA para accesibilidad.
4) JavaScript: fetch debounce AbortController
Este script implementa:
- Debounce para retrasar la petición hasta que el usuario deje de escribir (300ms por defecto).
- AbortController para cancelar peticiones previas y evitar race conditions.
- Renderizado sencillo de resultados con enlaces y miniaturas.
// /js/instant-search.js (function() { const input = document.getElementById(instant-search-input) const resultsContainer = document.getElementById(instant-search-results) const REST_URL = (window.instantSearchSettings window.instantSearchSettings.rest_url) ? window.instantSearchSettings.rest_url : /wp-json/instant-search/v1/search const DEBOUNCE_MS = 300 let debounceTimer = null let currentController = null function clearResults() { resultsContainer.innerHTML = resultsContainer.style.display = none } function renderResults(items) { if(!items items.length === 0) { resultsContainer.innerHTML =No se han encontrado resultados.
resultsContainer.style.display = block return } const list = document.createElement(ul) list.className = is-results-list items.forEach(item => { const li = document.createElement(li) li.className = is-result-item li.setAttribute(role, option) const a = document.createElement(a) a.href = item.link a.innerHTML = (item.thumbnail ?: )
escapeHtml(item.title)li.appendChild(a) list.appendChild(li) }) resultsContainer.innerHTML = resultsContainer.appendChild(list) resultsContainer.style.display = block } function escapeHtml(str) { if (!str) return return str.replace(/[<>]/g, function(m) { return ({:amp,<:lt,>:gt,:quot,:#039})[m] }) } function fetchResults(query) { if (currentController) { currentController.abort() // cancelar petición anterior } currentController = new AbortController() const signal = currentController.signal const url = new URL(REST_URL, window.location.origin) url.searchParams.set(s, query) url.searchParams.set(per_page, 8) // Opcional: si usas nonce const headers = {} if (window.instantSearchSettings window.instantSearchSettings.nonce) { headers[X-WP-Nonce] = window.instantSearchSettings.nonce } resultsContainer.innerHTML =escapeHtml(item.excerpt)
Buscando...
resultsContainer.style.display = block fetch(url.toString(), { method: GET, headers: headers, signal: signal, credentials: same-origin }) .then(response => { if (!response.ok) throw new Error(Network response was not ok) return response.json() }) .then(data => { renderResults(data) }) .catch(err => { if (err.name === AbortError) return // petición cancelada, silencioso resultsContainer.innerHTML =Error al buscar. Inténtalo más tarde.
}) } function onInput(e) { const value = e.target.value.trim() if (debounceTimer) clearTimeout(debounceTimer) if (!value) { clearResults() return } debounceTimer = setTimeout(function() { fetchResults(value) }, DEBOUNCE_MS) } // Listener if (input) { input.addEventListener(input, onInput) input.addEventListener(blur, function() { // Opcional: cerrar resultados al perder foco después de un pequeño retraso setTimeout(clearResults, 200) }) } })()
Explicación de puntos clave en el JS
- Debounce: evita lanzar una petición por cada pulsación. Ajusta DEBOUNCE_MS según la latencia tolerable y la velocidad de tecleo deseada.
- AbortController: previene condiciones de carrera y evita renderizar resultados de peticiones antiguas cuando ya hay nuevas.
- escapeHtml: evita XSS al inyectar títulos/extractos desde la API.
5) CSS básico para los resultados
Estilos simples para mostrar resultados en forma de lista tipo dropdown. Ajusta según tu theme.
/ instant-search.css / .instant-search-form { position: relative max-width: 600px } #instant-search-results { position: absolute top: 100% left: 0 right: 0 background: #fff border: 1px solid #ddd z-index: 50 display: none box-shadow: 0 4px 8px rgba(0,0,0,0.05) } .is-results-list { list-style: none margin: 0 padding: 0 } .is-result-item { border-bottom: 1px solid #f0f0f0 } .is-result-item a { display: flex align-items: center padding: 8px 10px text-decoration: none color: inherit } .is-thumb { width: 56px height: 56px object-fit: cover margin-right: 10px border-radius: 4px } .is-text { flex: 1 } .is-excerpt { margin: 4px 0 0 color: #666 font-size: 13px } .is-loading, .is-no-results, .is-error { padding: 10px margin: 0 color: #666 }
6) Accesibilidad y experiencia de teclado
- Usa role=listbox y role=option en el contenedor y elementos para que lectores de pantalla identifiquen la lista dinámica.
- Considera implementar navegación con flechas (arriba/abajo) y selección con Enter para una experiencia completa con teclado. La implementación básica presentada debe extenderse para manejar focus y aria-activedescendant si lo necesitas.
Ejemplo breve de manejo de teclado (opcional)
// Fragmento para extender la navegación por teclado (añadir a instant-search.js) // Este fragmento asume que renderResults crea una lista UL con .is-result-item let focusedIndex = -1 input.addEventListener(keydown, function(e) { const items = document.querySelectorAll(.is-result-item) if (!items.length) return if (e.key === ArrowDown) { e.preventDefault() focusedIndex = Math.min(focusedIndex 1, items.length - 1) items[focusedIndex].querySelector(a).focus() } else if (e.key === ArrowUp) { e.preventDefault() focusedIndex = Math.max(focusedIndex - 1, 0) items[focusedIndex].querySelector(a).focus() } else if (e.key === Escape) { clearResults() input.focus() } })
7) Recomendaciones y mejoras avanzadas
- Integración con ElasticPress o Algolia para sitios con mucho contenido o necesidades de relevancia personalizada.
- Indexado en caché y pre-generación de sugerencias (search indexing) para respuestas más rápidas.
- Agregar ranking por relevancia (meta, taxonomías, búsqueda por título ponderado) en el callback PHP si se desea mejorar precisión.
- Agregar lazy-loading o placeholders para miniaturas si la carga de imágenes afecta el rendimiento.
- Limitar el número de resultados y añadir paginación/infinite scroll para búsquedas más exhaustivas.
Conclusión
Esta solución proporciona una búsqueda instantánea ligera y controlada usando la REST API de WordPress, fetch, debounce y AbortController. Es fácil de integrar y ampliar: desde mejoras de accesibilidad y navegación por teclado hasta optimizaciones con caché y motores de búsqueda externos. Implementa las partes que necesites y ajusta parámetros (debounce, per_page, caché) según el comportamiento real de tus usuarios y la carga de tu servidor.
Recursos útiles
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |