Feeds, sitemap y una cabecera que sobra: pequeña auditoría de un sitio WordPress con curl

curl

Valencia, 08/09/2026, G.B.
Hay comandos que uno lanza casi por rutina y que, sin embargo, acaban destapando cosas interesantes. La semana pasada me tocó revisar el sitemap nativo de uno de mis sitios en WordPress y, de paso, aproveché para repasar algo que tenía pendiente desde hace tiempo: los feeds del sitio y una cabecera HTTP que no debería estar ahí.

Qué es el feed de un sitio en WordPress

El feed es una versión del contenido en XML —normalmente RSS 2.0, aunque WordPress también sabe hablar Atom— pensada para que lectores de noticias, agregadores o scripts consuman las entradas sin tener que «leer» el HTML de la página. Es una de esas piezas del núcleo de WordPress que llevan ahí desde siempre y que casi nadie mira hasta que algo deja de funcionar (a mí me pasó hace poco con un gadget de Blogger de terceros que dejó de refrescar entradas porque un Disallow: */feed/ en el robots.txt se lo estaba impidiendo).

Lo interesante es que WordPress no genera un único feed, sino uno por cada «vista» relevante del sitio:

  • Feed general, con todas las entradas: /feed/
  • Feed de comentarios de todo el sitio: /comments/feed/
  • Feed de una categoría: /category/nombre-categoria/feed/
  • Feed de una etiqueta: /tag/nombre-etiqueta/feed/
  • Feed de un autor: /author/nombre-autor/feed/
  • Feed de comentarios de una entrada concreta: /nombre-entrada/feed/
  • Feed de una búsqueda: /?s=termino&feed=rss2
  • Feed de un tipo de contenido personalizado, si el CPT lo soporta: /tipo-contenido/feed/

Y todavía hay una capa más: dentro de cada uno de esos ámbitos, el formato de salida se puede forzar con el parámetro ?feed=, con hasta cuatro variantes: rss2 (el que usa WordPress por defecto), rss (RSS 0.92, un formato ya legado), atom y rdf (RSS 1.0). En total, contando ámbitos y formatos, un sitio WordPress típico expone bastantes más «puertas de entrada» XML de las que uno recuerda a primera vista.

El sitemap no es un feed

Conviene no confundir los feeds con el sitemap. Desde WordPress 5.5 (2020), el núcleo genera automáticamente un sitemap XML nativo en /wp-sitemap.xml, sin necesidad de plugins de SEO. No es RSS ni Atom: es un índice que agrupa entradas, páginas, taxonomías y usuarios en sub-sitemaps independientes (wp-sitemap-posts-post-1.xml, wp-sitemap-users-1.xml, etc.), pensado para que los buscadores rastreen el sitio de forma eficiente.

Para comprobar rápidamente si el sitemap responde bien, sin descargarlo entero, uso el comando curl:

curl -I https://midominio.com/wp-sitemap.xml

El -I fuerza una petición HEAD: solo pide las cabeceras HTTP de la respuesta, no el cuerpo. Es la forma más rápida de saber si una URL existe, qué código de estado devuelve y qué tipo de contenido sirve, sin gastar ancho de banda en el XML completo.

Lo que las cabeceras cuentan (y lo que delatan)

Al lanzar ese curl -I contra uno de mis sitios, obtuve algo así (he sustituido la versión de PHP que aparecía por X.X.XX en el código que adjunto por motivos de seguridad):

HTTP/2 200
content-type: text/xml;charset=UTF-8
date: Fri, 04 Sep 2026 06:42:34 GMT
server: Apache
x-powered-by: PHP/X.X.XX

Todo correcto salvo un detalle: x-powered-by: PHP/X.X.XX está anunciando al mundo, con número de versión exacto, qué motor PHP corre detrás. No es una vulnerabilidad en sí misma, pero sí información gratuita para cualquiera que esté buscando qué CVEs probar contra esa versión concreta. Es el tipo de cabecera que no aporta nada al visitante legítimo y sí algo a quien está reconociendo el terreno.

La buena noticia es que server: Apache ya venía sin número de versión, así que esa parte no hacía falta tocarla.

Cómo quitar la cabecera con php.ini

Yo tengo creado el archivo php.ini (o .user.ini) propio en el directorio del sitio. El mío ya incluía algunos ajustes habituales:

upload_max_filesize = 128M
post_max_size = 128M
max_execution_time = 300
memory_limit = 512M
zlib.output_compression = 1
zlib.output_compression_level = 9

Simplemente basta con añadir una línea más:

expose_php = Off

Tras guardar el archivo -y en algunos casos esperar unos minutos a que el hosting recargue la configuración- un nuevo curl -I me ha confirmado que la cabecera x-powered-by había desaparecido. Un cambio de una sola línea, pero de esos que conviene no dejar pendientes.

Resumiendo

  • Un sitio WordPress puede exponer más de media docena de feeds distintos, cada uno con hasta cuatro formatos posibles.
  • El sitemap nativo (/wp-sitemap.xml) es una pieza distinta, propia del núcleo desde la versión 5.5.
  • curl -I es la herramienta de cabecera rápida para auditar cualquier URL sin descargar su contenido.
  • Si tu hosting lo permite, expose_php = Off en tu php.ini o .user.ini es un cambio mínimo con un beneficio de seguridad nada despreciable.

Un plugin propio para aprender comandos básicos de Linux en WordPress

plugin comandos linux

Tal y como comentaba aquí, he preparado un pequeño plugin para mostrar comandos de Linux, para aprender, vaya. Insertaré el shortcode en una nueva página, en vez de en una entrada o post

Valencia, 06/09/2026, G.B.
Llevo tiempo trabajando en plugins a medida, sin dependencias externas, para cubrir necesidades muy concretas sin cargar el sitio con librerías de terceros. El último que he montado va dirigido a quien empieza con Linux: Comandos Linux, un plugin que permite gestionar una pequeña base de datos de comandos —con su explicación y un ejemplo práctico— y mostrarla en el front end mediante un shortcode.

Qué hace

El plugin crea su propia tabla en la base de datos con cuatro campos por registro:

  • Nombre del comando (ls, grep, chmod…)
  • Explicación, en lenguaje sencillo
  • Ejemplo práctico, listo para copiar y probar en terminal

Desde el panel de administración se pueden crear, editar y eliminar comandos con un formulario simple, y el listado incluye un buscador y paginación para cuando la lista crezca.

El shortcode

Para mostrarlo en cualquier entrada o página basta con insertar, entre corchetes []:

comandos_linux

El front end reproduce la misma lógica que el panel: buscador propio y resultados paginados, sin necesidad de recargar toda la página gracias a los parámetros por URL. Si se quiere ajustar cuántos comandos se muestran por página, se puede indicar con (entre corchetes []:

comandos_linux por_pagina="10"

Por qué así

Como en el resto de plugins que he ido publicando aquí, la idea es mantenerlo ligero: sin librerías externas, con consultas preparadas contra la base de datos, y siguiendo el patrón POST/Redirect/GET para evitar reenvíos accidentales de formularios. Es la misma filosofía que ya apliqué en el plugin de recursos o en el de autores: cubrir una necesidad concreta con el menor peso posible.

De momento arranca con comandos básicos como ls, cd, grep o chmod, pero la idea es irlo ampliando poco a poco a medida que vaya cubriendo más terreno, desde gestión de permisos hasta redes o procesos.

Así queda insertado el shortcode en una página  (iré añadiendo más y mejorándolo): Comandos Linux


Descargar el plugin desde mi cuenta de pCloud

Hooks, actions y filters en WordPress: la diferencia que todo el mundo confunde

Valencia, 05/09/2026, G.B.
Si llevas un tiempo tocando el functions.php de WordPress o escribiendo plugins, seguro que te has encontrado con add_action() y add_filter() mil veces. Pero cuando hay que explicar la diferencia entre ambos, es fácil quedarse en un «bueno, son parecidos». No lo son tanto, y entender bien la diferencia te ahorra bugs raros el día menos pensado.

Hook es el concepto general

Lo primero es aclarar la terminología. En español solemos traducir hook como «gancho», y es el término general que engloba a los otros dos: cualquier punto del código de WordPress donde puedes «engancharte» para ejecutar tu propia función. Ese término general se divide en dos tipos:

  • Actions (acciones)
  • Filters (filtros)

Todo lo demás son matices de estos dos tipos.

Actions: «haz algo aquí»

Una acción es un punto del código donde WordPress simplemente dice: «en este momento ha pasado algo, si quieres, ejecuta tu función». No espera que le devuelvas nada.

// Se ejecuta cada vez que se publica una entrada
add_action( 'publish_post', function( $post_id ) {
    wp_mail( 'admin@midominio.com', 'Nueva entrada publicada', 'ID: ' . $post_id );
});

Aquí no hay ningún dato que recoger de vuelta. La función hace su trabajo (enviar un correo, escribir un log, mostrar algo en pantalla) y punto.

Filters: «toma este dato, modifícalo y devuélvemelo»

Un filtro, en cambio, existe porque WordPress necesita un valor para seguir funcionando: un título, un contenido, un booleano que decide qué hacer. Tu función recibe ese valor, opcionalmente lo cambia, y tiene que devolverlo siempre.

// Añade un emoji al final de cada título
add_filter( 'the_title', function( $title ) {
    return $title . ' 🚀';
});

Si te olvidas del return, rompes el filtro para todo el sitio — es el error clásico de quien empieza con filtros viniendo de acciones.

La tabla que lo resume

ActionFilter
¿Qué hace?Ejecuta código en un punto concretoModifica un dato y lo devuelve
¿Recibe un valor?Opcionalmente, como parámetrosSí, el dato a modificar
¿Debe devolver algo?NoSiempre
Ejemplo típicoEnviar un email al publicar una entradaCambiar el texto de un título

Una forma rápida de identificarlos si estás leyendo código fuente de WordPress: si ves do_action(...), es un hook de tipo acción; si ves apply_filters(...), es un filtro. Y add_action() / add_filter() son las funciones que usas tú para «engancharte» a esos puntos.

Un ejemplo real: desactivar el editor de bloques

El otro día leí un artículo de Ayudawordpress.com (Fernando Tellado) sobre cómo desactivar el editor de bloques de WordPress (Gutenberg) para desactivarlo en una instalación multisitio. El filtro que se usa es un buen ejemplo de por qué tiene que ser un filter y no una action:

add_filter( 'use_block_editor_for_post_type', '__return_false' );
add_filter( 'use_widgets_block_editor', '__return_false' );

WordPress necesita saber, para cada tipo de contenido, si debe cargar el editor de bloques o el clásico — y eso es exactamente un booleano que hay que devolver. Por eso no podría implementarse como acción: una acción no tiene forma de decirle a WordPress «no, usa esto otro en su lugar». Necesita el mecanismo de ida y vuelta que ofrece el filtro.

Por qué merece la pena tenerlo claro

Más allá de la teoría, esto tiene una consecuencia práctica muy directa: si alguna vez te preguntas «¿por qué mi función no está haciendo efecto?», una de las primeras cosas que hay que revisar es si te has enganchado a una acción cuando en realidad necesitabas un filtro (o viceversa). Es un error tonto, pero muy común, y entender la diferencia de raíz evita perder tiempo depurando algo que en realidad es un fallo de concepto, no de sintaxis.

De solución casera a plugin: Indaga Menu Search ya funciona también en Puntocomunica.com

Valencia, 03/09/2026, G.B.
Ayer os contaba cómo había sustituido Ivory Search por un buscador propio, integrado directamente en el menú mediante unas pocas líneas en el functions.php del child theme de INDAGA.net. Funcionaba bien, pero tenía una limitación evidente: estaba atado a este sitio en concreto. Si quería el mismo buscador en otra web, tocaba copiar y pegar código a mano, sitio por sitio.

Así que le he dado una vuelta y lo he convertido en un plugin independiente: Indaga Menu Search.

Por qué merecía la pena convertirlo en plugin

El código en sí no cambia demasiado —sigue usando exclusivamente el buscador nativo de WordPress, sin dependencias externas ni llamadas remotas—, pero empaquetarlo como plugin da varias ventajas:

  • Se activa y desactiva desde el panel, sin tocar el functions.php de ningún tema.
  • Sobrevive a un cambio de tema, porque el código ya no vive dentro del child theme.
  • Es reutilizable en cualquier proyecto WordPress, no solo en los que usan Twenty Seventeen.
  • La ubicación del menú se elige desde una pantalla de ajustes (Ajustes → Buscador en el menú), con un desplegable que muestra las ubicaciones que registra el tema activo y qué menú tiene asignado cada una. Nada de shortcodes ni de editar código: se elige la ubicación y el icono aparece automáticamente en todo el sitio.

Para quien quiera forzarlo por código en un proyecto concreto, sigue existiendo el filtro indaga_menu_search_location, que tiene prioridad sobre lo que se elija en el panel.

Primera instalación real: Puntocomunica.com

La primera prueba fuera de INDAGA.net ha sido en Puntocomunica.com, donde lo he instalado y activado sin ningún ajuste adicional más allá de seleccionar la ubicación del menú correspondiente desde el panel. En cuestión de minutos, el buscador nativo de WordPress estaba ya funcionando ahí también, con el mismo comportamiento: icono discreto en el menú que despliega el formulario de búsqueda al pulsarlo.

Poder instalar el mismo plugin en dos sitios completamente distintos, sin adaptar una sola línea de código más allá de un clic en un desplegable, es exactamente el objetivo de convertir una solución puntual en una herramienta reutilizable.

Descargar plugin de mi cuenta de pCloud

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.

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
}

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.

Función para calcular el tiempo de lectura de una entrada (post)

Valencia, 19/08/2026, G.B.
Si alguna vez has visto en un blog un texto como «5 min de lectura» justo encima del contenido de una entrada, seguramente te hayas preguntado cómo se calcula. La buena noticia es que no hace falta instalar ningún plugin para conseguirlo: con una función sencilla en el functions.php del tema hijo es suficiente.

La idea detrás del cálculo

El tiempo de lectura estimado se basa en un cálculo muy simple: se cuenta el número de palabras del contenido y se divide entre una velocidad media de lectura, normalmente entre 200 y 250 palabras por minuto. No es una ciencia exacta, pero da una idea útil y realista al lector sobre cuánto tiempo le va a llevar la entrada.

La función paso a paso

Lo primero es una función que calcule ese tiempo a partir del contenido del post:

function indaga_calculate_reading_time( $post_id = null ) {
  if ( ! $post_id ) {
    $post_id = get_the_ID();
  }
  $content = get_post_field( 'post_content', $post_id );
  $content = strip_shortcodes( $content );
  $content = wp_strip_all_tags( $content );
  preg_match_all( '/[\p{L}\p{N}]+/u', $content, $matches );
  $word_count = count( $matches[0] );
  $reading_time = ceil( $word_count / 200 );
  if ( $reading_time < 1 ) {
    $reading_time = 1;
  }
  return $reading_time;
}

Merece la pena detenerse en un par de detalles. Antes de contar palabras, se eliminan los shortcodes y las etiquetas HTML con strip_shortcodes() y wp_strip_all_tags(), para no contar código como si fuera texto. Y el conteo de palabras se hace con una expresión regular UTF-8 (preg_match_all con el patrón /[\p{L}\p{N}]+/u) en lugar de la función nativa str_word_count(), porque esta última no reconoce bien acentos ni la letra «ñ», algo imprescindible al trabajar con contenido en castellano.

Mostrarlo automáticamente en cada entrada

Con el cálculo ya resuelto, el siguiente paso es mostrarlo sin tener que tocar ninguna plantilla del tema. Para eso se engancha a un filtro que WordPress ya tiene previsto: the_content.

function indaga_add_reading_time_to_content( $content ) {
  if ( is_single() && in_the_loop() && is_main_query() && 'post' === get_post_type() ) {
    $reading_time = indaga_calculate_reading_time();
    $notice = '<p class="indaga-reading-time">Tiempo de lectura: ' . $reading_time . ' min</p>';
    $content = $notice . $content;
  }
  return $content;
}
add_filter( 'the_content', 'indaga_add_reading_time_to_content' );

Las tres comprobaciones dentro del if son las que marcan la diferencia entre que esto funcione bien o dé problemas: is_single() asegura que solo se muestre en la página individual de una entrada, in_the_loop() y is_main_query() evitan que el aviso aparezca duplicado en widgets, feeds o bucles secundarios, y la comprobación del tipo de contenido lo limita a entradas de blog, dejando fuera páginas u otros tipos de contenido.

Personalizar el estilo

El aviso se genera con la clase CSS indaga-reading-time, así que basta con añadir unas líneas al style.css del tema hijo para darle el aspecto deseado, por ejemplo:

.indaga-reading-time {
  font-style: italic;
  color: #666;
  margin-bottom: 1em;
}

En resumen

Con menos de treinta líneas de código, sin dependencias externas y sin necesidad de un plugin adicional, es posible añadir una funcionalidad que mejora la experiencia de lectura y encaja perfectamente con la filosofía de mantener el sitio ligero y bajo control propio.

Nota: Descargar de mi cuenta pCloud el código completo (en formato .txt)

Plugin Autores revisado y actualizado (versión 3.9)

autores-plugin

Valencia, 18/08/2026, G.B.
El pequeño plugin que hice para insertar publicaciones, Autores, me fallaba en Puntocomunica.com y en este sitio Indaga.net. Tenía que crear la tabla «a mano» desde phpMyAdmin.

En la web para la que hice este plugin funcionaba perfectamente. La diferencia es que esa web tiene la última versión de mariaDB y las otras dos, las mías, una versión más antigua de MySQL. Además, la seguridad no estaba del todo «afinada«.  Ahora creo que sí. Para mostrarlo hay que insertar el shortcode (entre corchetes[ ], como todos los shortcodes): autores_form

Una muestra:

Añadir nueva publicación

No tiene permiso para agregar autores.

Publicaciones listadas

Terry Pratchett
A Stroke of the Pen. The lost Stories
Libros - 2023
Manuel Vicent
Verás el cielo abierto
Libros - 2005
Rosa Montero
Historia del rey transparente
Libros - 2005
Terry Pratchett
Pyramids
Libros - 1992
Terry Pratchett
Equal Rites
Libros - 1987

Este es el enlace de descarga de mi cuenta pCloud: descargar autores-plugin.zip