Contents
Introducción
Este tutorial explica, paso a paso y con todo lujo de detalles, cómo crear endpoints en WordPress para gestionar favoritos por usuario y cómo construir una interfaz de usuario en JavaScript que consuma esos endpoints. Cubriremos la parte backend (REST API en PHP), la seguridad y validación, el almacenamiento (user meta vs tabla propia), el código JavaScript para la UI y buenas prácticas de accesibilidad y rendimiento.
Visión general de la solución
- Endpoints REST: GET para listar favoritos, POST para añadir, DELETE para eliminar.
- Almacenamiento simple: user meta con un array de IDs de post, suficiente para proyectos pequeños/medianos.
- Seguridad: permiso de usuario (is_user_logged_in / current_user_can) y saneamiento de entrada.
- Frontend: botones de favorito que usan fetch contra la REST API, con manejo de nonce para autenticación por cookie.
API: Endpoints y contrato
Ruta | Método | Descripción |
/wp-json/myplugin/v1/favorites | GET | Devuelve los IDs de los posts favoritos del usuario actual. |
/wp-json/myplugin/v1/favorites | POST | Añade un post a favoritos. Cuerpo JSON: { post_id: 123 } |
/wp-json/myplugin/v1/favorites/(?P |
DELETE | Elimina el post indicado de favoritos. |
Decisiones de diseño
- Usar user meta: get_user_meta/update_user_meta con clave user_favorites. Guardaremos un array de enteros. Es fácil y funciona sin esquema adicional.
- Para cargas altas o requisitos de consulta complejos (por ejemplo, consulta inversa: ¿qué usuarios marcaron este post?), considerar tabla propia con índices o post meta indexado.
- Usar permisos basados en login. No se exponen endpoints a usuarios anónimos.
Backend: registrar endpoints y handlers (PHP)
A continuación un ejemplo completo que puedes añadir como plugin simple o en el functions.php del tema (recomendado: plugin propio).
lt?php / Plugin Name: Favoritos de Usuario - REST API Description: Endpoints para gestionar favoritos por usuario. Version: 1.0 Author: Ejemplo / add_action(rest_api_init, function () { namespace = myplugin/v1 register_rest_route(namespace, /favorites, array( methods => GET, callback => mp_get_user_favorites, permission_callback => function() { return is_user_logged_in() } )) register_rest_route(namespace, /favorites, array( methods => POST, callback => mp_add_user_favorite, permission_callback => function() { return is_user_logged_in() } )) register_rest_route(namespace, /favorites/(?Pltpost_idgtd ), array( methods => DELETE, callback => mp_remove_user_favorite, permission_callback => function() { return is_user_logged_in() } )) }) / Obtiene los favoritos del usuario actual. / function mp_get_user_favorites( WP_REST_Request request ) { user_id = get_current_user_id() favorites = get_user_meta( user_id, user_favorites, true ) if ( empty(favorites) !is_array(favorites) ) { favorites = array() } return rest_ensure_response( array_values( array_map( absint, favorites ) ) ) } / Añade un favorito. Espera JSON: { post_id: 123 } / function mp_add_user_favorite( WP_REST_Request request ) { params = request->get_json_params() if ( ! isset( params[post_id] ) ) { return new WP_Error( missing_post_id, Falta post_id, array( status => 400 ) ) } post_id = absint( params[post_id] ) if ( post_id <= 0 get_post_status( post_id ) === false ) { return new WP_Error( invalid_post, post_id no válido, array( status => 400 ) ) } user_id = get_current_user_id() favorites = get_user_meta( user_id, user_favorites, true ) if ( ! is_array( favorites ) ) { favorites = array() } if ( in_array( post_id, favorites, true ) ) { return rest_ensure_response( array( message => ya estaba en favoritos, favorites => favorites ) ) } favorites[] = post_id favorites = array_values( array_unique( array_map( absint, favorites ) ) ) update_user_meta( user_id, user_favorites, favorites ) return rest_ensure_response( array( message => añadido, favorites => favorites ) ) } / Elimina un favorito por ID de post. / function mp_remove_user_favorite( WP_REST_Request request ) { post_id = absint( request->get_param( post_id ) ) if ( post_id <= 0 ) { return new WP_Error( invalid_post, post_id no válido, array( status => 400 ) ) } user_id = get_current_user_id() favorites = get_user_meta( user_id, user_favorites, true ) if ( ! is_array( favorites ) ) { favorites = array() } new = array_values( array_diff( favorites, array( post_id ) ) ) update_user_meta( user_id, user_favorites, new ) return rest_ensure_response( array( message => eliminado, favorites => new ) ) }
Explicaciones clave del código PHP
- register_rest_route usa permission_callback para rechazar accesos anónimos. Aquí se exige is_user_logged_in().
- Saneamiento: absint y comprobación de existencia de post con get_post_status.
- Persistencia simple: update_user_meta con un array único y ordenado.
- Respuestas: rest_ensure_response para devolver JSON consistente WP_Error para errores con códigos HTTP.
Encolar scripts y pasar datos al frontend
Para que el JavaScript tenga la URL raíz de la API y el nonce (token para autenticación por cookies), encola el script y usa wp_create_nonce(wp_rest).
add_action(wp_enqueue_scripts, function() { wp_enqueue_script(favorites-js, plugin_dir_url(__FILE__) . js/favorites.js, array(), 1.0, true) wp_localize_script(favorites-js, favsSettings, array( root => esc_url_raw( rest_url() ), nonce => wp_create_nonce( wp_rest ) )) })
Frontend: UI en JavaScript
Ejemplo de archivo js/favorites.js. El código asume que en el HTML existen botones con clase favorite-button y atributo data-post-id. El script hace delegación de eventos, consulta la API y actualiza la UI.
document.addEventListener(DOMContentLoaded, function() { const root = favsSettings favsSettings.root ? favsSettings.root : / const nonce = favsSettings favsSettings.nonce ? favsSettings.nonce : function getFavorites() { return fetch(root myplugin/v1/favorites, { credentials: same-origin, headers: { X-WP-Nonce: nonce } }).then(resp =gt resp.json()) } async function toggleFavorite(postId, button) { const isFav = button.classList.contains(is-favorite) try { if (!isFav) { const res = await fetch(root myplugin/v1/favorites, { method: POST, credentials: same-origin, headers: { Content-Type: application/json, X-WP-Nonce: nonce }, body: JSON.stringify({ post_id: postId }) }) const data = await res.json() if (res.ok) { button.classList.add(is-favorite) button.setAttribute(aria-pressed, true) } else { console.error(Error adding favorite, data) } } else { const res = await fetch(root myplugin/v1/favorites/ postId, { method: DELETE, credentials: same-origin, headers: { X-WP-Nonce: nonce } }) const data = await res.json() if (res.ok) { button.classList.remove(is-favorite) button.setAttribute(aria-pressed, false) } else { console.error(Error removing favorite, data) } } } catch (err) { console.error(Network or parsing error, err) } } // Inicializa botones con estado actual getFavorites().then(list => { document.querySelectorAll(.favorite-button[data-post-id]).forEach(btn =gt { const pid = parseInt(btn.getAttribute(data-post-id), 10) if (Array.isArray(list) list.indexOf(pid) !== -1) { btn.classList.add(is-favorite) btn.setAttribute(aria-pressed, true) } else { btn.classList.remove(is-favorite) btn.setAttribute(aria-pressed, false) } }) }) // Delegación de eventos: manejador único document.body.addEventListener(click, function(e) { const btn = e.target.closest(.favorite-button[data-post-id]) if (!btn) return e.preventDefault() const postId = parseInt(btn.getAttribute(data-post-id), 10) if (!postId) return toggleFavorite(postId, btn) }, false) })
Pautas de accesibilidad y UX
- Usar aria-pressed en botones para indicar estado binario.
- Añadir texto claro dentro del botón: Favorito / Quitar favorito y usar iconos decorativos con aria-hidden si es necesario.
- Deshabilitar el botón mientras se produce la petición para evitar doble envío y mostrar un pequeño spinner o cambio de cursor.
Estilos CSS sugeridos
.favorite-button{ background: transparent border: 1px solid #ddd padding: .35rem .6rem border-radius: 4px cursor: pointer transition: background .15s, color .15s } .favorite-button.is-favorite{ background: #ffeb3b border-color: #f1c40f } .favorite-button[disabled]{ opacity: .6 cursor: not-allowed }
Buenas prácticas y consideraciones avanzadas
- Escalabilidad: Si esperas millones de relaciones usuario-post, considera una tabla personalizada con índices y consultas optimizadas. User meta está bien para volúmenes moderados.
- Consistencia: Evitar condiciones de carrera cuando múltiples solicitudes modifican favoritos simultáneamente. Puedes aplicar lock lógico en PHP (por ejemplo, obtener y comparar antes de update) o usar transacciones en una tabla propia.
- Caching: Cachea resultados cuando sea posible y usa invalidación al añadir/eliminar favoritos.
- Rate limiting: Protege endpoints de abuso (limitar peticiones por usuario/IP si fuera necesario).
- Privacidad: Decide si los favoritos son privados por defecto (user meta es privado) o públicos. Si públicos, diseña endpoints que no expongan información sensible.
Alternativas de almacenamiento
- User meta (actual): Simple, fácil de usar. Inconveniente: no indexado para consultas inversas eficientes.
- Post meta: Podrías almacenar conteo de favoritos por post en post meta para mostrar números rápidamente, pero no la lista completa de usuarios.
- Tabla personalizada: Recomendado para grandes sitios. Estructura típica: id, user_id, post_id, created_at. Añadir índices en user_id y post_id.
Migración a tabla propia (esquema ejemplo)
Si decides migrar a tabla propia, crea la tabla en activation hook y usa consultas preparadas para insertar/leer. Ejemplo de esquema:
CREATE TABLE wp_user_favorites ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL, post_id BIGINT UNSIGNED NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY user_id (user_id), KEY post_id (post_id), UNIQUE KEY user_post (user_id, post_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
Errores comunes y cómo evitarlos
- No validar post_id —gt siempre usar absint y verificar existencia del post.
- No proteger endpoints —gt verificar is_user_logged_in o capacidades concretas.
- No manejar respuestas en el frontend —gt comprobar res.ok y mostrar errores de forma amable.
- Actualizar meta sin normalizar —gt usar array_unique y array_values para mantener datos limpios.
Resumen y checklist de implementación
- Crear endpoints con register_rest_route y permission_callback que verifique login.
- Implementar handlers que validen input, consulten y actualicen user meta (o tabla propia).
- Encolar script y pasar rest_url() wp_create_nonce(wp_rest) via wp_localize_script.
- Implementar frontend en JS con fetch, enviando encabezado X-WP-Nonce y credentials: same-origin.
- Agregar estados y accesibilidad en la UI (aria-pressed, deshabilitar mientras se procesa).
- Revisar escalabilidad y, si procede, migrar a tabla personalizada con índices.
Ejemplos rápidos de uso en plantilla
Botón que puedes colocar dentro del loop de WordPress (ejemplo mínimo):
ltbutton class=favorite-button data-post-id=lt?php the_ID() ?gt aria-pressed=falsegt ltspan class=labelgtFavoritolt/spangt lt/buttongt
El script inicializará el estado según la lista de favoritos del usuario.
Conclusión
Este tutorial muestra un camino claro y práctico para implementar favoritos por usuario en WordPress usando la REST API y una UI en JavaScript. La solución presentada es segura, modular y suficiente para muchos casos reales. Para necesidades de escala y consultas avanzadas conviene evaluar migrar a una tabla propia con índices adecuados.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |