Control y Reserva de Recursos (CRR): un sistema para la gestión de salas, equipos y servicios en WordPress

Valencia, 14/10/2026, G.B.
Os presento la versión 1.12.0 de mi plugin Control y Reserva de Recursos (CRR) para WordPress
. Una solución 100% nativa y completa diseñada para la gestión integral de inventarios, control de disponibilidad en tiempo real y reserva de espacios, materiales y servicios de forma sencilla e intuitiva desde la web.

Aquí va un ejemplo del sistema principal:

Debes iniciar sesión para realizar, editar o gestionar tus reservas.

Para ver la ocupación mensual en la vista de calendario:

Lun
Mar
Mié
Jue
Vie
Sáb
Dom
1
2
00:00 Sala Conferencias
3
00:00 Sala Conferencias
4
5
6
09:00 Microscopia
09:00 Sala Conferencias
7
09:00 Microscopia
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
×

Ficha de Reserva

Recurso:
Solicitante:
Fechas:
Horario:
Cantidad:
Precio Estimado:
Observaciones:

Acerca de CRR

La gestión eficiente de espacios compartidos, equipamiento de alta gama o servicios especializados suele requerir herramientas complejas o plataformas de terceros que escapan al control directo del sitio web. Con el objetivo de mantener una solución 100% nativa, ligera y sin dependencias externas, nace Control y Reserva de Recursos (CRR), un plugin diseñado a medida para centralizar todo el ciclo de reservas de forma independiente y segura.

¿Qué ofrece Control y Reserva de Recursos (CRR)?

Este desarrollo unifica en un único paquete todo lo necesario para que tanto los usuarios como los administradores gestionen la disponibilidad de recursos de manera fluida:

1. Gestión de Inventario y tipologías flexibles

El administrador cuenta con un panel completo de inventario integrado directamente en la base de datos de WordPress. Los recursos se pueden clasificar en tres grandes categorías (ampliables):

  • Espacios / Salas

  • Material / Equipamiento

  • Servicios Cada recurso incluye control de stock o capacidad simultánea, precio por hora configurable y un correo electrónico asignado para el responsable directo del recurso.

2. Selector inteligente con buscador y filtros en tiempo real

A la hora de solicitar una reserva, los usuarios disfrutan de un selector visual moderno. Permite filtrar los recursos por tipo mediante botones interactivos y refinar la búsqueda al instante escribiendo en un cajón de texto inteligente que filtra por nombre o tipología sin necesidad de recargar la página.

3. Cálculo automático de presupuestos y notificaciones

El sistema calcula de manera automática el coste estimado de la reserva en función de los días, las horas exactas de inicio/fin y la cantidad seleccionada. Además, envía de forma automatizada correos de confirmación y actualización tanto al solicitante como al responsable del recurso, incluyendo opcionalmente el desglose detallado del presupuesto.

4. Vista de calendario mensual interactiva

Mediante el shortcode crr_calendario, cualquier página de la web puede transformarse en un calendario de ocupación mensual. Las reservas se listan ordenadas por horario y, al hacer clic sobre cualquiera de ellas, se despliega una ficha flotante de detalles con toda la información clave de la reserva.

5. Panel de control frontend para usuarios autorizados

No todo depende del panel de administración de WordPress. El plugin permite definir en los ajustes generales una lista de correos electrónicos autorizados. Estos usuarios, sin necesidad de ser administradores, podrán acceder desde el frontal a un Panel de Gestión Avanzado que incluye:

  • Filtros por rango de fechas (Desde / Hasta).

  • Filtro por categorías de recursos.

  • Búsqueda de texto libre compatible con comodines (por ejemplo, utilizando asteriscos como *proyector* o Servicio de*).

6. Exportación dual: Excel y CSV

Para la labor administrativa y de reporting, el panel incluye opciones de exportación instantánea (tanto de todo el histórico como de los datos filtrados en pantalla). El usuario puede elegir entre:

  • 📊 Excel (.csv optimizado): Formato configurado con delimitador de punto y coma (;) y codificación UTF-8 con BOM, garantizando que tildes, eñes y caracteres especiales se abran perfectamente al hacer doble clic en Microsoft Excel o LibreOffice Calc.

  • 📄 CSV estándar: Para integraciones universales.

Compromiso técnico de desarrollo

Siguiendo las directrices de desarrollo nativo de Indaga.net, CRR destaca por:

  • Cero dependencias externas: Sin librerías JavaScript pesadas ni llamadas a APIs de terceros.

  • Seguridad rigurosa: Uso estricto de consultas preparadas ($wpdb), sanitización de entradas, escapado de salidas y validación mediante Nonces de WordPress.

  • Experiencia de usuario optimizada: Incorporación de persistencia de scroll mediante sessionStorage, evitando los molestos saltos al principio de la página tras enviar formularios o aplicar filtros.

  • 📋 Ficha Técnica del Plugin

    • Nombre del Plugin: Control y Reserva de Recursos (CRR)

    • Versión: 1.12.0

    • Autor: Guillermo Beltrán Pilato

    • Sitio Web: Indaga.net

    • Compatibilidad WordPress: 6.0 o superior

    • Licencia: Software libre / Independiente

    • Descripción Breve: Sistema completo e independiente para la gestión y reserva de recursos, salas y servicios con presupuestos automáticos, vista de calendario interactiva, selector avanzado por categorías con buscador instantáneo, panel frontal de filtrado dinámico y exportación dual (Excel optimizado o CSV estándar).

    • Shortcodes Disponibles:

      • crr_reservas (entre corchetes []): Muestra el formulario de solicitud, inventario y panel de gestión frontal.

      • crr_calendario (entre corchetes []): Renderiza la vista de calendario mensual de ocupación.

  • Descargar plugin

Plugin para contabilidad casera y control de pasos diarios

NOTA: Actualizo a la versión 1.7.7, para la gestión de presupuestos mensuales.

Valencia, 17/10/2026, G.B.
Este plugin para WordPress permite llevar una gestión clara del presupuesto familiar y el patrimonio, ofreciendo balances por períodos, gráficos visuales de ingresos y gastos, y un historial sencillo para el registro de pasos diarios.

Ficha Técnica del Plugin

Contabilidad Casera Indaga

  • Nombre del Plugin: Contabilidad Casera Indaga
  • Versión actual: 1.7.7
  • Autor: Guillermo Beltrán Pilato (Indaga.net)
  • Sitio Web del Autor: https://indaga.net
  • Licencia: GPL-2.0+
  • Texto de Dominio: contabilidad-casera-indaga
  • Enlace de Descarga: Descargar v.1.7.7

Descripción General

Sistema de contabilidad casera privado, ligero, seguro y 100% nativo en WordPress. Diseñado bajo los estándares de desarrollo de Indaga.net, opera sin dependencias externas ni servicios de terceros, garantizando la absoluta privacidad de los datos financieros, presupuestarios y de actividad física del usuario.

Novedades y Características de la Versión 1.7.7

Buscador de Conceptos Dedicado: Bloque de búsqueda optimizado e independiente en la pestaña de Gestión Financiera para filtrar de manera rápida y directa los movimientos por descripción, concepto o categoría utilizando consultas seguras con comodines (LIKE).

Edición y Desplegable de Categorías en Presupuestos: Permite seleccionar categorías existentes desde un menú desplegable dinámico o introducir nombres personalizados (como Gastos fantasma, Ocio, Alimentación, etc.) para un control granular de los límites mensuales.

Gestión CRUD Completa de Movimientos: Formulario dinámico integrado con opción de inserción y edición directa de registros anteriores (ingresos, gastos, ahorro e inversión), complementado con botones de acción rápida.

Control de Presupuestos y Alertas Dinámicas: Cálculo automático del gasto mensual por categoría con indicadores visuales de estado (correcto, cerca del límite o presupuesto superado).

Control de Actividad y Pasos: Registro diario de pasos con estimación automática de distancia recorrida en kilómetros.

Gráficos Interactivos Nativos: Integración ligera mediante Chart.js para visualizar la evolución mensual de ingresos/gastos y el desglose porcentual de gastos por categoría en formato de rosquilla (doughnut).

Requisitos Técnicos y Seguridad
Base de Datos Nativa: Utiliza tres tablas optimizadas mediante dbDelta (wp_indaga_contabilidad, wp_indaga_pasos, y wp_indaga_presupuestos).

Seguridad Integral:

Sanitización estricta de entradas de datos (sanitize_text_field, floatval, sanitize_textarea_field).

Protección contra ataques CSRF mediante Nonces de WordPress (check_admin_referer).

Control riguroso de privilegios y permisos de usuario (manage_options).

Cero Dependencias: Sin librerías JavaScript externas complejas ajenas a Chart.js y sin llamadas a APIs o CDNs de pago de terceros.

Descargar Contabilidad Casera Indaga v1.7.7

  • Novedades e Implementaciones de la Versión 1.7.7

    La versión 1.7.7 del plugin Contabilidad Casera Indaga incorpora importantes mejoras orientadas a la agilidad de búsqueda, la personalización de presupuestos y la precisión en la gestión de registros históricos. A continuación se resumen los cambios principales:

    1. Buscador de Conceptos Dedicado

    • Panel de búsqueda independiente: Incorporación de un bloque visual destacado y optimizado en la pestaña de Gestión Financiera para filtrar de forma rápida y directa los movimientos.

    • Filtro multicriterio seguro: Permite buscar y localizar registros de manera inmediata mediante consultas seguras con comodines (LIKE) evaluando simultáneamente los campos de descripción, concepto y categoría.

    • Gestión de filtros: Incluye botón de acción directa y enlace para limpiar la búsqueda de forma instantánea manteniendo el rango de fechas activo.

    2. Selección y Edición Libre de Categorías en Presupuestos

    • Desplegable de categorías existentes: Menú desplegable inteligente que recopila automáticamente las categorías ya utilizadas en la base de datos junto con las sugeridas habitualmente (Gastos fantasma, Ocio, Alimentación, etc.).

    • Edición personalizada: Permite seleccionar una categoría del desplegable o escribir directamente cualquier nombre personalizado en el campo de texto para adaptarlo con total flexibilidad al definir o actualizar los límites mensuales.

    3. Refuerzo en la Gestión CRUD de Movimientos

    • Sincronización de edición: Optimización del formulario dinámico para la correcta actualización y persistencia de movimientos financieros anteriores (ingresos, gastos, ahorro e inversión).

    • Consistencia en acciones rápidas: Estabilización de los procesos de inserción, actualización y borrado con sus correspondientes validaciones de seguridad mediante nonces.

    4. Actualizaciones de Arquitectura y Seguridad

    • Privacidad y Cero Dependencias: Mantenimiento de una arquitectura nativa 100% basada en las APIs oficiales de WordPress y consultas preparadas con $wpdb, sin recurrir a servicios externos o librerías de pago.

    • Seguridad integral reforzada: Aplicación estricta de funciones de sanitización (sanitize_text_field, floatval, sanitize_textarea_field) y validación estricta de permisos de administración (manage_options).

Actualización del plugin Mis Plugins (control de plugins de Indaga.net)

Valencia, 09/10/2026, G.B.
Llevo ya unos cuantos plugins a medida y, como pasa siempre, llega un momento en que pierdes la cuenta de cuál tiene qué versión, aunque los subo todos a mi cuenta de pCloud. Así que hice lo que haría cualquiera: otro plugin. Se llama Mis Plugins (Control de Plugins Propios) y es el catálogo donde apunto todos los demás. Puedes descargarlo desde mi cuenta de pCloud.

Qué hace

Guarda en su propia tabla de la base de datos una ficha por cada plugin, con estos campos:

  • Fecha, solo mes y año
  • Nombre y versión
  • Autor
  • Descripción
  • Tags, separados por comas
  • Enlace de descarga

Desde el panel de administración se crean, editan y eliminan fichas con un formulario sencillo. Y si me equivoco al rellenarlo, el formulario conserva lo que había escrito — antes me lo vaciaba y tocaba empezar de cero, que escuece.

El shortcode

Para enseñarlo en cualquier página o entrada basta con insertar (entre corchetes, []):

mis_plugins

Por defecto muestra diez plugins por página. Si quieres otra cifra (lo mismo, entre corchetes):

mis_plugins por_pagina="10"

El visitante puede buscar por nombre, versión, autor, descripción o tags, y ordenar el listado por fecha, nombre, versión o autor. La paginación funciona con parámetros por URL, sin recargas raras.

De tabla a bloques

La primera versión mostraba todo en una tabla, con una columna por campo. En el papel quedaba muy ordenado, pero en pantalla no había quien lo leyera: descripciones apretadas, tags cortados y en el móvil, ni hablar.

Ahora cada plugin es un bloque independiente: arriba el nombre y la versión, debajo la fecha y el autor, luego la descripción, los tags y el botón de descarga. Como ya no hay cabeceras de columna, el orden se elige con una barra «Ordenar por» encima del listado. De paso arreglé que la versión se ordenara como texto, así que 1.10.0 ya no aparece antes que 1.9.0.

Actualizar sin que WordPress se líe

Aquí viene la parte con anécdota. Para que WordPress reconozca el fichero como una actualización y no como un plugin nuevo, la cabecera tiene que conservar el nombre, la descripción y el autor tal cual, y lo único que cambia es el número de versión. A la primera entrega le había añadido una frase a la descripción, y eso ya basta para liarla.

Luego, al activarlo, me saltó un error fatal. Mi sospecha principal es que WordPress identifica un plugin por la ruta del fichero y no por su nombre. Si la ruta no coincide con la de la versión anterior, quedan los dos cargados a la vez y las funciones se declaran dos veces. La segunda posibilidad era un detalle de sintaxis que no le gusta a las versiones de PHP anteriores a la 7.1. Corregí lo segundo, comprobé que no hubiera dos copias activas y a partir de ahí funcionó. Moraleja: antes de actualizar, mira cómo se llama el fichero que ya tienes instalado.

Porqué así

Como en el resto de plugins, la idea es la misma: un único fichero PHP, sin librerías externas, consultas preparadas, nonces y el patrón POST/Redirect/GET. La estructura de la tabla no cambió entre versiones, así que no se pierde ni un dato al actualizar, que es una promesa que me tomo en serio.

Puedes ver cómo queda en la página de Control de Plugins.

Plugin Solicitud de Congresos para organizaciones

Valencia, 08/10/2026, G.B.
He actualizado y consolidado el plugin Solicitud Congreso (versión 1.6.1), una solución nativa concebida para optimizar el procedimiento de solicitud, aprobación y registro de asistencia a congresos para cualquier tipo de organización, empresa o institución.

Principales Novedades y Funcionalidades

  • Soporte Completo para Documentos Adjuntos: Permite adjuntar documentación acreditativa (programas, cartas de aceptación, justificaciones económicas) directamente en el formulario. Admite hasta 5 archivos por solicitud (PDF, Office, imágenes) con almacenamiento en una ubicación privada y descargas protegidas por permisos.

  • Doble Confirmación por Correo Electrónico: Cada acción (alta, modificación o cancelación) genera una notificación automática para los gestores y envía una copia exacta de confirmación al propio usuario solicitante con el resumen y sus adjuntos.

  • Integración Frontend por Shortcode: Con solo incluir el shortcode solicitud_congreso (entre corchetes, []) en cualquier página privada o de la intranet, los usuarios pueden enviar solicitudes, consultar su historial y gestionar o descargar sus documentos adjuntos sin necesidad de acceder al panel de administración de WordPress.

  • Panel de Control de Administración: Los administradores disponen de una pestaña de gestión global para revisar todas las solicitudes registradas, ajustar la dirección de correo notificadora y consultar la guía rápida de uso.

Garantía de Rendimiento y Seguridad

Siguiendo las directrices de desarrollo de Indaga.net, el plugin ha sido construido con tecnología 100% nativa de WordPress (API de WPDB, Options API y Shortcode API), sin depender de librerías de terceros ni servicios externos. Además, cuenta con validación estricta de seguridad anti-CSRF mediante nonces y aislamiento total de archivos adjuntos para garantizar el máximo cumplimiento en materia de protección de datos y privacidad.

Solicitud Congreso — Versión 1.6.1

  • Nombre del Plugin: Solicitud Congreso

  • Versión Actual: 1.6.1

  • Autor: Guillermo Beltrán Pilato / Indaga.net

  • Dominio del Texto: solicitud-congreso

  • Propósito: Gestión, solicitud y control centralizado de asistencia a congresos nacionales e internacionales para cualquier empresa, institución u organización. Incluye panel de control en administración, formulario frontend por shortcode, gestión privada de adjuntos y notificaciones automáticas por correo electrónico (con copia de confirmación al solicitante).

Arquitectura Técnica y Especificaciones

  1. Gestión de Base de Datos:

    • Tabla nativa independiente: {prefix}congresos.

    • Clave primaria con auto-incremento e índice secundario por id_solicitante para optimización de consultas.

    • Mantenimiento automático de estructura mediante dbDelta() y verificación dinámica de versión en el gancho plugins_loaded.

  2. Seguridad y Control de Acceso:

    • Sanitización y Escapado: Uso estricto de sanitize_text_field, sanitize_email, absint, esc_url_raw, esc_html y esc_attr.

    • Protección Anti-CSRF: Implementación de nonces dinámicos para operaciones de guardado, edición, borrado y descarga.

    • Aislamiento de Recursos: Validación de rol (manage_options) para vistas globales de administración y restricción estricta por get_current_user_id() para usuarios estándar.

  3. Sistema de Adjuntos Privado:

    • Carpeta Protegida: Guardado en wp-content/uploads/solicitud-congreso/ aislada de navegación mediante archivo .htaccess (Options -Indexes, Deny from all).

    • Ofuscación de Nombres: Guardado en disco con identificadores aleatorios de 24 caracteres (sc_nombre_aleatorio). El nombre original se mantiene en la base de datos dentro de una estructura JSON.

    • Descargas Seguras: Flujo de descarga derivado con verificación de permisos y cabeceras anti-caché (readfile con Content-Disposition).

    • Límites: Hasta 5 archivos por solicitud (máx. 5 MB por archivo) en formatos PDF, Word, ODT, Excel, JPG y PNG.

  4. Sistema Doble de Notificaciones por Email:

    • Notificaciones automáticas HTML en eventos de creación, modificación o cancelación/eliminación enviadas al correo del gestor.

    • Copia automática de confirmación enviada directamente al correo del solicitante (a través del email de contacto o del usuario registrado en WordPress).

    • Adjunto físico de archivos en el correo cuando el tamaño total acumulado no supere los 10 MB.

  5. Integración Frontend y Backend:

    • Shortcodes: solicitud_congreso y solicitud_congresos (entre corchetes []).

    • Patrón POST/Redirect/GET: Gestión de estados y mensajes temporales entre redirecciones mediante transients por usuario (sc_aviso_ y sc_form_old_).

    • Caché: Prevención explícita de almacenamiento en caché en páginas privadas activando la constante

    • Descargar el plugin desde mi pCloud 

Ejemplo:

Debes iniciar sesión para gestionar tus solicitudes de congreso. Iniciar sesión

Compromiso de desarrollo: Porqué las actualizaciones de mis plugins nunca más perderán funcionalidades

Valencia, 05/10/2026, G.B.
Me he dado cuenta de que, tras lanzar algunas actualizaciones en mis plugins, he llegado a perder por el camino funcionalidades que eran importantes para mí y para quienes los usan. A partir de ahora mi intención y compromiso es: ninguna actualización volverá a dar un paso atrás.

Cuando desarrollamos y evolucionamos software, es fácil centrarse en la emoción de lanzar una nueva característica o refactorizar el código. Sin embargo, la verdadera calidad de un plugin no solo se mide por lo nuevo que aporta, sino por respetar y conservar la estabilidad de todo lo que ya funcionaba.

Las 3 reglas de oro para las próximas versiones

Para garantizar que cada nueva versión sea una mejora 100% acumulativa, he establecido un protocolo de desarrollo estricto basado en tres pilares:

Compatibilidad acumulativa y no regresión:
Antes de liberar cualquier versión (como las recientes iteraciones de Contabilidad Casera Indaga), se verifica minuciosamente que cada función previa (desde la exportación de datos a CSV con UTF-8 BOM hasta los gráficos de análisis, el seguimiento de actividad o los sistemas de presupuestos) se mantenga intacta y plenamente operativa.

Evolución segura de la base de datos:
Las migraciones de datos y las modificaciones de tablas se seguirán ejecutando de forma incremental mediante procesos seguros (dbDelta y control de versiones interno). El historial de datos y las configuraciones estarán siempre a salvo, sin riesgo de sobrescritura o pérdida de registros.

Auditoría de código previa al despliegue:
Cada actualización pasará por un proceso de revisión completa para asegurar que ningún hook, acción o vista de la interfaz quede fuera por descuido al integrar nuevas herramientas.

Creciendo con solidez

El software debe evolucionar para simplificar la vida, no para complicarla reaprendiendo lo que ya estaba resuelto. Con este compromiso, cada actualización en Indaga.net sumará valor real, manteniendo intactas la confianza, la usabilidad y la potencia de herramientas desarrolladas. Al menos esa es mi intención…

Resumen de las directrices de desarrollo de plugins (Indaga.net)

  1. Retrocompatibilidad Garantizada: Las nuevas funciones deben añadirse como capas opcionales. No se eliminarán ni romperán funcionalidades existentes en instalaciones previas.

  2. Cero Dependencias Externas: Desarrollo 100% nativo en WordPress. Sin librerías JS/PHP de terceros ni llamadas a APIs externas innecesarias, garantizando máxima velocidad, independencia y ligereza.

  3. Seguridad Nativa y Rigurosa:

    • Sanitización de entradas (sanitize_text_field, sanitize_email, etc.).

    • Escapado de salidas (esc_html, esc_attr, esc_textarea, etc.).

    • Validación anti-CSRF mediante Nonces (wp_nonce_field, check_admin_referer).

    • Control estricto de permisos mediante capacidades de usuario (manage_options, user_id).

Cuaderno de Ideas: un plugin ligero para tus anotaciones privadas en WordPress

Valencia, 28/09/2026, G.B.
A la hora de gestionar publicaciones y proyectos de contenidos, la dispersión de notas es un dolor de cabeza. Solemos acumular anotaciones en aplicaciones externas, documentos sueltos o notas rápidas que terminan perdiéndose. Para resolver esto, decidí crear un cuaderno de ideas privado integrado dentro del propio panel de administración de WordPress, permitiéndome preparar borradores, visualizarlos de forma privada y exportarlos a distintos formatos (HTML, Markdown o TPL/BBCode para foros como phpBB) de manera 100% nativa.

¿Por qué desarrollar este plugin?

Aunque en el repositorio de WordPress existen bastantes extensiones para tomar notas, la gran mayoría sufren de dos grandes inconvenientes: o cargan librerías pesadas en JavaScript que ralentizan el panel, o guardan la información en servidores de terceros con modelos de suscripción.

Con este plugin conseguimos un entorno de trabajo rápido, privado e integrado en la base de datos de WordPress. Nos permite trabajar sin dependencias externas y transformar cualquier borrador en el código necesario para publicar en blogs, repositorios como GitHub o comunidades en phpBB.

Requisitos y arquitectura técnica

Para mantener el desarrollo ligero y libre de conflictos de compatibilidad, la arquitectura del plugin se apoya en tres pilares básicos:

  • Cero dependencias externas: Funciona única y exclusivamente con la API nativa de WordPress (Custom Post Types, Custom Taxonomies y Meta Boxes). No requiere frameworks ni scripts externos.
  • Seguridad por diseño: Implementa tokens de seguridad (nonces) para validar formularios, comprobación estricta de permisos de usuario (edit_posts), restricción de vista previa pública y sanitización de campos de texto antes de guardarlos en la base de datos.
  • Exportación y vista previa en el servidor: Genera vistas dinámicas para copiar el código transformado al portapapeles y permite la previsualización del post en el frontend de forma estrictamente privada (devolviendo un error 403 de acceso denegado si intenta acceder un usuario no autenticado).

El código completo del plugin

Crea la carpeta /wp-content/plugins/cuaderno-ideas/ y guarda dentro el archivo cuaderno-ideas.php con el siguiente código:

<?php
/**
 * Plugin Name: Cuaderno de Ideas Privado
 * Description: Diario de ideas privado para gestión de publicaciones con vista previa privada y exportación a HTML, Markdown y TPL (BBCode).
 * Version:     1.2.0
 * Author:      INDAGA.net
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Evitar acceso directo
}

/**
 * 1. REGISTRO DEL CUSTOM POST TYPE Y TAXONOMÍAS NATIVAS
 */
function cip_registrar_post_type() {
    $labels = array(
        'name'               => 'Ideas',
        'singular_name'      => 'Idea',
        'menu_name'          => 'Cuaderno Ideas',
        'add_new'            => 'Añadir Idea',
        'add_new_item'       => 'Añadir Nueva Idea',
        'edit_item'          => 'Editar Idea',
        'new_item'           => 'Nueva Idea',
        'view_item'          => 'Ver Idea Privada',
        'search_items'       => 'Buscar Ideas',
        'not_found'          => 'No se encontraron ideas',
    );

    $args = array(
        'labels'             => $labels,
        'public'             => false, // No se indexa en listados públicos
        'publicly_queryable' => true,  // Permite renderizar la vista individual
        'show_ui'            => true,  // Visible en el panel de administración
        'show_in_menu'       => true,
        'query_var'          => true,
        'rewrite'            => array( 'slug' => 'idea-privada' ),
        'capability_type'    => 'post',
        'has_archive'        => false,
        'hierarchical'       => false,
        'menu_position'      => 20,
        'menu_icon'          => 'dashicons-lightbulb',
        'supports'           => array( 'title', 'editor' ),
    );

    register_post_type( 'cip_idea', $args );

    // Categorías de Ideas
    register_taxonomy( 'cip_categoria', 'cip_idea', array(
        'label'        => 'Categorías de Ideas',
        'rewrite'      => false,
        'hierarchical' => true,
        'show_ui'      => true,
    ) );

    // Etiquetas de Ideas
    register_taxonomy( 'cip_etiqueta', 'cip_idea', array(
        'label'        => 'Etiquetas de Ideas',
        'rewrite'      => false,
        'hierarchical' => false,
        'show_ui'      => true,
    ) );
}
add_action( 'init', 'cip_registrar_post_type' );

/**
 * 2. SEGURIDAD: BLOQUEAR VISTA PÚBLICA A USUARIOS NO AUTENTICADOS
 */
function cip_proteger_vista_privada() {
    if ( is_singular( 'cip_idea' ) ) {
        if ( ! is_user_logged_in() || ! current_user_can( 'edit_posts' ) ) {
            wp_die(
                'Acceso restringido: Esta idea pertenece a un cuaderno de notas privado.',
                'Acceso Denegado',
                array( 'response' => 403 )
            );
        }
    }
}
add_action( 'template_redirect', 'cip_proteger_vista_privada' );

/**
 * 3. SUBMENÚ Y PÁGINA DE INSTRUCCIONES / CONFIGURACIÓN EN EL PANEL
 */
function cip_agregar_menu_instrucciones() {
    add_submenu_page(
        'edit.php?post_type=cip_idea',
        'Instrucciones y Uso',
        'Instrucciones',
        'manage_options',
        'cip-instrucciones',
        'cip_render_pagina_instrucciones'
    );
}
add_action( 'admin_menu', 'cip_agregar_menu_instrucciones' );

function cip_render_pagina_instrucciones() {
    $active_tab = isset( $_GET['tab'] ) ? sanitize_text_field( $_GET['tab'] ) : 'instrucciones';
    ?>
    <div class="wrap">
        <h1>Cuaderno de Ideas Privado</h1>

        <h2 class="nav-tab-wrapper">
            <a href="?post_type=cip_idea&page=cip-instrucciones&tab=instrucciones" class="nav-tab <?php echo $active_tab === 'instrucciones' ? 'nav-tab-active' : ''; ?>">
                Instrucciones
            </a>
            <a href="?post_type=cip_idea&page=cip-instrucciones&tab=ficha" class="nav-tab <?php echo $active_tab === 'ficha' ? 'nav-tab-active' : ''; ?>">
                Ficha Técnica
            </a>
        </h2>

        <div class="tab-content" style="margin-top:20px; background:#fff; padding:20px; border:1px solid #ccc; max-width:900px;">
            <?php if ( $active_tab === 'instrucciones' ) : ?>
                <h2>Guía de Uso del Cuaderno de Ideas</h2>
                <hr>
                <ol>
                    <li><strong>Crear una Idea:</strong> Dirígete a <code>Cuaderno Ideas &gt; Añadir Idea</code>. Escribe el título y el cuerpo del borrador utilizando el editor nativo.</li>
                    <li><strong>Ver Idea como Post Privado:</strong> Haz clic en el botón «Ver Idea Privada» en el panel lateral para abrir la vista individual de la entrada en el frontend (solo accesible si has iniciado sesión).</li>
                    <li><strong>Categorización:</strong> Asigna categorías y etiquetas en la barra lateral para organizar tus temas.</li>
                    <li><strong>Gestión de Estado:</strong> En el panel lateral «Detalles y Estado de la Idea», define la prioridad (Baja, Media, Alta), marca la casilla si ya ha sido publicada, e indica el lugar y fecha de publicación.</li>
                    <li><strong>Exportar Contenido:</strong> En el panel inferior del editor («Exportar Contenido»), selecciona entre las versiones <strong>HTML</strong>, <strong>Markdown</strong> o <strong>TPL (BBCode para phpBB)</strong> y utiliza los botones para copiar directamente el código al portapapeles.</li>
                </ol>
            <?php else : ?>
                <h2>Ficha del Plugin</h2>
                <hr>
                <p>Diario de ideas privado para gestión interna de publicaciones, vista previa privada e exportación a múltiples formatos de marcado.</p>
                
                <p><strong>Acceso:</strong> Cuaderno Ideas &gt; Instrucciones (<code>wp-admin/edit.php?post_type=cip_idea&amp;page=cip-instrucciones</code>)</p>
                
                <p><strong>Alias / Shortcodes:</strong> No aplica (es un plugin exclusivamente de administración interna privada).</p>
                
                <p><strong>Dependencias:</strong> Ninguna (100% nativo mediante Custom Post Types y API de WordPress).</p>
                
                <p><strong>Seguridad:</strong> Validación mediante nonces (<code>wp_verify_nonce</code>), restricción de vista previa en frontend (<code>template_redirect</code> con respuesta 403 para usuarios no autorizados), verificación de capacidades (<code>edit_posts</code>), sanitización al guardar (<code>sanitize_text_field</code>) y escapado estricto en la salida (<code>esc_textarea</code>, <code>esc_attr</code>, <code>esc_url</code>).</p>
            <?php endif; ?>
        </div>
    </div>
    <?php
}

/**
 * 4. CREACIÓN DE CAMPOS PERSONALIZADOS (META BOXES)
 */
function cip_agregar_meta_boxes() {
    add_meta_box(
        'cip_detalles_idea',
        'Detalles y Estado de la Idea',
        'cip_render_meta_box',
        'cip_idea',
        'side',
        'high'
    );

    add_meta_box(
        'cip_exportar_idea',
        'Exportar Contenido',
        'cip_render_export_box',
        'cip_idea',
        'normal',
        'high'
    );
}
add_action( 'add_meta_boxes', 'cip_agregar_meta_boxes' );

function cip_render_meta_box( $post ) {
    wp_nonce_field( 'cip_guardar_metas', 'cip_nonce' );

    $prioridad   = get_post_meta( $post->ID, '_cip_prioridad', true ) ?: 'media';
    $publicado   = get_post_meta( $post->ID, '_cip_publicado', true );
    $donde       = get_post_meta( $post->ID, '_cip_donde_publicado', true );
    $fecha_pub   = get_post_meta( $post->ID, '_cip_fecha_publicado', true );
    $permalink   = get_permalink( $post->ID );

    ?>
    <?php if ( 'publish' === $post->post_status ) : ?>
        <p>
            <a href="<?php echo esc_url( $permalink ); ?>" target="_blank" class="button button-secondary" style="width:100%; text-align:center;">
                <span class="dashicons dashicons-visibility" style="vertical-align:middle; margin-top:-2px;"></span> Ver Idea Privada
            </a>
        </p>
        <hr>
    <?php endif; ?>

    <p>
        <label for="cip_prioridad"><strong>Prioridad:</strong></label><br>
        <select name="cip_prioridad" id="cip_prioridad" style="width:100%;">
            <option value="baja" <?php selected( $prioridad, 'baja' ); ?>>Baja</option>
            <option value="media" <?php selected( $prioridad, 'media' ); ?>>Media</option>
            <option value="alta" <?php selected( $prioridad, 'alta' ); ?>>Alta</option>
        </select>
    </p>
    <hr>
    <p>
        <label for="cip_publicado">
            <input type="checkbox" name="cip_publicado" id="cip_publicado" value="1" <?php checked( $publicado, '1' ); ?>>
            <strong>¿Publicado fuera?</strong>
        </label>
    </p>
    <p>
        <label for="cip_donde_publicado"><strong>¿Dónde se ha publicado?</strong></label><br>
        <input type="text" name="cip_donde_publicado" id="cip_donde_publicado" value="<?php echo esc_attr( $donde ); ?>" style="width:100%;" placeholder="Ej: Blog principal, Medium...">
    </p>
    <p>
        <label for="cip_fecha_publicado"><strong>Fecha de publicación:</strong></label><br>
        <input type="date" name="cip_fecha_publicado" id="cip_fecha_publicado" value="<?php echo esc_attr( $fecha_pub ); ?>" style="width:100%;">
    </p>
    <?php
}

/**
 * 5. GUARDADO SEGURO DE DATOS (SANITIZACIÓN)
 */
function cip_guardar_metas( $post_id ) {
    if ( ! isset( $_POST['cip_nonce'] ) || ! wp_verify_nonce( $_POST['cip_nonce'], 'cip_guardar_metas' ) ) {
        return;
    }
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }
    if ( ! current_user_can( 'edit_post', $post_id ) ) {
        return;
    }

    if ( isset( $_POST['cip_prioridad'] ) ) {
        update_post_meta( $post_id, '_cip_prioridad', sanitize_text_field( $_POST['cip_prioridad'] ) );
    }

    $publicado = isset( $_POST['cip_publicado'] ) ? '1' : '0';
    update_post_meta( $post_id, '_cip_publicado', $publicado );

    if ( isset( $_POST['cip_donde_publicado'] ) ) {
        update_post_meta( $post_id, '_cip_donde_publicado', sanitize_text_field( $_POST['cip_donde_publicado'] ) );
    }

    if ( isset( $_POST['cip_fecha_publicado'] ) ) {
        update_post_meta( $post_id, '_cip_fecha_publicado', sanitize_text_field( $_POST['cip_fecha_publicado'] ) );
    }
}
add_action( 'save_post', 'cip_guardar_metas' );

/**
 * 6. MOTOR DE EXPORTACIÓN (HTML, MARKDOWN, TPL/BBCODE)
 */
function cip_render_export_box( $post ) {
    $content = $post->post_content;
    $title   = $post->post_title;

    $markdown = "# " . $title . "\n\n" . strip_tags( $content );

    $bbcode = "[b]" . $title . "[/b]\n\n";
    $bbcode .= wp_strip_all_tags( $content );

    ?>
    <p>Copia directamente la idea convertida al formato de destino:</p>
    <div style="margin-bottom:15px;">
        <button type="button" class="button" onclick="document.getElementById('cip_exp_html').select(); document.execCommand('copy');">Copiar HTML</button>
        <button type="button" class="button" onclick="document.getElementById('cip_exp_md').select(); document.execCommand('copy');">Copiar Markdown</button>
        <button type="button" class="button" onclick="document.getElementById('cip_exp_tpl').select(); document.execCommand('copy');">Copiar TPL / BBCode</button>
    </div>

    <h4>HTML Limpio:</h4>
    <textarea id="cip_exp_html" style="width:100%; height:80px;" readonly><?php echo esc_textarea( wpautop( $content ) ); ?></textarea>

    <h4>Markdown (.md):</h4>
    <textarea id="cip_exp_md" style="width:100%; height:80px;" readonly><?php echo esc_textarea( $markdown ); ?></textarea>

    <h4>TPL / phpBB (BBCode):</h4>
    <textarea id="cip_exp_tpl" style="width:100%; height:80px;" readonly><?php echo esc_textarea( $bbcode ); ?></textarea>
    <?php
}

Explicación del código de forma sencilla

Para entender la lógica tras el plugin, podemos resumir su funcionamiento en cinco partes clave:

Custom Post Type privado pero consultable

Utilizamos register_post_type configurando 'public' => false pero activando 'publicly_queryable' => true. Esto nos permite generar un enlace individual para visualizar la entrada como si fuera un post publicado estándar dentro de nuestro tema gráfico, pero sin aparecer en buscadores ni en los listados generales del sitio.

Protección de la vista privada en el servidor

Para asegurar que nadie ajeno lea nuestras anotaciones, la función cip_proteger_vista_privada() se ejecuta en el hook template_redirect. Si un visitante no autenticado (o sin permisos de edición) intenta abrir la URL directa de una idea, el servidor interrumpe la carga y devuelve una pantalla de error 403 (Acceso Denegado).

Panel de Instrucciones integrado

Siguiendo la estructura limpia que usamos en nuestros desarrollos, se añade una página dentro del menú del plugin (Cuaderno Ideas > Instrucciones) con pestañas dinámicas que recogen la guía de uso y la ficha técnica con los detalles de seguridad y accesos.

Metadatos y control de estado

Mediante add_meta_box creamos un panel lateral en el editor para fijar la prioridad de la idea, indicar si ya ha sido publicada fuera, la fecha y el medio de destino. Al guardar, el proceso valida un código nonce anti-CSRF y limpia los datos recibidos mediante sanitize_text_field.

Motor de exportación al vuelo

El panel inferior convierte el título y el cuerpo del borrador a tres formatos limpios. Para el HTML aplica la función wpautop() para estructurar los párrafos; para Markdown añade la sintaxis básica de encabezados; y para TPL/phpBB traduce el formato a etiquetas BBCode tipo [b]. Incluye además botones en JavaScript nativo para copiar el resultado con un solo clic.

Si quieres probarlo, puedes descargarlo desde Control de plugins.

De Ivory Search al motor nativo: cómo crear tu propio buscador de WordPress seguro, ligero y sin plugins externos

Valencia, 23/09/2026, G.B.
La búsqueda interna es una de las piezas clave en cualquier sitio web con una buena cantidad de contenidos, categorías y enlaces. Sin embargo, mantener un buscador funcional en WordPress sin cargar el sitio de dependencias o scripts superfluos puede llegar a ser complicado.

Durante mucho tiempo estuve utilizando en el sitio el conocido plugin Ivory Search. Cumplía su función, pero tras las últimas actualizaciones comenzó a fallar hasta que dejó de funcionar del todo. Como solución provisional, opté por sustituirlo por un buscador integrado directamente en la barra de navegación del menú (proceso que ya os comenté en un post anterior aquí en Indaga.net). Aunque funcionaba (y sigue funcionando) correctamente para búsquedas rápidas, faltaba la flexibilidad de disponer de una caja de búsqueda limpia, personalizable y reutilizable mediante shortcode en cualquier página, entrada o widget.

La solución por la que me he decidido ha sido desarrollar un  plugin propio en un único archivo .php: 100% nativo, ligero, sin librerías de terceros y  seguro.

¿Por qué prescindir de plugins comerciales de búsqueda?

La mayoría de los plugins de búsqueda del repositorio introducen tablas adicionales en la base de datos, hojas de estilo CSS pesadas, scripts JavaScript que ralentizan el renderizado («pintado») y paneles llenos de características comerciales inútiles.

El núcleo de WordPress ya cuenta con un motor de búsqueda integrado extremadamente eficiente que se invoca mediante el parámetro estándar ?s=. Lo único que necesitamos es una interfaz limpia para capturar la búsqueda.

Los requisitos para este plugin han sido:

  • Cero dependencias externas: Sin librerías JS, sin llamadas a CDNs y con icono de lupa insertado en SVG nativo
  • Seguridad estricta: Validación y sanitización de todas las entradas del usuario y escape en el renderizado HTML (prevención de vulnerabilidades XSS
  • Aislamiento de estilos: Generación de selectores dinámicos para evitar conflictos con el tema activo de WordPress
  • Panel de administración e instrucciones: Un panel sencillo dentro de Ajustes > Buscador Indaga para cambiar colores, texto del placeholder y elegir varios diseños de maquetación
  • Atributos en Shortcode: Posibilidad de anular la configuración global directamente desde la etiqueta indaga_search (entre corchetes, []). Aquí podéis verlo insertado funcionamiento.

Estructura y código completo del plugin

Para instalarlo, basta con crear la carpeta /wp-content/plugins/indaga-search-plugin/ y guardar dentro el archivo indaga-search-plugin.php con el siguiente código PHP:

<?php
/**
 * Plugin Name: Indaga Search Form
 * Plugin URI:  https://indaga.net
 * Description: Buscador nativo de WordPress ligero, seguro y sin dependencias. Permite varios diseños y configuraciones personalizables desde el panel o mediante atributos de shortcode.
 * Version:     1.0.0
 * Author:      INDAGA.net
 * Text Domain: indaga-search
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Evitar acceso directo por URL
}

/**
 * 1. REGISTRO DE OPCIONES Y PÁGINA DE CONFIGURACIÓN EN EL PANEL
 */
function isf_register_settings() {
	register_setting( 'isf_options_group', 'isf_placeholder', array(
		'type'              => 'string',
		'default'           => 'Buscar en el sitio...',
		'sanitize_callback' => 'sanitize_text_field',
	) );

	register_setting( 'isf_options_group', 'isf_button_text', array(
		'type'              => 'string',
		'default'           => 'Buscar',
		'sanitize_callback' => 'sanitize_text_field',
	) );

	register_setting( 'isf_options_group', 'isf_layout', array(
		'type'              => 'string',
		'default'           => 'inline',
		'sanitize_callback' => 'isf_sanitize_layout',
	) );

	register_setting( 'isf_options_group', 'isf_primary_color', array(
		'type'              => 'string',
		'default'           => '#2563eb',
		'sanitize_callback' => 'sanitize_hex_color',
	) );

	register_setting( 'isf_options_group', 'isf_bg_color', array(
		'type'              => 'string',
		'default'           => '#ffffff',
		'sanitize_callback' => 'sanitize_hex_color',
	) );

	register_setting( 'isf_options_group', 'isf_border_color', array(
		'type'              => 'string',
		'default'           => '#cbd5e1',
		'sanitize_callback' => 'sanitize_hex_color',
	) );
}
add_action( 'admin_init', 'isf_register_settings' );

function isf_sanitize_layout( $input ) {
	$valid_layouts = array( 'inline', 'stacked', 'minimal', 'icon_only' );
	return in_array( $input, $valid_layouts, true ) ? $input : 'inline';
}

function isf_add_admin_menu() {
	add_options_page(
		'Configuración de Buscador Indaga',
		'Buscador Indaga',
		'manage_options',
		'indaga-search',
		'isf_render_admin_page'
	);
}
add_action( 'admin_menu', 'isf_add_admin_menu' );

function isf_render_admin_page() {
	if ( ! current_user_can( 'manage_options' ) ) {
		return;
	}

	$placeholder   = get_option( 'isf_placeholder', 'Buscar en el sitio...' );
	$button_text   = get_option( 'isf_button_text', 'Buscar' );
	$layout        = get_option( 'isf_layout', 'inline' );
	$primary_color = get_option( 'isf_primary_color', '#2563eb' );
	$bg_color      = get_option( 'isf_bg_color', '#ffffff' );
	$border_color  = get_option( 'isf_border_color', '#cbd5e1' );
	?>
	<div class="wrap">
		<h1>Ajustes del Buscador Indaga.net</h1>
		<p>Personaliza el aspecto del buscador nativo o consulta las instrucciones para incrustarlo mediante shortcode.</p>

		<div style="display: flex; gap: 30px; flex-wrap: wrap; margin-top: 20px;">
			<div style="flex: 1; min-width: 320px; max-width: 600px;">
				<form method="post" action="options.php">
					<?php
					settings_fields( 'isf_options_group' );
					do_settings_sections( 'isf_options_group' );
					?>
					<table class="form-table" role="presentation">
						<tr>
							<th scope="row"><label for="isf_placeholder">Texto del placeholder</label></th>
							<td>
								<input type="text" id="isf_placeholder" name="isf_placeholder" value="<?php echo esc_attr( $placeholder ); ?>" class="regular-text" />
							</td>
						</tr>
						<tr>
							<th scope="row"><label for="isf_button_text">Texto del botón</label></th>
							<td>
								<input type="text" id="isf_button_text" name="isf_button_text" value="<?php echo esc_attr( $button_text ); ?>" class="regular-text" />
							</td>
						</tr>
						<tr>
							<th scope="row"><label for="isf_layout">Diseño de la estructura</label></th>
							<td>
								<select id="isf_layout" name="isf_layout">
									<option value="inline" <?php selected( $layout, 'inline' ); ?>>En línea (Clásico - Botón a la derecha)</option>
									<option value="stacked" <?php selected( $layout, 'stacked' ); ?>>Apilado (Campo de texto arriba, botón abajo)</option>
									<option value="minimal" <?php selected( $layout, 'minimal' ); ?>>Minimalista (Caja integrada con lupa)</option>
									<option value="icon_only" <?php selected( $layout, 'icon_only' ); ?>>Solo icono (Botón cuadrado compacto)</option>
								</select>
							</td>
						</tr>
						<tr>
							<th scope="row"><label for="isf_primary_color">Color principal</label></th>
							<td>
								<input type="color" id="isf_primary_color" name="isf_primary_color" value="<?php echo esc_attr( $primary_color ); ?>" />
							</td>
						</tr>
						<tr>
							<th scope="row"><label for="isf_bg_color">Color de fondo del campo</label></th>
							<td>
								<input type="color" id="isf_bg_color" name="isf_bg_color" value="<?php echo esc_attr( $bg_color ); ?>" />
							</td>
						</tr>
						<tr>
							<th scope="row"><label for="isf_border_color">Color del borde</label></th>
							<td>
								<input type="color" id="isf_border_color" name="isf_border_color" value="<?php echo esc_attr( $border_color ); ?>" />
							</td>
						</tr>
					</table>
					<?php submit_button( 'Guardar cambios' ); ?>
				</form>
			</div>

			<div style="flex: 1; min-width: 300px; max-width: 450px; background: #fff; padding: 20px; border: 1px solid #c3c4c7; border-radius: 4px; height: fit-content;">
				<h2>Instrucciones de Uso</h2>
				<p>Inserta el buscador en cualquier lugar utilizando:</p>
				<p><code>
</code></p> <hr style="margin: 15px 0; border: 0; border-top: 1px solid #eee;" /> <h3>Parámetros del Shortcode</h3> <ul> <li><code>layout="inline|stacked|minimal|icon_only"</code></li> <li><code>placeholder="Buscar..."</code></li> <li><code>color="#2563eb"</code></li> </ul> </div> </div> </div> <?php } /** * 2. LÓGICA DEL SHORTCODE
*/ function isf_search_shortcode( $atts ) { $db_placeholder = get_option( 'isf_placeholder', 'Buscar en el sitio...' ); $db_button_text = get_option( 'isf_button_text', 'Buscar' ); $db_layout = get_option( 'isf_layout', 'inline' ); $db_primary_color = get_option( 'isf_primary_color', '#2563eb' ); $db_bg_color = get_option( 'isf_bg_color', '#ffffff' ); $db_border_color = get_option( 'isf_border_color', '#cbd5e1' ); $atts = shortcode_atts( array( 'placeholder' => $db_placeholder, 'button_text' => $db_button_text, 'layout' => $db_layout, 'color' => $db_primary_color, 'bg' => $db_bg_color, 'border' => $db_border_color, ), $atts, 'indaga_search' ); $layout = isf_sanitize_layout( $atts['layout'] ); $placeholder = sanitize_text_field( $atts['placeholder'] ); $button_text = sanitize_text_field( $atts['button_text'] ); $primary_color = sanitize_hex_color( $atts['color'] ); $bg_color = sanitize_hex_color( $atts['bg'] ); $border_color = sanitize_hex_color( $atts['border'] ); $unique_id = 'indaga-search-' . wp_rand( 1000, 9999 ); $search_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle;"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>'; $output = '<style>'; $output .= '.' . $unique_id . ' { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; width: 100%; margin: 10px 0; }'; $output .= '.' . $unique_id . ' form { display: flex; gap: 8px; margin: 0; width: 100%; box-sizing: border-box; }'; $output .= '.' . $unique_id . ' input[type="search"] { flex: 1; background-color: ' . esc_attr( $bg_color ) . '; border: 1px solid ' . esc_attr( $border_color ) . '; border-radius: 4px; padding: 8px 12px; font-size: 14px; color: #1e293b; outline: none; transition: border-color 0.2s ease; box-sizing: border-box; }'; $output .= '.' . $unique_id . ' input[type="search"]:focus { border-color: ' . esc_attr( $primary_color ) . '; }'; $output .= '.' . $unique_id . ' button[type="submit"] { background-color: ' . esc_attr( $primary_color ) . '; color: #ffffff; border: none; border-radius: 4px; padding: 8px 16px; font-size: 14px; font-weight: 500; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; gap: 6px; }'; if ( 'stacked' === $layout ) { $output .= '.' . $unique_id . ' form { flex-direction: column; }'; $output .= '.' . $unique_id . ' button[type="submit"] { width: 100%; }'; } elseif ( 'minimal' === $layout ) { $output .= '.' . $unique_id . ' form { position: relative; }'; $output .= '.' . $unique_id . ' input[type="search"] { padding-right: 40px; }'; $output .= '.' . $unique_id . ' button[type="submit"] { position: absolute; right: 4px; top: 4px; bottom: 4px; background: transparent; color: ' . esc_attr( $primary_color ) . '; }'; } elseif ( 'icon_only' === $layout ) { $output .= '.' . $unique_id . ' button[type="submit"] { padding: 8px 12px; }'; } $output .= '</style>'; $home_url = esc_url( home_url( '/' ) ); $current_query = get_search_query(); $output .= '<div class="indaga-search-container ' . esc_attr( $unique_id ) . '">'; $output .= '<form role="search" method="get" action="' . $home_url . '">'; $output .= '<input type="search" value="' . esc_attr( $current_query ) . '" name="s" placeholder="' . esc_attr( $placeholder ) . '" aria-label="Buscar" required />'; $output .= '<button type="submit">'; if ( 'minimal' === $layout || 'icon_only' === $layout ) { $output .= $search_svg; } else { $output .= $search_svg . ' ' . esc_html( $button_text ); } $output .= '</button>'; $output .= '</form>'; $output .= '</div>'; return $output; } add_shortcode( 'indaga_search', 'isf_search_shortcode' );

Formas de invocación en tus páginas

Una vez activado el plugin en WordPress, puedes invocar el buscador en cualquier parte de varias formas distintas )lo encontrarás en Ajustes -> Buscador Indaga):

  • Buscador clásico en línea:

    indaga_search

    (recordad, siempre los shortcodes siempre entre corchetes):

  • Formato minimalista con la lupa integrada:
    indaga_search layout="minimal" placeholder="Explorar en Indaga.net..."

  • Formato apilado para barras laterales o pie de página:

    indaga_search layout="stacked" button_text="Buscar en el archivo"

Con este pequeño plugin vuelvo a tener más control sobre el rendimiento del sitio, asegurándome de que el buscador responda al instante, es seguro y no añade sobrecarga al servidor.

Descargar plugin de mi cuenta de pCloud

PD. Actualizaré la página Control de plugins. En breve…

Hooks y actions de WordPress explicados con un plugin real: Mis Plugins

Valencia, 21/09/2026, G.B.
Ya he hablado varias veces por aquí de hooks, actions y filters en WordPress, pero siempre viene bien un ejemplo real y completo en vez de fragmentos sueltos. Así que voy a usar mi propio plugin Mis Plugins como caso práctico: es pequeño, no tiene dependencias externas, y toca casi todos los hooks que se usan en el día a día de un plugin.

Qué es un hook

Un hook es un punto del código de WordPress donde puedes «engancharte» para ejecutar tu propia función, sin tocar el núcleo. Hay dos tipos:

  • Actions: ejecutan tu función en un momento concreto (por ejemplo, cuando se activa un plugin, o cuando se carga el panel de administración). No devuelven nada, solo hacen algo.
  • Filters: reciben un valor, tu función lo modifica y lo devuelve. Se usan para transformar datos antes de que WordPress los use.

Mis Plugins usa sobre todo actions, así que empiezo por ahí.

register_activation_hook: al activar el plugin

Este hook se dispara una sola vez, justo cuando activas el plugin desde el panel. Lo uso para crear la tabla propia en la base de datos:

register_activation_hook( __FILE__, 'indaga_mp_activar' );

function indaga_mp_activar() {
    global $wpdb;
    $table_name = indaga_mp_table_name();
    $charset_collate = $wpdb->get_charset_collate();

    require_once ABSPATH . 'wp-admin/includes/upgrade.php';

    $sql = "CREATE TABLE {$table_name} (
        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
        fecha DATE NOT NULL,
        nombre_plugin VARCHAR(191) NOT NULL,
        PRIMARY KEY (id)
    ) {$charset_collate};";

    dbDelta( $sql );
}

La función dbDelta es la forma segura de crear o actualizar tablas propias: compara la estructura que le pasas con la que ya existe y hace los cambios necesarios sin borrar datos.

plugins_loaded: comprobar la versión en cada carga

register_activation_hook solo se dispara al activar, así que si subo una versión nueva del plugin con columnas añadidas, necesito otro punto de enganche que se ejecute siempre. Para eso uso plugins_loaded, comparando un número de versión guardado en las opciones:

add_action( 'plugins_loaded', 'indaga_mp_comprobar_version_bd' );

function indaga_mp_comprobar_version_bd() {
    if ( get_option( 'indaga_mp_db_version' ) !== INDAGA_MP_DB_VERSION ) {
        indaga_mp_activar();
    }
}

Así, cuando subí la versión 2.0.0 con los campos de autor y tags, la tabla se actualizó sola en cuanto WordPress cargó los plugins, sin que el usuario tuviera que desactivar y reactivar nada.

admin_menu: añadir la página al panel

Este hook se dispara mientras WordPress construye el menú de administración. Es el sitio correcto para registrar una nueva pantalla:

add_action( 'admin_menu', 'indaga_mp_menu_admin' );

function indaga_mp_menu_admin() {
    add_menu_page(
        'Mis Plugins',
        'Mis Plugins',
        'manage_options',
        INDAGA_MP_SLUG,
        'indaga_mp_render_pagina_admin',
        'dashicons-admin-plugins',
        80
    );
}

admin_init: procesar el formulario, nunca en el callback del menú

Este es el hook con el que más cuidado tengo, porque un error aquí ya me ha dado algún quebradero de cabeza en otros plugins: procesar un formulario POST dentro de la función que pinta la página de administración puede dejar la pantalla congelada. La solución es mover ese procesamiento a admin_init, comprobando primero en qué pantalla estamos:

add_action( 'admin_init', 'indaga_mp_procesar_admin_init' );

function indaga_mp_procesar_admin_init() {
    if ( ! isset( $_GET['page'] ) || INDAGA_MP_SLUG !== $_GET['page'] ) {
        return;
    }
    if ( ! current_user_can( 'manage_options' ) ) {
        return;
    }
    // Aquí se valida el nonce y se guarda o borra el registro.
}

admin_enqueue_scripts: cargar estilos solo donde tocan

Para no meter CSS en todas las pantallas del panel, engancho los estilos a este hook y compruebo en qué página estoy antes de cargarlos:

add_action( 'admin_enqueue_scripts', 'indaga_mp_admin_estilos' );

function indaga_mp_admin_estilos( $hook ) {
    if ( strpos( (string) $hook, INDAGA_MP_SLUG ) === false ) {
        return;
    }
    wp_register_style( 'indaga-mp-admin', false );
    wp_enqueue_style( 'indaga-mp-admin' );
    wp_add_inline_style( 'indaga-mp-admin', '/* CSS aquí */' );
}

add_shortcode: mostrarlo en el front end

No es técnicamente un action ni un filter, pero funciona con la misma lógica de enganche: le dices a WordPress que, cuando encuentre el shortcode mis_plugins (entre brackets, que si no se ejecuta aquí)  en el contenido, ejecute tu función y sustituya el shortcode por lo que devuelva:

add_shortcode( 'mis_plugins', 'indaga_mp_shortcode' );

function indaga_mp_shortcode( $atts ) {
    $atts = shortcode_atts( array(
        'por_pagina' => 10,
    ), $atts, 'mis_plugins' );

    // Consulta a la base de datos y devuelve el HTML con ob_start()/ob_get_clean().
}

Un detalle interesante para quien quiera profundizar en filters: shortcode_atts, por dentro, aplica el filter shortcode_atts_mis_plugins, que permite a otro plugin modificar los atributos por defecto sin tocar mi código. Es un buen ejemplo de cómo WordPress deja puertas abiertas incluso en funciones tan pequeñas como esta.

Resumen

Con seis hooks (uno de activación, uno de carga, tres de administración y un shortcode) queda montado un plugin completo, con su propia tabla, su formulario y su vista en el front end. Si quieres verlo funcionando, tienes el listado en Control de plugins, con el catálogo de todos los que he ido publicando aquí.

Un plugin propio de FAQs con lógica condicional para WordPress

Hace poco os contaba cómo monté Comandos Linux, un plugin propio con su tabla, su panel y su shortcode. Esta vez le tocaba a algo que llevaba tiempo queriendo hacer: una sección de preguntas frecuentes con lógica condicional, sin depender de un plugin de formularios de terceros

Valencia, 12/09/2026, G.B.
En un proyecto para una intranet usé Fluent Forms para montar unas FAQs con lógica condicional: el usuario elegía un tema y solo veía las preguntas de ese tema. Funciona muy bien, pero es una herramienta pensada para formularios complejos, y para una sección de FAQs en Indaga.net me sobraba casi todo. Así que hice lo de siempre aquí: quedarme solo con la pieza que necesito y montarla yo mismo.

El planteamiento

La «lógica condicional» que quería no es tan sofisticada como la de un constructor de formularios: no necesito árboles de decisión ni preguntas que dependan de respuestas anteriores. Lo que necesito es más simple y, para una FAQ, más que suficiente: el usuario elige una categoría (por ejemplo «WordPress» o «Facturación») y solo se muestran las preguntas de esa categoría. El resto permanece oculto hasta que cambia de categoría.

Con eso claro, el plugin se reduce a tres piezas: una tabla propia en la base de datos, un panel de administración para gestionar las preguntas, y un shortcode que pinta el selector y las preguntas en el front end.

La tabla

Cada pregunta se guarda con seis campos: categoria, pregunta, respuesta (admite HTML básico, para poder meter enlaces o negritas en la respuesta), orden (para decidir en qué posición aparece dentro de su categoría) y activo (para poder desactivar una pregunta sin borrarla). Nada de post types personalizados ni de meta fields sueltos: una tabla con dbDelta() en la activación, como en el resto de plugins que he ido publicando aquí.

El panel de administración

El formulario de alta y edición es sencillo: categoría (con autocompletado a partir de las categorías ya creadas, para no acabar con «Facturación», «facturacion» y «Facturación » conviviendo en la misma tabla), pregunta, respuesta con el editor visual de WordPress, orden y una casilla de activo. El listado de abajo tiene buscador por texto, filtro por categoría y paginación, para cuando la lista crezca y no quepa cómodamente en una sola pantalla.

Aquí me llevé un buen susto durante las pruebas: el formulario, al guardar, se quedaba la página a medio cargar, como congelada. La causa no tenía nada que ver con la base de datos ni con archivos grandes, sino con el orden de ejecución de WordPress: el guardado del formulario y la redirección posterior se ejecutaban dentro de la misma función que pinta el HTML del panel, y para ese punto WordPress ya ha enviado las cabeceras HTTP. Una redirección que llega tarde, simplemente, no llega. La solución fue mover todo el procesamiento del formulario a un enganche admin_init, que se ejecuta antes de que se imprima nada, y que la función de renderizado se limite a pintar. Detalle poco vistoso, pero de esos que conviene tener en la cabeza si escribís vuestros propios paneles de administración.

La lógica condicional, en el front end

Aquí está el motivo del plugin. Al insertar el shortcode, el plugin agrupa las preguntas por categoría y, si hay más de una, añade automáticamente unos botones de selección arriba del todo, empezando por un botón «Todas». Al pulsar sobre una categoría, un poco de JavaScript sin dependencias oculta el resto de preguntas y muestra solo las de esa categoría, sin recargar la página. Si solo existe una categoría, o si el shortcode se usa fijando una en concreto, el selector directamente no aparece: no tiene sentido mostrar un filtro con una sola opción.

A eso se suma un buscador de texto libre y una paginación, ambos también resueltos en el cliente: no hace falta ir a la base de datos cada vez que alguien escribe una letra o cambia de página, porque una sección de FAQs no suele tener miles de registros. Las preguntas se despliegan en acordeón al hacer clic, así que la página no se llena de texto de golpe.

El shortcode

Para insertarlo en cualquier entrada o página basta con:

indaga_faqs (entre corchetes, [ ])

Y admite varios parámetros para adaptarlo a cada sitio:

indaga_faqs categoria=»Facturación»

Muestra solo esa categoría, sin selector. Útil si queréis una página de FAQs dedicada a un único tema.

indaga_faqs mostrar_selector=»no»

Lista todas las categorías seguidas, sin botones de filtro.

indaga_faqs mostrar_buscador=»no»

Oculta la barra de búsqueda superior.

indaga_faqs por_pagina=»5″

Cambia cuántas preguntas se muestran por página (10 por defecto).

Porqué así

La misma filosofía de siempre: identificar qué parte de una herramienta de terceros es la que de verdad necesito, y quedarme solo con eso, escrito a medida, ligero y sin dependencias que arrastrar en cada actualización. Para una intranet con formularios complejos, Fluent Forms tiene todo el sentido. Para una sección de FAQs en un blog, con esto sobra.

Descargar plugin de mi cuenta de pCloud

Míralo en acción: Indaga FAQs

Plugin para importar CSV o Excel a cualquier tabla de WordPress

Un plugin propio para convertir un CSV o un Excel en filas listas para cualquier tabla de la base de datos, con dos modos: inserción directa o script SQL descargable

Valencia, 10/09/2026, G.B.
Cada vez que necesito poblar una tabla propia —la de comandos Linux, la de frases aleatorias, la de FAQs— acababa haciendo lo mismo: montar el INSERT INTO línea a línea a partir de un listado o insertarlo manualmente en el panel de administración del plugin. Pero si necesito crear muchos registros a la vez, la cosa se hacía algo pesada. Así que he preparado un plugin genérico para no repetir ese trabajo: Importador CSV/Excel a Tabla, que sube un archivo y lo convierte en inserciones para la tabla que le indiques.

Qué hace

El plugin añade una página en Herramientas → Importador CSV/Excel. Desde ahí se sube un archivo .csv o .xlsx y se indican tres cosas:

  • El nombre real de la tabla destino, con su prefijo (por ejemplo, wp_fa_frases)
  • Las columnas destino, separadas por comas y en el mismo orden que las columnas del archivo (sin incluir id si es autoincremental)
  • Si la primera fila es cabecera, para que se ignore

Y después hay que elegir el modo:

  • Insertar directamente en la base de datos, fila a fila, con un resumen de cuántas se han insertado, cuántas han fallado y cuántas se han descartado por no tener suficientes columnas
  • Generar un script SQL descargable, con el INSERT INTO completo, para revisarlo y ejecutarlo yo mismo en phpMyAdmin en vez de dejar que el plugin toque la tabla directamente

Cómo lee los archivos

Con el CSV no hay mucho misterio: se parsea con fgetcsv(), detectando automáticamente si el delimitador es coma, punto y coma o tabulador, y respetando el BOM UTF-8 que añaden Excel o LibreCalc al exportar.

El XLSX es más delicado porque, en el fondo, es un ZIP con varios XML dentro. En vez de tirar de una librería de terceros como PhpSpreadsheet, lo abro con ZipArchive y leo directamente sharedStrings.xml (donde Excel guarda los textos) y sheet1.xml (donde guarda la estructura de celdas) con SimpleXML. Las dos son extensiones nativas de PHP, así que sigo sin añadir ninguna dependencia externa al sitio.

Esto tiene límites que asumo: solo lee la primera hoja del libro, no soporta celdas combinadas ni fórmulas, y las fechas se leen como el valor crudo de la celda, sin formatear. Para el uso que le voy a dar —listados sencillos de una sola hoja— es más que suficiente.

Por qué así

El plugin sigue la misma arquitectura que el resto de los que tengo en marcha: procesamiento del formulario enganchado a admin_init en lugar de dentro del callback del menú (para evitar el típico problema de cabeceras ya enviadas), patrón POST/Redirect/GET para no reenviar el formulario al recargar, y comprobación de nonce y de capacidad manage_options antes de tocar nada. debo comprobar si eso ralentiza el «pintado» del escritorio de WordPress.

La idea de fondo es la misma de siempre: cubrir una necesidad muy concreta —pasar una hoja de cálculo a SQL— con el menor peso posible y sin depender de plugins de terceros que no controlo.

Qué me falta por probar

Lo acabo de instalar y todavía no lo he puesto a prueba con datos reales: antes tengo que revisar la estructura de las tablas con las que voy a trabajar para asegurarme de que las columnas coinciden. Si funciona bien, es probable que añada una vista previa de las primeras filas del archivo antes de confirmar la importación, para pillar errores de columnas antes de que lleguen a la base de datos.

Descargar plugin de mi cuenta de pCloud