Adiós a Ivory Search: cómo he puesto un buscador propio en el menú de INDAGA.net

Valencia, 02/09/2026, G.B.
Desde hace tiempo, el buscador de INDAGA.net dependía de un plugin llamado Ivory Search. Funcionaba bien… hasta que ha dejado de hacerlo. Empezó a fallar sin motivo aparente, y en vez de perder horas depurando un plugin de terceros, decidí hacer lo que suelo hacer con este tipo de problemas: prescindir de él y montar algo propio, ligero y sin dependencias externas. Aquí os cuento el proceso completo, con sus vueltas incluidas.

El punto de partida

WordPress trae de serie un buscador nativo perfectamente funcional: el clásico ?s= que consulta directamente la base de datos a través de WP_Query. No necesita ningún plugin. Lo único que hacía falta era darle un sitio visible en la web, concretamente un icono en el menú de navegación que, al pulsarlo, desplegara el cuadro de búsqueda.

Como ya tengo un child theme de Twenty Seventeen (Indaga Child lo he llamado) con código PHP propio, la solución más limpia era añadir el buscador ahí mismo, aprovechando los hooks que ofrece WordPress para modificar el menú.

El código: un filtro y un poco de CSS

WordPress permite enganchar contenido extra a cualquier menú mediante el filtro wp_nav_menu_items. Con esto añadí un elemento más a la lista del menú: un botón con el icono de lupa (usando Dashicons, la librería de iconos que trae WordPress) y, justo debajo, el formulario de búsqueda nativo oculto por defecto:

add_filter('wp_nav_menu_items', 'indaga_add_search_to_menu', 10, 2);
function indaga_add_search_to_menu($items, $args) {
    if (isset($args->theme_location) && $args->theme_location === 'top') {
        $search_html  = '<li>';
        $search_html .= '<button type="button" aria-expanded="false" aria-controls="menu-search-form" aria-label="Abrir buscador">';
        $search_html .= '<span class="dashicons dashicons-search"></span>';
        $search_html .= '</button>';
        $search_html .= '<div id="menu-search-form">';
        $search_html .= get_search_form(false);
        $search_html .= '</div>';
        $search_html .= '</li>';

        $items .= $search_html;
    }
    return $items;
}

Este filtro lo añadí directamente al functions.php de mi child theme, junto al resto de funciones personalizadas que ya tenía (como el aviso de tiempo de lectura o la notificación por correo al publicar). Es el mismo archivo donde centralizo todo el código propio del tema.

El CSS se encarga de que el formulario permanezca oculto hasta que se pulsa el icono. Para el comportamiento de abrir y cerrar (toggle), en vez de meter el JavaScript directamente en el footer, preferí crear un archivo aparte: assets/js/menu-search-toggle.js, dentro de la carpeta del child theme. Mantener el JS en su propio archivo, en vez de incrustarlo en línea, hace que sea más fácil de mantener y que el navegador pueda cachearlo por separado.

document.addEventListener('DOMContentLoaded', function () {
    var toggleBtn = document.querySelector('.search-toggle-btn');
    var searchForm = document.querySelector('.menu-search-form');

    if (!toggleBtn || !searchForm) return;

    toggleBtn.addEventListener('click', function () {
        var isOpen = searchForm.classList.toggle('is-open');
        toggleBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
        if (isOpen) {
            var input = searchForm.querySelector('input[type="search"]');
            if (input) input.focus();
        }
    });

    document.addEventListener('click', function (e) {
        if (!searchForm.contains(e.target) && !toggleBtn.contains(e.target)) {
            searchForm.classList.remove('is-open');
            toggleBtn.setAttribute('aria-expanded', 'false');
        }
    });
});

Para que WordPress cargue ese archivo, hace falta encolarlo correctamente, también desde functions.php, en vez de simplemente enlazarlo a mano:

add_action('wp_enqueue_scripts', 'indaga_enqueue_search_toggle_script');
function indaga_enqueue_search_toggle_script() {
    wp_enqueue_script(
        'indaga-menu-search-toggle',
        get_stylesheet_directory_uri() . '/assets/js/menu-search-toggle.js',
        array(),
        wp_get_theme()->get('Version'),
        true
    );
}

Usar wp_get_theme()->get('Version') como número de versión del script tiene una ventaja práctica: cada vez que subo cambios al JS y actualizo la versión del child theme, WordPress invalida automáticamente la caché del navegador para ese archivo, evitando que se quede una versión antigua cacheada, tal y como ya hacía con el CSS.

El primer obstáculo: identificar el menú correcto

WordPress diferencia entre el nombre visible de un menú (el que ve el administrador en el panel, en mi caso «Principal (2)») y su theme_location, que es el identificador interno que usa el tema para saber dónde pintarlo. Son cosas distintas, y confundirlas es un error habitual.

Como Twenty Seventeen solo registra dos ubicaciones —top (menú superior) y social (redes sociales)— y mi child theme no añade ninguna ubicación propia, bastaba con usar top en el filtro. Lo confirmé revisando directamente el código fuente del tema en su repositorio oficial, sin necesidad de tocar nada en el servidor.

El segundo obstáculo: el icono invisible

Con el código subido, el HTML del icono aparecía correctamente en el código fuente de la página (lo comprobé viendo el código fuente del navegador), pero visualmente no se veía nada en el menú. Aquí es donde toca ser metódico:

¿Está el HTML presente? Sí, confirmado.

¿Puede ser la caché? Casi seguro. Uso Autoptimize para combinar y minificar CSS y JS, y si no se regenera bien después de subir cambios, el navegador sigue sirviendo la versión antigua.

¿Y si el icono está ahí pero es «invisible» por color? Bingo. El icono de la lupa (Dashicons) no heredaba el color blanco del resto del menú, así que se confundía con el fondo. Solo se veía al pasar el ratón por encima, porque el estado hover sí forzaba un cambio de color.

Intentar arreglarlo fijando color: #ffffff en el botón no fue suficiente: el pseudo-elemento que dibuja el icono de la fuente Dashicons no siempre hereda el color tan limpiamente como cabría esperar, sobre todo cuando hay otras reglas CSS con más peso de por medio. La solución definitiva y más robusta fue añadir directamente un fondo oscuro al botón en el CSS personalizado del tema:

button {
    background: #000000;
    border: none;
    cursor: pointer;
    padding: 8px 12px;
    line-height: 1;
    color: #ffffff;
}

button .dashicons {
    color: inherit;
    font-size: 20px;
    width: 20px;
    height: 20px;
}

Con el fondo negro y el icono en blanco, el contraste queda garantizado sin depender de si el navegador hereda bien o mal el color de un pseudo-elemento de fuente.

El resultado

Un buscador funcional, integrado en el menú «Principal (2)», que usa exclusivamente el motor de búsqueda nativo de WordPress. Sin plugins de terceros que mantener, sin sorpresas de compatibilidad, y con el código totalmente bajo mi control para poder ajustarlo cuando quiera.

Brave Accounts y Email Aliases: contraseñas que nunca viajan y correos que nunca se revelan

brave

Valencia, 01/09/2026, G.B.
Brave acaba de presentar Brave Accounts, un nuevo sistema para iniciar sesión en sus servicios que cambia por completo la forma en que se gestionan las contraseñas. La promesa, resumida en una frase: tu contraseña nunca llega a los servidores de Brave. Ni cifrada, ni convertida en hash, ni «guardada un instante en memoria y luego descartada». Directamente, no se transmite.

Sobre esa base ha llegado ya el primer servicio propio: Email Aliases, que permite registrarte en cualquier web sin dar tu correo real. En este artículo vamos a ver cómo funciona la criptografía detrás de Brave Accounts y qué ofrece exactamente Email Aliases.

El problema de siempre: el instante en que tu contraseña queda expuesta

Cuando inicias sesión en cualquier web convencional, el navegador abre una conexión TLS que cifra la contraseña mientras viaja por la red. Hasta ahí, todo bien. El problema aparece cuando esa contraseña llega al servidor: en algún punto —a menudo en un balanceador de carga o un CDN, antes incluso de que la aplicación la vea— la conexión se descifra y la contraseña queda en texto plano, aunque sea solo por una fracción de segundo.

Ese instante es la grieta. Ahí es donde puede colarse cualquier cosa: un log mal configurado que escribe cuerpos de petición en disco, una dependencia comprometida en la ruta de autenticación, un empleado malintencionado, un atacante haciendo scraping de memoria.

Podrías pensar: ¿y si el navegador calcula el hash y solo envía eso? El problema es que, si haces eso, el hash se convierte en la contraseña: cualquiera que robe la base de datos podría reenviar ese hash directamente al servidor y autenticarse sin saber la contraseña real. La verificación tiene que hacerse contra el valor auténtico, así que ese valor auténtico tiene que entregarse en algún momento. O eso se creía.

Y luego está el escenario de filtración masiva. El caso de LinkedIn en 2012 sigue siendo el ejemplo de manual: contraseñas guardadas como SHA-1 sin sal (salt), un hash rápido y sin ninguna variación entre usuarios. La filtración inicial se estimó en 6,5 millones de cuentas; cuatro años después, al aparecer el volcado completo, resultaron ser 117 millones, y prácticamente todas se descifraron en cuestión de días porque contraseñas idénticas producían hashes idénticos.

La solución: el protocolo OPAQUE

Brave Accounts se apoya en OPAQUE, un protocolo de intercambio de claves autenticado por contraseña (aPAKE, por sus siglas en inglés) desarrollado por los investigadores Jarecki, Krawczyk y Xu, estandarizado por el IRTF y publicado como RFC 9807 en julio de 2025. Cuenta con una prueba formal de seguridad en un modelo criptográfico robusto (universally composable).

La idea central: cliente y servidor ejecutan una especie de «baile» criptográfico en el que el cliente demuestra que conoce la contraseña sin revelarla en ningún momento, y ambas partes terminan compartiendo una clave de sesión nueva. Todo esto se apoya en curvas elípticas, así que es rápido y transparente para el usuario.

Cómo funciona

Registro. Cuando eliges tu contraseña, tu dispositivo y el servidor ejecutan conjuntamente una función pseudoaleatoria oblivious (OPRF): tú «enmascaras» tu contraseña con un valor aleatorio y envías esa versión enmascarada; el servidor aplica una clave secreta propia y te devuelve el resultado; tu dispositivo lo desenmascara. El valor final depende tanto de tu contraseña como del secreto del servidor, pero ninguno de los dos ha visto el dato del otro.

Tu dispositivo pasa después ese valor por Argon2id (una función de derivación resistente a fuerza bruta) y lo combina con un nonce aleatorio para generar un par de claves de autenticación. Lo único que se envía al servidor es una clave pública, una clave de enmascarado y un pequeño «sobre» (envelope) con el nonce y una etiqueta de autenticación. Nada de eso sirve de nada sin la contraseña original.

Inicio de sesión. Al volver a escribir la contraseña, el mismo baile OPRF reproduce el mismo valor si la contraseña es correcta. El servidor devuelve el envelope guardado; el dispositivo lo desenmascara, regenera el par de claves y comprueba la etiqueta de autenticación. Si la contraseña era correcta, todo encaja y ambas partes negocian una clave de sesión mediante un intercambio autenticado. Si era incorrecta, las claves derivadas simplemente no cuadran, y nada sensible llega a viajar por la red.

Por qué es importante

  1. No hay contraseña en texto plano en ningún momento, así que se eliminan de raíz los escenarios de logs filtrados, memoria comprometida o insiders curiosos.
  2. Se acaba la precomputación de diccionarios. Como parte de la derivación depende de un secreto que solo tiene el servidor, un atacante no puede preparar tablas de contraseñas por adelantado: necesita robar la base de datos y el secreto del servidor, y aun así el ataque tiene que hacerse cuenta por cuenta, contra una función de derivación costosa en memoria (Argon2id).
  3. OPAQUE no es solo un login: es una primitiva de acuerdo de claves. El protocolo también genera una export key, un secreto derivado de la contraseña que el servidor jamás llega a conocer. Esa clave se puede usar para cifrado extremo a extremo — es, de hecho, un mecanismo parecido al que usa WhatsApp para cifrar las copias de seguridad de chats protegidas con contraseña.

Brave ya apunta a construir sobre esto su próxima versión de Sync: en lugar del actual emparejamiento por código QR, será posible iniciar sesión con la cuenta de Brave para sincronizar un dispositivo nuevo con cifrado extremo a extremo. Eso sí, la sincronización sin cuenta seguirá disponible para quien no quiera crear una.

Lo que OPAQUE no resuelve

Conviene ser honestos con las limitaciones, que Brave reconoce en su propio anuncio:

  • No protege frente al phishing. Si el atacante controla la página o la app donde escribes tu contraseña, puede capturarla antes de que el protocolo entre en juego.
  • No convierte una contraseña débil en una fuerte. «password123» sigue cayendo ante un ataque de fuerza bruta online, de ahí que Brave Accounts limite los intentos y exija contraseñas razonablemente robustas.
  • Un compromiso total del servidor sigue siendo un riesgo. Un atacante decidido podría, en ese escenario, intentar un ataque de diccionario offline cuenta por cuenta. OPAQUE lo hace mucho más lento y no paralelizable entre usuarios, pero no lo hace imposible.

El primer servicio construido sobre Brave Accounts: Email Aliases

Brave Accounts no es un fin en sí mismo, sino la base sobre la que Brave empieza a construir servicios propios. El primero en llegar, integrado directamente en el navegador desde la versión 1.94.117 (27 de agosto de 2026), es Email Aliases.

La idea es sencilla y no del todo nueva —servicios como SimpleLogin, Firefox Relay o Hide My Email de Apple llevan tiempo ofreciendo algo parecido—, pero aquí llega sin necesidad de extensión ni suscripción: al rellenar el campo de correo de cualquier formulario de registro, Brave ofrece generar una dirección de alias de un solo uso para ese sitio. Los mensajes que lleguen a esa dirección se reenvían automáticamente a tu correo real, que el sitio web nunca llega a conocer.

¿Por qué importa esto? Porque, a diferencia de una cookie, el correo electrónico es un identificador que te sigue entre dispositivos, navegadores y servicios. Aunque Brave ya aísla lo que cada web puede almacenar en el navegador (cookies y caché particionados por sitio), eso no evita que dos empresas crucen datos por su cuenta usando tu email como nexo común. Ahí es donde entra este servicio: al usar una dirección distinta para cada sitio, rompe ese enlace de rastreo entre servicios y, de paso, facilita cortar el grifo si una dirección empieza a recibir spam — basta con desactivar ese alias concreto sin tocar tu bandeja principal.

Cómo funciona en la práctica:

  • Hace falta una cuenta de Brave (gratuita) con tu correo real vinculado.
  • Los alias se generan al pulsar sobre un campo de email en una web, o eligiendo «Nuevo alias de correo» desde el menú contextual.
  • Se gestionan desde Configuración → Autocompletar y contraseñas → Email Aliases. También escribiendo en la barra de direcciones brave://settings/email-aliases
  • Brave ofrece cinco alias gratuitos por ahora; la compañía ha anunciado que llegará un nivel de pago sin ese límite, además de soporte para móvil próximamente.
  • Los mensajes reenviados pasan por un filtro automático de spam y malware, y se borran de los servidores de Brave a los pocos segundos de entregarse.
  • Las notas que añadas a cada alias se guardan solo en local, salvo que actives Brave Sync, en cuyo caso viajan cifradas de extremo a extremo.

Vale la pena avisar de un matiz práctico: al ser un servicio de correo nuevo, es posible que durante los primeros tiempos algunos mensajes reenviados caigan en la carpeta de spam del destinatario, mientras Brave construye reputación como remitente.

email aliases

Cómo probarlo

Ambas funciones ya están disponibles en el canal estable de escritorio, pero solo a partir de la versión 1.94.117. Si tu Brave es más antiguo (como me pasó a mí, que estaba en la 1.93.136), no verás el banner de creación de cuenta ni la opción de alias hasta actualizar: ve a brave://settings/help para forzar la comprobación de actualizaciones, reinicia el navegador y después entra en brave://settings/getStarted para crear tu cuenta. Desde ahí, el alias de correo se ofrece directamente al rellenar cualquier formulario de registro.

La versión de escritorio es la misma para Windows, macOS y Linux —solo cambia el empaquetado (.exe, .dmg/.pkg, .deb/.rpm)—, así que la actualización llega a todos por igual, aunque en Linux con paquetes de terceros (Flatpak, Snap) el ritmo puede variar algo respecto al repositorio oficial APT.


Fuentes: Blog oficial de Brave — Brave Accounts y Blog oficial de Brave — Email Aliases

Clementine, mi reproductor de música en Linux

clementine
  • Ahora mismo escuchando una canción en Clementine que grabé con mi hermano Gianni: «Tasmanian Blues»… Igual se me ocurre algún día subirla algún repositorio de música gratuito. Tendré que consultarlo. Ya veremos…

Valencia, 31/08/2026, G.B.
Llevo tiempo usando Clementine en mi Ubuntu MATE 22.04, y sigue siendo mi opción de cabecera para gestionar música local. Es un reproductor multiplataforma, gratuito y de código abierto, que nació en 2010 como bifurcación de Amarok 1.4, cuando ese proyecto dio el salto a una interfaz que a muchos no nos habría convencido.

Lo que más valoro de Clementine es justamente eso: mantiene una interfaz clásica, ligera y muy funcional, sin florituras innecesarias. Para quien viene de iTunes o del Amarok de toda la vida, resulta muy intuitivo.

Lo que ofrece:

  • Gestión de biblioteca musical local con portadas y etiquetas
  • Reproducción de streaming (SoundCloud, Jamendo, radios de internet)
  • Compatible con MP3, FLAC, OGG, WMA y más formatos habituales
  • Ecualizador, letras de canciones y sincronización con dispositivos MP3/iPod
  • Consumo de recursos muy contenido, algo que se agradece en equipos más modestos

Es software libre bajo licencia GPL, y aunque su ritmo de desarrollo se ha ralentizado respecto a sus años de mayor actividad (2010-2015), sigue recibiendo actualizaciones puntuales y cuenta con una comunidad fiel, sobre todo en el mundo Linux.

Web oficial: clementine-player.org

Diferencias entre los atributos para enlaces nofollow y noopener

link

Valencia, 30/08/2026, G.B.
Cuando trabajamos con enlaces en HTML es habitual encontrarse con atributos como rel="nofollow" o rel="noopener", muchas veces usados juntos y, por eso mismo, confundidos con frecuencia. Aunque ambos «viajan» dentro del mismo atributo rel, cumplen funciones completamente distintas: uno habla con los buscadores, el otro protege al usuario.

nofollow: una señal para los buscadores

El atributo nofollow es una instrucción dirigida a los rastreadores de buscadores como Google o Bing. Le indica al motor de búsqueda que no debe seguir ese enlace para rastrear la página de destino ni transferirle autoridad (lo que tradicionalmente se conocía como «PageRank») a través de él.

Se usa habitualmente en:

  • Enlaces en comentarios de usuarios, para evitar que se abuse de ellos con fines de SEO.
  • Enlaces patrocinados o de afiliados (aunque hoy Google recomienda sponsored para estos casos específicos).
  • Enlaces hacia sitios externos sobre los que no queremos «avalar» contenido con nuestra autoridad de dominio.

Es importante entender que nofollow no afecta en absoluto a la experiencia del usuario. El enlace sigue funcionando exactamente igual al hacer clic; es una señal puramente semántica para los buscadores.

noopener: una medida de seguridad del navegador

El atributo noopener, en cambio, no tiene ninguna relación con el posicionamiento. Es una protección de seguridad del navegador que entra en juego cuando un enlace se abre en una nueva pestaña con target="_blank".

Por defecto, cuando se abre un enlace de esta forma, la página de destino recibe acceso parcial a la ventana original a través del objeto window.opener en JavaScript. Esto puede ser aprovechado por una página maliciosa para manipular o redirigir la pestaña de origen, una técnica conocida como tabnabbing.

Añadir rel="noopener" corta ese acceso: la nueva pestaña se abre con normalidad, pero pierde la capacidad de interactuar con la ventana que la originó. Como beneficio adicional, en varios navegadores esto también mejora ligeramente el rendimiento al abrir la nueva pestaña.

¿Por qué se ven tantas veces juntos?

Es habitual encontrar enlaces con rel="nofollow noopener", especialmente en enlaces externos que se abren en una pestaña nueva. La combinación tiene sentido porque cada atributo resuelve un problema distinto:

<a href="https://ejemplo.com" target="_blank" rel="nofollow noopener">
  Enlace externo
</a>
  • nofollow cuida la relación con los buscadores.
  • noopener cuida la seguridad del usuario.

No son intercambiables ni redundantes: usar solo uno de los dos deja la otra cuestión sin resolver. Si un enlace se abre en pestaña nueva, noopener debería estar presente casi siempre por seguridad, independientemente de si además se decide aplicar o no nofollow según el criterio SEO de cada caso.

Recursos Plugin v2.0: unificando dos plugins en uno

plugin recursos

Valencia, 29/08/2026, G.B.
Uno de esos proyectos que llevaba tiempo rondándome era simplificar el mantenimiento de dos plugins de WordPress que, con el tiempo, habían acabado haciendo cosas muy parecidas. La solución: fusionarlos en uno solo, Recursos Plugin v2.0, y de paso aprovechar para resolver algunos problemas técnicos que arrastraba desde hacía tiempo.

Por qué unificar

Mantener dos plugins con funcionalidades solapadas significa duplicar esfuerzo cada vez que hay que tocar algo: dos bases de código que revisar, dos sitios donde aplicar el mismo parche, dos superficies de error. Unificarlos en un único plugin reduce esa carga de mantenimiento y deja un código más coherente y fácil de razonar.

Como en el resto de mis plugins, la premisa se mantiene: cero dependencias externas. Todo el código es PHP nativo, sin librerías de terceros que puedan quedar desactualizadas o introducir vulnerabilidades.

El reto técnico: búsqueda vía GET

La búsqueda de recursos funciona con parámetros en la URL (GET) en lugar de POST. Esto tiene una ventaja clara para el usuario: los resultados de búsqueda son enlazables y compartibles. Puedes copiar la URL después de buscar y enviarla directamente a alguien, o guardarla en marcadores, y al abrirla se reproduce la misma búsqueda.

Para los formularios que sí requieren envío de datos, apliqué el patrón POST/Redirect/GET: el formulario se envía por POST, el servidor procesa la petición y responde con una redirección a una URL GET. Esto evita el clásico problema de que, al recargar la página después de enviar un formulario, el navegador pregunte si quieres «reenviar los datos» —o peor, los reenvíe sin avisar duplicando la acción.

El arreglo de compatibilidad con MySQL

Este plugin hereda el mismo ajuste que ya había aplicado en mi plugin de Autores: cambiar la longitud de las claves de índice a VARCHAR(191).

¿Por qué 191 y no 255? Según lo que he consultado 191 es un límite práctico impuesto por MySQL 5.7 cuando se usa el juego de caracteres utf8mb4 (necesario para soportar emojis y caracteres Unicode completos). Con utf8mb4, cada carácter puede ocupar hasta 4 bytes, y el límite de InnoDB para un índice es de 767 bytes en total. Eso deja un máximo de 191 caracteres para columnas indexadas antes de que MySQL lance un error.

Es uno de esos detalles que no se nota hasta que un día, sin previo aviso, la creación de una tabla falla con un mensaje críptico. Ya solucionado, y de forma consistente en todos mis plugins.

Instrucciones rápidas (desde el panel de control del plugin)

  1. Rellena el formulario de abajo y pulsa «Añadir Recurso» para crear uno nuevo.
  2. Pulsa «Editar» en la tabla para modificar un recurso existente.
  3. Pulsa «Eliminar» para borrarlo (se pedirá confirmación).
  4. Para mostrar los recursos en una página o entrada, usa el shortcode mostrar_recursos (entre corchetes [ ])
    (con buscador y paginación).
  5. Para mostrar solo los 4 recursos más recientes, usa el shortcode ultimos_recursos (entre corchetes [ ])

    Ejemplo con el shortcode para mostrar los últimos recursos añadidos:

    Mega
    Servicio con sede en Nueva Zelanda que destaca por su cifrado de extremo a extremo por defecto (zero-knowledge): ni siquiera Mega puede acceder al contenido de tus archivos. Ofrece 20 GB gratuitos de entrada.
    Dropbox
    Uno de los pioneros del sector, conocido por su sincronización robusta y fiable entre dispositivos. Ofrece funciones interesantes de recuperación de versiones anteriores de archivos y buena integración con apps de terceros.
    OneDrive
    Servicio de Microsoft integrado de forma nativa en Windows y en la suite Office 365. Cómodo si ya trabajas en el ecosistema Microsoft, con edición colaborativa directa en Word, Excel y PowerPoint online.
    pCloud
    Almacenamiento en la nube con sede en Suiza, con opción de pago único de por vida (en lugar de suscripción mensual) y cifrado adicional opcional (pCloud Crypto). Buena alternativa para quien busca privacidad reforzada.

Resultado

Después de un tiempo probándolo en producción, el plugin unificado funciona correctamente. Un solo punto de mantenimiento, búsquedas enlazables, sin reenvíos de formulario molestos, y compatibilidad garantizada con instalaciones MySQL 5.7. Os dejo el enlace de descarga del plugin, por si álguien quiere probarlo:

Descargar plugin (en formato .zip) de mi cuenta de pCloud

Plugin de WordPress para frases aleatorias

Valencia, 29/08/2026, G.B.
Tal y comenté recientemente en este post, aquí va el plugin de WordPress para frases aleatorias. Os paso también el archivo.php.

Recordad que para subir un plugin a WordPress (que no esté en el repositorio, como es el caso), hay que crear una carpeta con el mismo nombre del archivo .php, comprimirlo en .zip y subirlo por FTP o S FTP (mejor esto último) al apartado del escritorio de WordPress «Subir plugin».

Aquí va e código de la primera versión del plugin. Si no consigues verlo correcatente en este post, descarga el archivo frases-aleatorias.zip de mi cuenta de pCloud e instálalo en WordPRess – > Plugins.

<?php
/**
* Plugin Name: Frases Aleatorias Widget
* Description: Widget, shortcode

y panel de administración para gestionar frases (añadir, editar, eliminar). Cada frase incluye enlace de traducción automática al castellano. Sin dependencias externas.
* Version: 2.0
* Author: Puntocomunica.com
*/

if ( ! defined( ‘ABSPATH’ ) ) {
exit; // Salida directa no permitida.
}

define( ‘FA_TABLA’, ‘fa_frases’ ); // se le añadirá el prefijo de WP al usarla

/* ==========================================================
* INSTALACIÓN: crea la tabla y la rellena con las 50 frases
* la primera vez que se activa el plugin.
* ========================================================== */
function fa_activar_plugin() {
global $wpdb;
$tabla = $wpdb->prefix . FA_TABLA;
$charset_collate = $wpdb->get_charset_collate();

$sql = «CREATE TABLE $tabla (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
texto TEXT NOT NULL,
autor VARCHAR(191) NOT NULL,
PRIMARY KEY (id)
) $charset_collate;»;

require_once ABSPATH . ‘wp-admin/includes/upgrade.php’;
dbDelta( $sql );

// Solo se siembran datos si la tabla está vacía (primera instalación)
$total = $wpdb->get_var( «SELECT COUNT(*) FROM $tabla» );
if ( (int) $total === 0 ) {
$frases_iniciales = array(
array( «He who blames others has a long way to go on his journey. He who blames himself is halfway there. He who blames no one has arrived.», «Chinese Proverb» ),
array( «The only way to do great work is to love what you do.», «Steve Jobs» ),
array( «In the middle of difficulty lies opportunity.», «Albert Einstein» ),
array( «It does not matter how slowly you go as long as you do not stop.», «Confucius» ),
array( «Fall seven times, stand up eight.», «Japanese Proverb» ),
array( «The journey of a thousand miles begins with a single step.», «Lao Tzu» ),
array( «Whether you think you can or you think you can’t, you’re right.», «Henry Ford» ),
array( «The best way to predict the future is to create it.», «Abraham Lincoln» ),
array( «Success is not final, failure is not fatal: it is the courage to continue that counts.», «Winston Churchill» ),
array( «Believe you can and you’re halfway there.», «Theodore Roosevelt» ),
array( «Do not go where the path may lead, go instead where there is no path and leave a trail.», «Ralph Waldo Emerson» ),
array( «The mind is everything. What you think you become.», «Buddha» ),
array( «Water which is too pure has no fish.», «Chinese Proverb» ),
array( «A smooth sea never made a skilled sailor.», «African Proverb» ),
array( «He that can have patience can have what he will.», «Benjamin Franklin» ),
array( «You miss 100% of the shots you don’t take.», «Wayne Gretzky» ),
array( «Life is what happens when you’re busy making other plans.», «John Lennon» ),
array( «The only impossible journey is the one you never begin.», «Tony Robbins» ),
array( «Not all those who wander are lost.», «J.R.R. Tolkien» ),
array( «It always seems impossible until it’s done.», «Nelson Mandela» ),
array( «Do what you can, with what you have, where you are.», «Theodore Roosevelt» ),
array( «If you want to go fast, go alone. If you want to go far, go together.», «African Proverb» ),
array( «The best time to plant a tree was 20 years ago. The second best time is now.», «Chinese Proverb» ),
array( «What lies behind us and what lies before us are tiny matters compared to what lies within us.», «Ralph Waldo Emerson» ),
array( «Turn your wounds into wisdom.», «Oprah Winfrey» ),
array( «Knowing yourself is the beginning of all wisdom.», «Aristotle» ),
array( «The unexamined life is not worth living.», «Socrates» ),
array( «He who has a why to live can bear almost any how.», «Friedrich Nietzsche» ),
array( «Out of difficulties grow miracles.», «Jean de La Bruyère» ),
array( «A journey of a thousand miles must begin with a single step.», «Lao Tzu» ),
array( «Even a small star shines in the darkness.», «Finnish Proverb» ),
array( «The bamboo that bends is stronger than the oak that resists.», «Japanese Proverb» ),
array( «Better to light a candle than curse the darkness.», «Chinese Proverb» ),
array( «When the winds of change blow, some build walls, others build windmills.», «Chinese Proverb» ),
array( «Fall down seven times, get up eight.», «Japanese Proverb» ),
array( «One who plants a tree knows they will not sit in its shade.», «Indian Proverb» ),
array( «The nail that sticks out gets hammered down.», «Japanese Proverb» ),
array( «A single arrow is easily broken, but not ten in a bundle.», «Japanese Proverb» ),
array( «He who has never failed has never tried anything new.», «African Proverb» ),
array( «Even the greatest of trees was once a nut who held its ground.», «American Proverb» ),
array( «The river that forgets its source will dry up.», «African Proverb» ),
array( «A bird does not sing because it has an answer, it sings because it has a song.», «Chinese Proverb» ),
array( «Patience is bitter, but its fruit is sweet.», «Aristotle» ),
array( «He who is not courageous enough to take risks will accomplish nothing in life.», «Muhammad Ali» ),
array( «The best preparation for tomorrow is doing your best today.», «H. Jackson Brown Jr.» ),
array( «Success usually comes to those who are too busy to be looking for it.», «Henry David Thoreau» ),
array( «Do not wait for the perfect moment, take the moment and make it perfect.», «Unknown» ),
array( «Great things are done by a series of small things brought together.», «Vincent van Gogh» ),
array( «Perseverance is not a long race; it is many short races one after the other.», «Walter Elliot» ),
array( «You cannot cross the sea merely by standing and staring at the water.», «Rabindranath Tagore» ),
);

foreach ( $frases_iniciales as $f ) {
$wpdb->insert( $tabla, array( ‘texto’ => $f[0], ‘autor’ => $f[1] ) );
}
}
}
register_activation_hook( __FILE__, ‘fa_activar_plugin’ );

/* ==========================================================
* FUNCIONES DE ACCESO A DATOS
* ========================================================== */
function fa_obtener_frases() {
global $wpdb;
$tabla = $wpdb->prefix . FA_TABLA;
return $wpdb->get_results( «SELECT id, texto, autor FROM $tabla ORDER BY id ASC» );
}

function fa_obtener_frases_json() {
$frases = fa_obtener_frases();
$salida = array();
foreach ( $frases as $f ) {
$salida[] = array( ‘texto’ => $f->texto, ‘autor’ => $f->autor );
}
return wp_json_encode( $salida );
}

/* ==========================================================
* RENDERIZADO EN PORTADA (widget y shortcode)
* Incluye enlace de traducción automática al castellano
* (Google Translate), generado por JS según la frase mostrada.
* ========================================================== */
function fa_render_markup_y_script( $instancia_id ) {
ob_start();
?>
<div id=»fa-widget-<?php echo esc_attr( $instancia_id ); ?>» class=»fa-widget» style=»font-family: Georgia, serif; padding: 16px; border-left: 4px solid #444; background: #f9f9f9;»>
<p class=»fa-texto» style=»font-style: italic; font-size: 16px; line-height: 1.5; margin: 0 0 8px 0;»></p>
<p class=»fa-autor» style=»text-align: right; font-weight: bold; margin: 0 0 8px 0;»></p>
<p style=»text-align: right; margin: 0 0 12px 0;»>
<a class=»fa-traducir» href=»#» target=»_blank» rel=»noopener noreferrer» style=»font-size: 13px; text-decoration: underline;»>Traducir al castellano</a>
</p>
<button class=»fa-btn» type=»button» style=»cursor: pointer; padding: 6px 14px; border: none; border-radius: 4px; background: #444; color: #fff; font-size: 14px;»>Otra frase</button>
</div>
<script>
(function() {
var frases = <?php echo fa_obtener_frases_json(); ?>;
var contenedor = document.getElementById(‘fa-widget-<?php echo esc_js( $instancia_id ); ?>’);
if (!contenedor || !frases.length) return;
var elTexto = contenedor.querySelector(‘.fa-texto’);
var elAutor = contenedor.querySelector(‘.fa-autor’);
var elTraducir = contenedor.querySelector(‘.fa-traducir’);
var elBtn = contenedor.querySelector(‘.fa-btn’);

function mostrarFraseAleatoria() {
var indice = Math.floor(Math.random() * frases.length);
var frase = frases[indice];
elTexto.textContent = ‘\u201C’ + frase.texto + ‘\u201D’;
elAutor.textContent = ‘\u2014 ‘ + frase.autor;
elTraducir.href = ‘https://translate.google.com/?sl=en&tl=es&text=’ + encodeURIComponent(frase.texto) + ‘&op=translate’;
}

elBtn.addEventListener(‘click’, mostrarFraseAleatoria);
mostrarFraseAleatoria();
})();
</script>
<?php
return ob_get_clean();
}

function fa_shortcode_frases_aleatorias() {
static $contador = 0;
$contador++;
return fa_render_markup_y_script( ‘sc-‘ . $contador );
}
add_shortcode( ‘frases_aleatorias’, ‘fa_shortcode_frases_aleatorias’ );

class FA_Widget_Frases extends WP_Widget {
public function __construct() {
parent::__construct(
‘fa_widget_frases’,
__( ‘Frase Aleatoria’, ‘frases-aleatorias’ ),
array( ‘description’ => __( ‘Muestra una frase célebre al azar, con enlace de traducción y botón para cambiarla.’, ‘frases-aleatorias’ ) )
);
}

public function widget( $args, $instance ) {
echo $args[‘before_widget’];
if ( ! empty( $instance[‘titulo’] ) ) {
echo $args[‘before_title’] . apply_filters( ‘widget_title’, $instance[‘titulo’] ) . $args[‘after_title’];
}
echo fa_render_markup_y_script( ‘widget-‘ . $this->id );
echo $args[‘after_widget’];
}

public function form( $instance ) {
$titulo = ! empty( $instance[‘titulo’] ) ? $instance[‘titulo’] : __( ‘Frase del día’, ‘frases-aleatorias’ );
?>
<p>
<label for=»<?php echo esc_attr( $this->get_field_id( ‘titulo’ ) ); ?>»><?php _e( ‘Título:’, ‘frases-aleatorias’ ); ?></label>
<input class=»widefat» id=»<?php echo esc_attr( $this->get_field_id( ‘titulo’ ) ); ?>» name=»<?php echo esc_attr( $this->get_field_name( ‘titulo’ ) ); ?>» type=»text» value=»<?php echo esc_attr( $titulo ); ?>»>
</p>
<?php
}

public function update( $new_instance, $old_instance ) {
$instance = array();
$instance[‘titulo’] = ( ! empty( $new_instance[‘titulo’] ) ) ? sanitize_text_field( $new_instance[‘titulo’] ) : »;
return $instance;
}
}
add_action( ‘widgets_init’, function () {
register_widget( ‘FA_Widget_Frases’ );
} );

/* ==========================================================
* PANEL DE ADMINISTRACIÓN: listar, añadir, editar, eliminar
* ========================================================== */
add_action( ‘admin_menu’, function () {
add_menu_page(
‘Frases Aleatorias’,
‘Frases Aleatorias’,
‘manage_options’,
‘fa-frases’,
‘fa_pagina_admin’,
‘dashicons-format-quote’,
58
);
} );

function fa_pagina_admin() {
if ( ! current_user_can( ‘manage_options’ ) ) {
return;
}

global $wpdb;
$tabla = $wpdb->prefix . FA_TABLA;

/* ———- Procesar acciones enviadas por formulario ———- */
if ( isset( $_POST[‘fa_accion’] ) && check_admin_referer( ‘fa_guardar_frase’, ‘fa_nonce’ ) ) {
$texto = isset( $_POST[‘fa_texto’] ) ? sanitize_textarea_field( wp_unslash( $_POST[‘fa_texto’] ) ) : »;
$autor = isset( $_POST[‘fa_autor’] ) ? sanitize_text_field( wp_unslash( $_POST[‘fa_autor’] ) ) : »;

if ( $texto !== » && $autor !== » ) {
if ( $_POST[‘fa_accion’] === ‘crear’ ) {
$wpdb->insert( $tabla, array( ‘texto’ => $texto, ‘autor’ => $autor ) );
echo ‘<div class=»notice notice-success is-dismissible»><p>Frase añadida.</p></div>’;
} elseif ( $_POST[‘fa_accion’] === ‘actualizar’ && ! empty( $_POST[‘fa_id’] ) ) {
$wpdb->update( $tabla, array( ‘texto’ => $texto, ‘autor’ => $autor ), array( ‘id’ => (int) $_POST[‘fa_id’] ) );
echo ‘<div class=»notice notice-success is-dismissible»><p>Frase actualizada.</p></div>’;
}
} else {
echo ‘<div class=»notice notice-error is-dismissible»><p>El texto y el autor no pueden estar vacíos.</p></div>’;
}
}

if ( isset( $_GET[‘fa_eliminar’] ) && check_admin_referer( ‘fa_eliminar_frase’ ) ) {
$wpdb->delete( $tabla, array( ‘id’ => (int) $_GET[‘fa_eliminar’] ) );
echo ‘<div class=»notice notice-success is-dismissible»><p>Frase eliminada.</p></div>’;
}

/* ———- ¿Estamos editando una frase? ———- */
$editando = null;
if ( isset( $_GET[‘fa_editar’] ) ) {
$editando = $wpdb->get_row( $wpdb->prepare( «SELECT * FROM $tabla WHERE id = %d», (int) $_GET[‘fa_editar’] ) );
}

$frases = fa_obtener_frases();
?>
<div class=»wrap»>
<h1>Frases Aleatorias</h1>
<p>Shortcode: <code>

</code> · También disponible como widget en Apariencia → Widgets.</p>

<h2><?php echo $editando ? ‘Editar frase’ : ‘Añadir nueva frase’; ?></h2>
<form method=»post» style=»max-width: 600px;»>
<?php wp_nonce_field( ‘fa_guardar_frase’, ‘fa_nonce’ ); ?>
<input type=»hidden» name=»fa_accion» value=»<?php echo $editando ? ‘actualizar’ : ‘crear’; ?>»>
<?php if ( $editando ) : ?>
<input type=»hidden» name=»fa_id» value=»<?php echo esc_attr( $editando->id ); ?>»>
<?php endif; ?>

<table class=»form-table»>
<tr>
<th><label for=»fa_texto»>Texto (inglés)</label></th>
<td><textarea name=»fa_texto» id=»fa_texto» rows=»3″ class=»large-text» required><?php echo $editando ? esc_textarea( $editando->texto ) : »; ?></textarea></td>
</tr>
<tr>
<th><label for=»fa_autor»>Autor</label></th>
<td><input type=»text» name=»fa_autor» id=»fa_autor» class=»regular-text» required value=»<?php echo $editando ? esc_attr( $editando->autor ) : »; ?>»></td>
</tr>
</table>

<?php submit_button( $editando ? ‘Actualizar frase’ : ‘Añadir frase’ ); ?>
<?php if ( $editando ) : ?>
<a href=»<?php echo esc_url( admin_url( ‘admin.php?page=fa-frases’ ) ); ?>» class=»button»>Cancelar edición</a>
<?php endif; ?>
</form>

<h2>Frases existentes (<?php echo count( $frases ); ?>)</h2>
<table class=»widefat striped»>
<thead>
<tr>
<th style=»width: 55%;»>Texto</th>
<th>Autor</th>
<th style=»width: 160px;»>Acciones</th>
</tr>
</thead>
<tbody>
<?php foreach ( $frases as $f ) : ?>
<tr>
<td><?php echo esc_html( $f->texto ); ?></td>
<td><?php echo esc_html( $f->autor ); ?></td>
<td>
<a href=»<?php echo esc_url( admin_url( ‘admin.php?page=fa-frases&fa_editar=’ . $f->id ) ); ?>»>Editar</a>
|
<a href=»<?php echo esc_url( wp_nonce_url( admin_url( ‘admin.php?page=fa-frases&fa_eliminar=’ . $f->id ), ‘fa_eliminar_frase’ ) ); ?>»
onclick=»return confirm(‘¿Eliminar esta frase?’);» style=»color: #b32d2e;»>Eliminar</a>
|
<a href=»https://translate.google.com/?sl=en&tl=es&text=<?php echo rawurlencode( $f->texto ); ?>&op=translate» target=»_blank» rel=»noopener noreferrer»>Traducir</a>
</td>
</tr>
<?php endforeach; ?>
<?php if ( empty( $frases ) ) : ?>
<tr><td colspan=»3″>Todavía no hay frases.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php
}

Script para cargar frases aleatorias como gadget en Blogger

Valencia, 28/08/2026, G.B.
Un script para cargar 50 frases de forma aleatoria en un gadget de Blogger. En breve estará disponible como plugin para WordPress con la misma función.

Aquí dejo el código con las instrucciones para insertarlo como gadget de Blogger. Yo lo he llamado «Some Thoughts…». Incluye 50 frases (en inglés, eso sí). El ejemplo en Blogger puedes verlo aquí y también en WordPress como un widget el pie de página de esta misma web. Repito, habrá un plugin para WordPress que incluya administración de las frases o «pensamientos».

Sin más, este es el código:

<!-- 
GADGET DE FRASES ALEATORIAS PARA BLOGGER
Cómo instalarlo:
1. En el panel de Blogger, ve a Diseño > Añadir un gadget.
2. Elige "HTML/JavaScript".
3. Pega todo este código en el cuadro de contenido.
4. Guarda.
-->

<div id="frase-widget" style="font-family: Georgia, serif; padding: 16px; border-left: 4px solid #444; background: #f9f9f9; max-width: 100%; box-sizing: border-box;">
<p id="frase-texto" style="font-style: italic; font-size: 16px; line-height: 1.5; margin: 0 0 8px 0;"></p>

<p id="frase-autor" style="text-align: right; font-weight: bold; margin: 0 0 12px 0;"></p>

<button id="frase-btn" style="cursor: pointer; padding: 6px 14px; border: none; border-radius: 4px; background: #444; color: #fff; font-size: 14px;">
Otra frase
</button>
</div>

<script>
(function() {
var frases = [
{
texto: "He who blames others has a long way to go on his journey. He who blames himself is halfway there. He who blames no one has arrived.",
autor: "Chinese Proverb"
},
{
texto: "The only way to do great work is to love what you do.",
autor: "Steve Jobs"
},
{
texto: "In the middle of difficulty lies opportunity.",
autor: "Albert Einstein"
},
{
texto: "It does not matter how slowly you go as long as you do not stop.",
autor: "Confucius"
},
{
texto: "Fall seven times, stand up eight.",
autor: "Japanese Proverb"
},
{
texto: "The journey of a thousand miles begins with a single step.",
autor: "Lao Tzu"
},
{
texto: "Whether you think you can or you think you can't, you're right.",
autor: "Henry Ford"
},
{
texto: "The best way to predict the future is to create it.",
autor: "Abraham Lincoln"
},
{
texto: "Success is not final, failure is not fatal: it is the courage to continue that counts.",
autor: "Winston Churchill"
},
{
texto: "Believe you can and you're halfway there.",
autor: "Theodore Roosevelt"
},
{
texto: "Do not go where the path may lead, go instead where there is no path and leave a trail.",
autor: "Ralph Waldo Emerson"
},
{
texto: "The mind is everything. What you think you become.",
autor: "Buddha"
},
{
texto: "Water which is too pure has no fish.",
autor: "Chinese Proverb"
},
{
texto: "A smooth sea never made a skilled sailor.",
autor: "African Proverb"
},
{
texto: "He that can have patience can have what he will.",
autor: "Benjamin Franklin"
},
{
texto: "You miss 100% of the shots you don't take.",
autor: "Wayne Gretzky"
},
{
texto: "Life is what happens when you're busy making other plans.",
autor: "John Lennon"
},
{
texto: "The only impossible journey is the one you never begin.",
autor: "Tony Robbins"
},
{
texto: "Not all those who wander are lost.",
autor: "J.R.R. Tolkien"
},
{
texto: "It always seems impossible until it's done.",
autor: "Nelson Mandela"
},
{
texto: "Do what you can, with what you have, where you are.",
autor: "Theodore Roosevelt"
},
{
texto: "If you want to go fast, go alone. If you want to go far, go together.",
autor: "African Proverb"
},
{
texto: "The best time to plant a tree was 20 years ago. The second best time is now.",
autor: "Chinese Proverb"
},
{
texto: "What lies behind us and what lies before us are tiny matters compared to what lies within us.",
autor: "Ralph Waldo Emerson"
},
{
texto: "Turn your wounds into wisdom.",
autor: "Oprah Winfrey"
},
{
texto: "Knowing yourself is the beginning of all wisdom.",
autor: "Aristotle"
},
{
texto: "The unexamined life is not worth living.",
autor: "Socrates"
},
{
texto: "He who has a why to live can bear almost any how.",
autor: "Friedrich Nietzsche"
},
{
texto: "Out of difficulties grow miracles.",
autor: "Jean de La Bruyère"
},
{
texto: "A journey of a thousand miles must begin with a single step.",
autor: "Lao Tzu"
},
{
texto: "Even a small star shines in the darkness.",
autor: "Finnish Proverb"
},
{
texto: "The bamboo that bends is stronger than the oak that resists.",
autor: "Japanese Proverb"
},
{
texto: "Better to light a candle than curse the darkness.",
autor: "Chinese Proverb"
},
{
texto: "When the winds of change blow, some build walls, others build windmills.",
autor: "Chinese Proverb"
},
{
texto: "Fall down seven times, get up eight.",
autor: "Japanese Proverb"
},
{
texto: "One who plants a tree knows they will not sit in its shade.",
autor: "Indian Proverb"
},
{
texto: "The nail that sticks out gets hammered down.",
autor: "Japanese Proverb"
},
{
texto: "A single arrow is easily broken, but not ten in a bundle.",
autor: "Japanese Proverb"
},
{
texto: "He who has never failed has never tried anything new.",
autor: "African Proverb"
},
{
texto: "Even the greatest of trees was once a nut who held its ground.",
autor: "American Proverb"
},
{
texto: "The river that forgets its source will dry up.",
autor: "African Proverb"
},
{
texto: "A bird does not sing because it has an answer, it sings because it has a song.",
autor: "Chinese Proverb"
},
{
texto: "Patience is bitter, but its fruit is sweet.",
autor: "Aristotle"
},
{
texto: "He who is not courageous enough to take risks will accomplish nothing in life.",
autor: "Muhammad Ali"
},
{
texto: "The best preparation for tomorrow is doing your best today.",
autor: "H. Jackson Brown Jr."
},
{
texto: "Success usually comes to those who are too busy to be looking for it.",
autor: "Henry David Thoreau"
},
{
texto: "Do not wait for the perfect moment, take the moment and make it perfect.",
autor: "Unknown"
},
{
texto: "Great things are done by a series of small things brought together.",
autor: "Vincent van Gogh"
},
{
texto: "Perseverance is not a long race; it is many short races one after the other.",
autor: "Walter Elliot"
},
{
texto: "You cannot cross the sea merely by standing and staring at the water.",
autor: "Rabindranath Tagore"
}
];

function mostrarFraseAleatoria() {
var indice = Math.floor(Math.random() * frases.length);
var frase = frases[indice];

// Comillas dobles normales
document.getElementById('frase-texto').textContent =
'"' + frase.texto + '"';

// Guion normal en lugar del guion largo
document.getElementById('frase-autor').textContent =
'- ' + frase.autor;
}

document
.getElementById('frase-btn')
.addEventListener('click', mostrarFraseAleatoria);

mostrarFraseAleatoria();
})();
</script>

Nicotine, la alternativa a SoulSeek

nicotine

Valencia, 25/08/2026, G.B.
Hoy me he encontrado en este mismo sitio un viejo post sobre el programa P2P (Peer To Peer) SoulSeek. Aunque lo tenía instalado en Ubuntu Mate 22.0.4, he intentado instalarlo en el portátil con el cual estoy redactando esta entrada y no ha habido forma de arrancar el .AppImage. Así pues, en vez de enredarme con las posibles causas, he decidido instalar una alternativa: Nicotine+.

nicotine interfaz

¿Y de qué va Nicotine+, qué ventajas tiene?

Pues básicamente es un cliente alternativo para la red Soulseek, escrito en Python y con interfaz GTK. Para quien no lo conozca, Soulseek es una red P2P que lleva viva desde el año 2000 (sí, ha llovido), centrada casi en exclusiva en música, y con una comunidad que sigue siendo sorprendentemente activa incluso hoy en día. A diferencia de otras redes P2P más «todoterreno», Soulseek tiene un rollo muy de nicho: colecciones curadas, ripeos de calidad, vinilos digitalizados, rarezas, directos, discografías completas… el paraíso para quien busca algo que no va a encontrar en Spotify.

El cliente oficial de Soulseek existe, pero es cerrado, algo tosco y su soporte en Linux ha sido siempre irregular (de ahí mi intento fallido con el AppImage). Nicotine+ nació precisamente para cubrir ese hueco: es software libre, lleva desarrollándose desde hace más de 20 años, y hoy por hoy es probablemente el cliente más recomendado para usar Soulseek en Linux, aunque también hay versiones para Windows y macOS.

Sus ventajas principales, resumidas:

  • Es libre y multiplataforma, con desarrollo activo y actualizaciones frecuentes.
  • Interfaz mucho más cuidada que el cliente oficial, con pestañas para búsquedas, descargas, salas de chat, listas de «buddies», etc.
  • Gestión de descargas y colas bastante más robusta: reintentos automáticos, priorización, control fino de qué compartes y qué no.
  • Ligero: no depende de Electron ni de frameworks pesados, así que consume poquísimos recursos comparado con según qué alternativas.
  • Empaquetado nativo en la mayoría de distros, sin depender de un AppImage suelto que un día decide no arrancar (como me ha pasado a mí).

Cómo instalar Nicotine+ en Linux (Ubuntu MATE / derivados de Ubuntu-Debian)

Aquí es donde la cosa se pone fácil, porque Nicotine+ está en los repositorios oficiales de Ubuntu y Debian desde hace tiempo. Lo más directo, vía terminal (lo que yo he hecho):

sudo apt update
sudo apt install nicotine

Sí, el paquete se llama simplemente nicotine, aunque el proyecto se llame Nicotine+. Cosas de la vida del software libre.

Si tu versión de Ubuntu/Mate trae una versión antigua en repos y quieres ir a la última release, tienes un par de alternativas mejores que tirar de AppImage:

Flatpak (recomendado si quieres la versión más reciente sin líos de dependencias):

flatpak install flathub org.nicotine_plus.Nicotine

PPA oficial del proyecto, si prefieres mantenerte con apt pero con versiones más al día:

sudo add-apt-repository ppa:nicotine-team/stable
sudo apt update
sudo apt install nicotine

Una vez instalado, lo lanzas desde el menú de aplicaciones o con nicotine en terminal, te creas una cuenta en la red Soulseek (es instantáneo, no hace falta verificación de email ni nada raro) y ya puedes empezar a buscar. Eso sí: como en toda red P2P, comparte algo de tu propia colección — la etiqueta no escrita de Soulseek es que si solo descargas y no compartes nada, tarde o temprano notarás que las descargas te van más lentas o directamente algunos usuarios te ignoran.

En el próximo post os cuento cómo tengo configurada la carpeta compartida y algunos trucos de búsqueda para encontrar justo lo que buscáis sin perderos entre resultados basura.

Por qué desactivar Disable Comments no basta para reabrir los comentarios en WordPress

comentarios wordpress

Valencia, 22/08/2026, G.B.
Hace unos días me encontré con un problema aparentemente sencillo que terminó siendo bastante más terco de lo esperado: quería volver a permitir comentarios en las entradas de este blog, y por más que lo intentaba, el formulario para escribir un comentario nuevo seguía sin aparecer. Los comentarios antiguos se veían perfectamente, pero no había forma de dejar uno nuevo.

El sospechoso obvio: Disable Comments

En algún momento había usado el plugin Disable Comments para bloquear los comentarios en ciertos tipos de contenido. Cuando decidí revertir eso, hice lo que parecía razonable:

  1. Entré en los ajustes del plugin y cambié el tipo de contenido (la taxonomía de entradas) para el que se bloqueaban los comentarios, dejando «Entradas» fuera del bloqueo.
  2. Guardé los cambios.
  3. Desactivé el plugin.

Nada. Los comentarios seguían sin poder crearse.

Pensando que quizás el plugin no se había desactivado del todo bien, fui un paso más allá:

  1. Lo desinstalé por completo, borrándolo del sitio.
  2. Vacié toda la caché, tanto la del plugin de caché del sitio como cualquier caché a nivel de servidor.

Seguía sin funcionar. En este punto ya no tenía sentido seguir sospechando del plugin: ni siquiera estaba instalado.

Reduciendo el problema

Revisé el archivo single.php del tema por si el formulario de comentarios se estaba omitiendo por algún motivo relacionado con la plantilla, pero el código era el estándar, sin nada raro. También descarté que hubiera algún hook personalizado interfiriendo en functions.php del tema hijo: nada relacionado con comments_open, save_post ni wp_insert_post_data.

Lo que sí encontré, consultando directamente la base de datos, fue esto: entradas ya publicadas, incluso una programada, tenían el campo comment_status en closed en la tabla wp_posts, a pesar de que el ajuste general de WordPress (Ajustes > Comentarios > «Permitir que la gente envíe comentarios en las entradas nuevas») estaba correctamente en abierto.

La explicación tiene sentido una vez se entiende cómo funciona WordPress internamente: el ajuste default_comment_status solo se aplica en el momento en que se crea la fila del post por primera vez. Si esa entrada se creó (aunque fuera como borrador) mientras el ajuste global estaba en cerrado, o mientras Disable Comments seguía activo bloqueando ese tipo de contenido, el valor closed queda grabado de forma permanente en esa fila concreta. Cambiar el ajuste general después no reescribe retroactivamente las entradas que ya existían; solo afecta a las que se creen a partir de ese momento.

Es decir, el problema nunca estuvo en el plugin en sí, sino en un dato ya escrito en la base de datos que el plugin dejó como herencia antes de desactivarse.

La solución: una consulta SQL

Con muchas entradas afectadas, corregirlas una a una desde el panel de WordPress (edición en lote, cambiando «Comentarios» a «Permitir») era viable pero lento. La solución rápida fue una actualización directa en la base de datos desde phpMyAdmin:

USE nombre_de_tu_base_de_datos;
UPDATE wp_posts SET comment_status = 'open' WHERE post_type = 'post' AND comment_status = 'closed';

Esta consulta actualiza de golpe el estado de comentarios de todas las entradas (post_type = 'post') que estuvieran cerradas, dejando fuera páginas y otros tipos de contenido. Antes de ejecutarla conviene hacer una copia de seguridad de la tabla wp_posts (tienes que poner tu prefijo de las tablas. Por defecto, si no lo has cambiado en la instalación de WordPress es wp_), y si se quiere comprobar el alcance antes de aplicar el cambio, un simple SELECT COUNT(*) FROM wp_posts WHERE post_type = 'post' AND comment_status = 'closed'; da el número exacto de entradas que se van a ver afectadas.

Conclusión

Cuando un plugin como Disable Comments actúa escribiendo valores directamente en la base de datos en lugar de aplicar solo un filtro en tiempo real, desactivarlo (o incluso desinstalarlo) no revierte esos cambios. Los ajustes generales de WordPress tampoco se aplican de forma retroactiva a entradas ya existentes. En estos casos, revisar directamente la base de datos con una consulta SQL es la forma más fiable de confirmar la causa real del problema y, de paso, la manera más rápida de solucionarlo cuando hay muchas entradas implicadas.