Contents
Introducción
Este artículo explica con todo lujo de detalles cómo implementar sugerencias de enlaces internos en el editor de bloques de WordPress usando la REST API y JavaScript. La técnica se compone de dos capas principales:
- Servidor (PHP): registrar un endpoint REST seguro que busque posts/pages y devuelva resultados limpios y paginados.
- Cliente (JS en el editor de bloques): llamar al endpoint con debounce, mostrar sugerencias y permitir al usuario insertar el enlace (por ejemplo, generando un bloque párrafo con el enlace o insertando el link en el contenido seleccionado).
Estrategia general
1) Crear un endpoint REST personalizado (por ejemplo /wp-json/internal-links/v1/search) que acepte parámetros como search, post_type, exclude, per_page. El endpoint debe validar permisos (capacidad de editar posts o al menos leer privados si corresponde), sanitizar la entrada y limitar el número de resultados para evitar sobrecarga.
2) Encolar un script para el editor de bloques que reciba datos localizados (rest_url, nonce, postId) y que utilice apiFetch para pedir sugerencias al endpoint. Implementar debounce, caching en memoria y manejo de errores.
3) Presentar las sugerencias al usuario con una UI simple (lista desplegable o componente de sugerencias) y, al seleccionar, insertar un enlace en el editor (por ejemplo creando un bloque párrafo con el anchor o modificando la selección actual).
Ventajas
- Control total sobre criterios de búsqueda y datos devueltos (por ejemplo: incluir excerpt, featured image, taxonomías).
- Mayor rendimiento posible con limitación y cache server-side/client-side.
- Seguridad: permisos y nonce protegen la operación.
Implementación servidor (PHP)
A continuación un ejemplo de cómo registrar el endpoint REST y devolver resultados pertinentes. El código incluye sanitización básica, permiso y una respuesta JSON con id, title, url, excerpt y post_type.
GET, callback => il_search_internal_links, permission_callback => function() { // Permitimos la búsqueda a usuarios con capacidad de read (público) o ajustar según necesidad: return current_user_can( read ) }, args => array( search => array( required => false, ), post_type => array( required => false, default => post, ), exclude => array( required => false, ), per_page => array( required => false, default => 10, ), ), ) ) } ) / Callback que realiza la búsqueda. @param WP_REST_Request request @return WP_REST_Response / function il_search_internal_links( request ) { search = sanitize_text_field( request->get_param( search ) ) post_type = sanitize_text_field( request->get_param( post_type ) ) exclude = request->get_param( exclude ) // puede ser ID o array per_page = intval( request->get_param( per_page ) ) if ( per_page <= 0 per_page > 50 ) { per_page = 10 } args = array( s => search, post_type => post_type, posts_per_page => per_page, post_status => array( publish ), fields => ids, no_found_rows => true, ) if ( ! empty( exclude ) ) { if ( is_array( exclude ) ) { args[post__not_in] = array_map( intval, exclude ) } else { args[post__not_in] = array( intval( exclude ) ) } } query = new WP_Query( args ) ids = query->posts results = array() foreach ( ids as id ) { title = get_the_title( id ) permalink = get_permalink( id ) results[] = array( id => (int) id, title => wp_strip_all_tags( title ), url => esc_url_raw( permalink ), excerpt => wp_trim_words( wp_strip_all_tags( get_the_excerpt( id ) ), 20, ... ), post_type => get_post_type( id ), ) } return rest_ensure_response( results ) } ?>
Encolar el script del editor y pasar datos localizados
En el lado del servidor también debemos encolar el script que se usará en la interfaz del editor y pasar valores útiles: rest_url, nonce y el ID del post actual para poder excluirlo en las búsquedas.
esc_url_raw( rest_url( internal-links/v1/search ) ), nonce => wp_create_nonce( wp_rest ), postId => get_the_ID() ?: 0, ) ) } ) ?>
Implementación cliente (JS) en el editor de bloques
A continuación un componente mínimo que se puede añadir como un plugin de editor (PluginSidebar o control en Inspector). Utiliza apiFetch y muestra sugerencias en una lista. Incluye debounce, cache básico y manejo de errores. Al seleccionar una sugerencia crea un bloque párrafo con el enlace.
/ File: src/editor-internal-links.js Requiere: @wordpress/element, @wordpress/components, @wordpress/api-fetch, @wordpress/data, @wordpress/blocks / const { useState, useEffect, useRef } = wp.element const { TextControl, Spinner, Button } = wp.components const apiFetch = wp.apiFetch const { dispatch } = wp.data const { createBlock } = wp.blocks function debounce(fn, delay = 300) { let t return function(...args) { clearTimeout(t) t = setTimeout(() => fn.apply(this, args), delay) } } function useCache() { const cache = useRef(new Map()) const get = (key) => cache.current.get(key) const set = (key, value) => cache.current.set(key, value) return { get, set } } function InternalLinkSuggester() { const [term, setTerm] = useState() const [loading, setLoading] = useState(false) const [results, setResults] = useState([]) const [error, setError] = useState(null) const cache = useCache() // Debounced fetch const doSearch = useRef( debounce(async (q) => { if (!q) { setResults([]) setLoading(false) return } const cached = cache.get(q) if (cached) { setResults(cached) setLoading(false) return } setLoading(true) setError(null) try { const response = await apiFetch({ path: ILInternalLinks.restUrl ?search= encodeURIComponent(q) per_page=10exclude= encodeURIComponent(ILInternalLinks.postId 0), headers: { X-WP-Nonce: ILInternalLinks.nonce, } }) // Esperamos un array de objetos {id, title, url, excerpt, post_type} cache.set(q, response) setResults(response) } catch (err) { console.error(Error buscando enlaces internos, err) setError(Error al buscar) } finally { setLoading(false) } }, 300) ).current useEffect(() => { setLoading(true) doSearch(term) }, [term]) function insertLinkAsParagraph(item) { const html = {item.title} const block = createBlock(core/paragraph, { content: html }) dispatch(core/block-editor).insertBlocks(block) // opcional: limpiar búsqueda setTerm() setResults([]) } return ( wp.element.createElement(div, { className: il-internal-link-suggester }, wp.element.createElement(TextControl, { label: Buscar enlace interno, value: term, onChange: (v) => setTerm(v), placeholder: Escribe título, palabra clave o slug..., }), loading wp.element.createElement(div, null, wp.element.createElement(Spinner)), error wp.element.createElement(div, { style: { color: red } }, error), wp.element.createElement(ul, { style: { listStyle: none, paddingLeft: 0 } }, results.map((r) => wp.element.createElement(li, { key: r.id, style: { marginBottom: 8px } }, wp.element.createElement(div, null, wp.element.createElement(b, null, r.title), wp.element.createElement(div, { style: { fontSize: 13px, color: #666 } }, r.excerpt) ), wp.element.createElement(Button, { isSecondary: true, onClick: () => insertLinkAsParagraph(r), style: { marginTop: 6px } }, Insertar enlace) ) ) ) ) ) } // Registro como plugin del editor (simplificado) const { registerPlugin } = wp.plugins const { PluginSidebar, PluginSidebarMoreMenuItem } = wp.editPost function ILPlugin() { return wp.element.createElement( wp.element.Fragment, null, wp.element.createElement(PluginSidebarMoreMenuItem, null, Enlaces internos), wp.element.createElement(PluginSidebar, { name: il-internal-links, title: Insertar enlace interno }, wp.element.createElement(InternalLinkSuggester, null) ) ) } registerPlugin(il-internal-links-plugin, { render: ILPlugin })
Detalles y buenas prácticas
1) Seguridad y permisos
- El endpoint debe tener una permission_callback que devuelva false si el usuario no puede realizar la acción. En muchos casos read es suficiente, pero si tu búsqueda debe mostrar posts privados, requiere capacidades más elevadas.
- Usa nonce (wp_create_nonce( wp_rest )) y pásalo en la cabecera X-WP-Nonce o usa la configuración global wpApiSettings.
2) Sanitización y límites
- Sanitiza la entrada con sanitize_text_field y convierte parámetros numéricos con intval.
- Limita per_page y evita consultas con posts_per_page masivos. Añade no_found_rows => true para acelerar las consultas cuando no necesitas paginación total.
3) Relevancia y rendimiento
- WP_Query con s hace búsquedas básicas. Para relevancia o búsquedas complejas considera integrar ElasticPress o WP Search con motores más potentes.
- En el servidor puedes preparar columnas de búsqueda (metadatos) y/o usar LIKE personalizado para mejorar resultados otra opción es usar el endpoint core /wp/v2/search que ya devuelve resultados de búsqueda simplificados.
4) Cache en cliente y servidor
- Cache en cliente (memoria) por término evita llamadas redundantes durante la escritura.
- Si la base de datos es grande, cachea resultados en transients por término en el servidor con expiración corta.
5) UX y accesibilidad
- Proporciona feedback claro (spinner, mensaje de error, sin resultados).
- Permite navegar y seleccionar con teclado (mejorar la lista para manejo con arrow keys/enter).
Formato de respuesta recomendado
Diseña la respuesta del endpoint como array de objetos con claves previsibles. Ejemplo:
[ { id: 123, title: Mi artículo interesante, url: https://ejemplo.com/mi-articulo-interesante, excerpt: Resumen breve del artículo..., post_type: post }, ... ]
Tabla de parámetros del endpoint
Parámetro | Tipo | Descripción | Valor por defecto |
---|---|---|---|
search | string | Texto a buscar en título/excerpt/contenido | |
post_type | string array | Tipo(s) de post a incluir | post |
exclude | int array | ID(s) de post a excluir (ej. post actual) | 0 |
per_page | int | Número máximo de resultados | 10 |
Extensiones y mejoras posibles
- Integrar imágenes destacadas en la respuesta y mostrarlas en la UI de sugerencias.
- Priorizar resultados por taxonomías compartidas con el post actual (relevancia contextual).
- Permitir la inserción directa en el bloque seleccionado usando RichText.applyFormat para convertir texto seleccionado en enlace en lugar de crear un párrafo nuevo.
- Soporte para múltiples idiomas / sites en multisite filtrando por site o usando un endpoint multisitio.
- Soporte para guardado automático de métricas (qué enlaces se insertan) para análisis de enlazado interno.
Resumen
Sugerir enlaces internos en el editor de bloques combinando un endpoint REST bien diseñado con una UI JavaScript en el editor es una solución flexible y segura. El ejemplo mostrado cubre lo esencial: registro de ruta, sanitización, localización de datos para el script, búsqueda con debounce, cache simple y una acción de inserción en el editor. Desde aquí puedes mejorar la relevancia, la interfaz (autocompletar con keyboard nav) y el rendimiento (cache server-side o búsqueda externa) según las necesidades del proyecto.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |