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)
escapeHtml(item.excerpt)
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 = 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 🙂 |
