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 🙂 |
