Como crear endpoints para favoritos de usuario y UI en JS en WordPress

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/(?Pd ) 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

  1. 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.
  2. 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.
  3. Caching: Cachea resultados cuando sea posible y usa invalidación al añadir/eliminar favoritos.
  4. Rate limiting: Protege endpoints de abuso (limitar peticiones por usuario/IP si fuera necesario).
  5. 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

  1. Crear endpoints con register_rest_route y permission_callback que verifique login.
  2. Implementar handlers que validen input, consulten y actualicen user meta (o tabla propia).
  3. Encolar script y pasar rest_url() wp_create_nonce(wp_rest) via wp_localize_script.
  4. Implementar frontend en JS con fetch, enviando encabezado X-WP-Nonce y credentials: same-origin.
  5. Agregar estados y accesibilidad en la UI (aria-pressed, deshabilitar mientras se procesa).
  6. 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 🙂



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *