Un plugin de WordPress es la forma correcta de añadir funcionalidad a una web sin tocar el núcleo ni el tema. Con un archivo PHP, una cabecera y unos cuantos hooks ya tienes un plugin funcionando. En esta guía construimos uno completo desde cero: shortcode, página de ajustes, seguridad, internacionalización y empaquetado final.
Está pensada para WordPress 7.x y PHP 8.x. Todo el código que verás se puede copiar y probar en una instalación local o de staging.
Resumen rápido
- Un plugin es una carpeta dentro de
wp-content/plugins/con al menos un archivo PHP y una cabecera de comentario. - La cabecera (
Plugin Name,Version,Text Domain…) es lo que WordPress lee para listarlo en el panel. - Todo el código se engancha con acciones y filtros: no se ejecuta «suelto».
- Valida siempre:
ABSPATH, capacidades, nonces, sanitización al guardar y escaping al mostrar. - Guarda los datos con la Settings API o con tablas propias creadas con
dbDelta(). - Empaqueta en ZIP, pruébalo en staging y documenta el
readme.txt.
1. Qué es un plugin y cuándo crearlo
Un plugin es un paquete de código que se engancha al ciclo de vida de WordPress para añadir o modificar funcionalidad. Se carga después del núcleo y antes del tema, así que puede intervenir en casi cualquier parte de la ejecución: contenido, panel, correos, API REST, tareas programadas o la base de datos.
La regla es sencilla: si el cambio debe sobrevivir a un cambio de tema, va en un plugin. Si es puramente visual y depende del tema, va en un tema hijo. Y si es una modificación del núcleo, casi siempre hay un hook para hacerlo sin tocarlo.
| Necesidad | Dónde va |
|---|---|
| Añadir un tipo de contenido, un shortcode o una integración | Plugin propio |
| Cambiar estilos, plantillas o layout | Tema hijo |
| Ajuste pequeño y atado al tema | functions.php del tema hijo |
| Cambiar comportamiento del núcleo | Plugin con hooks, nunca editando wp-includes/ |
| Funcionalidad crítica que no debe desactivarse | Mu-plugin en wp-content/mu-plugins/ |
Versión: esta guía se centra en WordPress 7.x y PHP 8.0 o superior. Las APIs que usamos (Settings API, Shortcode API, REST API, i18n) son estables desde hace años y siguen siendo el estándar.
2. Requisitos y entorno de desarrollo
Antes de escribir la primera línea necesitas un entorno donde probar sin miedo. Puedes usar WordPress local con LocalWP, Docker, XAMPP o un staging del hosting.
- WordPress 7.x actualizado.
- PHP 8.0 o superior, con
WP_DEBUGyWP_DEBUG_LOGactivos. - Un editor con soporte PHP (VS Code, PhpStorm) y la extensión de estándares de WordPress.
- WP-CLI para activar, desactivar y probar el plugin desde la terminal.
- Un repositorio Git: aunque el plugin sea pequeño, el historial te salva.
// wp-config.php (solo en desarrollo)
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
define( 'SCRIPT_DEBUG', true );
Los avisos PHP aparecerán en wp-content/debug.log. Si tu plugin genera un warning, lo verás ahí antes de que lo vea un visitante.
3. Estructura mínima de un plugin
Un plugin puede ser un único archivo PHP dentro de wp-content/plugins/. Para algo serio, conviene una carpeta con esta estructura:
wp-content/plugins/solusen-avisos/
├── solusen-avisos.php # Archivo principal con la cabecera
├── includes/
│ ├── shortcode.php # Lógica del shortcode
│ └── settings.php # Página de ajustes
├── assets/
│ └── aviso.css # Estilos del front
├── languages/
│ └── solusen-avisos-es_ES.po
├── readme.txt # Documentación y changelog
└── uninstall.php # Limpieza al desinstalar
Las carpetas no son obligatorias, pero separar responsabilidades evita que el archivo principal se convierta en un monolito de mil líneas. El nombre de la carpeta y del archivo principal deben coincidir para que el plugin se pueda distribuir limpiamente.
Convención de nombres: usa un prefijo único (solusen_avisos_, Solusen_Avisos) en funciones, clases, opciones y hooks. Es la mejor defensa contra colisiones con otros plugins.
4. La cabecera del plugin
WordPress no ejecuta el plugin hasta que encuentra la cabecera en el archivo principal. Es un comentario con campos fijos que el panel lee para mostrar nombre, versión, autor y compatibilidad.
<?php
/**
* Plugin Name: Solusen Avisos
* Plugin URI: https://solusen.es/
* Description: Añade un shortcode de avisos configurables y una página de ajustes.
* Version: 1.0.0
* Requires at least: 6.5
* Requires PHP: 8.0
* Author: Solusen
* Author URI: https://solusen.es/
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: solusen-avisos
* Domain Path: /languages
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'SOLUSEN_AVISOS_VERSION', '1.0.0' );
define( 'SOLUSEN_AVISOS_FILE', __FILE__ );
define( 'SOLUSEN_AVISOS_DIR', plugin_dir_path( __FILE__ ) );
require_once SOLUSEN_AVISOS_DIR . 'includes/shortcode.php';
require_once SOLUSEN_AVISOS_DIR . 'includes/settings.php';
La comprobación de ABSPATH impide que alguien llame al archivo directamente por URL y ejecute código sin cargar WordPress. Es obligatoria en todos los archivos PHP del plugin, no solo en el principal.
Campos de cabecera más importantes
| Campo | Para qué sirve |
|---|---|
Plugin Name | Nombre visible en el listado de plugins. Obligatorio. |
Version | Versión actual. Se usa para caché de assets y actualizaciones. |
Requires at least / Requires PHP | Requisitos mínimos; WordPress avisa si no se cumplen. |
Text Domain / Domain Path | Dominio de traducción y carpeta de los .po/.mo. |
License | Licencia del código; GPL-2.0-or-later es lo habitual en WordPress. |
5. Activación, desactivación y desinstalación
WordPress ofrece hooks para reaccionar al ciclo de vida del plugin. Son el lugar correcto para crear opciones por defecto, tablas o tareas programadas, y para limpiar cuando el plugin se desinstala.
Al activar
register_activation_hook( __FILE__, 'solusen_avisos_activar' );
function solusen_avisos_activar() {
if ( false === get_option( 'solusen_avisos_opciones' ) ) {
add_option( 'solusen_avisos_opciones', array(
'mensaje' => 'Bienvenido a nuestra web.',
'tipo' => 'info',
) );
}
if ( ! wp_next_scheduled( 'solusen_avisos_cron' ) ) {
wp_schedule_event( time(), 'daily', 'solusen_avisos_cron' );
}
}
La comprobación con false === get_option() evita sobrescribir los ajustes si el plugin se reactiva. Es un error muy común añadir la opción sin comprobar y borrar la configuración del usuario.
Al desactivar
register_deactivation_hook( __FILE__, 'solusen_avisos_desactivar' );
function solusen_avisos_desactivar() {
wp_clear_scheduled_hook( 'solusen_avisos_cron' );
}
La desactivación debe ser reversible: no borres datos del usuario, solo detén lo que hayas programado.
Al desinstalar
El archivo uninstall.php se ejecuta cuando el usuario borra el plugin. Aquí sí se eliminan opciones y tablas, siempre con comprobación previa.
<?php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
delete_option( 'solusen_avisos_opciones' );
global $wpdb;
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}solusen_avisos" );
Cuidado: borrar datos en la desinstalación es irreversible. Si el plugin guarda contenido que el usuario puede querer conservar, ofrécele una opción para decidir si eliminar los datos al desinstalar.
6. Hooks: acciones y filtros
Los hooks son el mecanismo de extensión de WordPress. Hay dos tipos:
- Acciones (
add_action): ejecutan código en un momento concreto, por ejemploinit,admin_menuowp_enqueue_scripts. - Filtros (
add_filter): reciben un valor, lo modifican y lo devuelven, por ejemplothe_contentowp_mail_from.
// Acción: se ejecuta al cargar el front.
add_action( 'wp_footer', 'solusen_avisos_pie' );
function solusen_avisos_pie() {
echo '<!-- Solusen Avisos activo -->';
}
// Filtro: modifica el contenido de las entradas.
add_filter( 'the_content', 'solusen_avisos_anadir_al_contenido' );
function solusen_avisos_anadir_al_contenido( $contenido ) {
if ( ! is_singular( 'post' ) || ! in_the_loop() || ! is_main_query() ) {
return $contenido;
}
return $contenido . do_shortcode( '[solusen_aviso]' );
}
El tercer argumento de add_action/add_filter es la prioridad (10 por defecto) y el cuarto el número de argumentos. Si dos plugins modifican lo mismo, la prioridad decide el orden.
7. Crear un shortcode
Un shortcode es una etiqueta corta como [solusen_aviso] que el usuario puede insertar en una entrada, una página o un bloque de shortcode. Se registra con add_shortcode() y devuelve HTML, nunca lo imprime directamente.
<?php
// includes/shortcode.php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_shortcode( 'solusen_aviso', 'solusen_avisos_shortcode' );
function solusen_avisos_shortcode( $atts ) {
$opciones = get_option( 'solusen_avisos_opciones', array() );
$atts = shortcode_atts(
array(
'tipo' => $opciones['tipo'] ?? 'info',
),
$atts,
'solusen_aviso'
);
$mensaje = $opciones['mensaje'] ?? '';
if ( '' === $mensaje ) {
return '';
}
return sprintf(
'<div class="solusen-aviso solusen-aviso--%s">%s</div>',
esc_attr( $atts['tipo'] ),
esc_html( $mensaje )
);
}
El uso sería [solusen_aviso] o [solusen_aviso tipo="aviso"]. Fíjate en dos detalles clave: shortcode_atts() define valores por defecto y esc_html()/esc_attr() protegen la salida.
8. Página de ajustes en el panel
La Settings API conecta un formulario del panel con la tabla wp_options. WordPress se encarga del guardado, los nonces y los avisos de confirmación si usas settings_fields() y do_settings_sections().
<?php
// includes/settings.php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_action( 'admin_menu', 'solusen_avisos_menu' );
function solusen_avisos_menu() {
add_options_page(
__( 'Solusen Avisos', 'solusen-avisos' ),
__( 'Solusen Avisos', 'solusen-avisos' ),
'manage_options',
'solusen-avisos',
'solusen_avisos_pagina'
);
}
add_action( 'admin_init', 'solusen_avisos_registrar_ajustes' );
function solusen_avisos_registrar_ajustes() {
register_setting( 'solusen_avisos_grupo', 'solusen_avisos_opciones', array(
'type' => 'array',
'sanitize_callback' => 'solusen_avisos_sanitizar',
'default' => array(),
) );
add_settings_section(
'solusen_avisos_seccion',
__( 'Ajustes del aviso', 'solusen-avisos' ),
'__return_false',
'solusen-avisos'
);
add_settings_field(
'solusen_avisos_mensaje',
__( 'Mensaje', 'solusen-avisos' ),
'solusen_avisos_campo_mensaje',
'solusen-avisos',
'solusen_avisos_seccion'
);
add_settings_field(
'solusen_avisos_tipo',
__( 'Tipo', 'solusen-avisos' ),
'solusen_avisos_campo_tipo',
'solusen-avisos',
'solusen_avisos_seccion'
);
}
function solusen_avisos_sanitizar( $entrada ) {
$tipos = array( 'info', 'exito', 'aviso' );
return array(
'mensaje' => sanitize_text_field( $entrada['mensaje'] ?? '' ),
'tipo' => in_array( $entrada['tipo'] ?? '', $tipos, true ) ? $entrada['tipo'] : 'info',
);
}
Y la función que pinta el formulario:
function solusen_avisos_campo_mensaje() {
$opciones = get_option( 'solusen_avisos_opciones', array() );
printf(
'<input type="text" class="regular-text" name="solusen_avisos_opciones[mensaje]" value="%s">',
esc_attr( $opciones['mensaje'] ?? '' )
);
}
function solusen_avisos_campo_tipo() {
$opciones = get_option( 'solusen_avisos_opciones', array() );
$actual = $opciones['tipo'] ?? 'info';
$tipos = array( 'info' => 'Info', 'exito' => 'Éxito', 'aviso' => 'Aviso' );
echo '<select name="solusen_avisos_opciones[tipo]">';
foreach ( $tipos as $valor => $etiqueta ) {
printf(
'<option value="%s" %s>%s</option>',
esc_attr( $valor ),
selected( $actual, $valor, false ),
esc_html( $etiqueta )
);
}
echo '</select>';
}
function solusen_avisos_pagina() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<form action="options.php" method="post">
<?php
settings_fields( 'solusen_avisos_grupo' );
do_settings_sections( 'solusen-avisos' );
submit_button( __( 'Guardar cambios', 'solusen-avisos' ) );
?>
</form>
</div>
<?php
}
No reinventes el formulario: si usas la Settings API, WordPress añade el nonce, comprueba capacidades y muestra el aviso «Ajustes guardados». Si haces el guardado a mano con $_POST, tendrás que implementar todo eso tú.
9. Encolar CSS y JavaScript
Nunca enlaces CSS o JS con etiquetas <link> o <script> sueltas. Se encolan con wp_enqueue_style() y wp_enqueue_script() para que WordPress gestione dependencias, versiones y orden de carga.
add_action( 'wp_enqueue_scripts', 'solusen_avisos_assets' );
function solusen_avisos_assets() {
wp_enqueue_style(
'solusen-avisos',
plugin_dir_url( __FILE__ ) . 'assets/aviso.css',
array(),
SOLUSEN_AVISOS_VERSION
);
}
add_action( 'admin_enqueue_scripts', 'solusen_avisos_admin_assets' );
function solusen_avisos_admin_assets( $hook ) {
if ( 'settings_page_solusen-avisos' !== $hook ) {
return;
}
wp_enqueue_script(
'solusen-avisos-admin',
plugin_dir_url( __FILE__ ) . 'assets/admin.js',
array( 'jquery' ),
SOLUSEN_AVISOS_VERSION,
true
);
}
Pasar la versión como cuarto argumento hace que el navegador descargue el archivo nuevo cuando subes la versión del plugin. En el panel, comprueba siempre $hook para cargar assets solo en tu página y no en todo el admin.
10. Seguridad: nonces, capacidades y escaping
Un plugin mal escrito es una puerta abierta. Estas cuatro reglas cubren la mayoría de los fallos:
- Comprueba la capacidad antes de permitir una acción:
current_user_can( 'manage_options' ). - Verifica el nonce en formularios y peticiones AJAX:
check_admin_referer()owp_verify_nonce(). - Sanitiza la entrada al guardar:
sanitize_text_field(),absint(),sanitize_email(),wp_kses_post(). - Escapa la salida al mostrar:
esc_html(),esc_attr(),esc_url(),wp_kses_post().
// Formulario con nonce.
<form method="post">
<?php wp_nonce_field( 'solusen_avisos_accion', 'solusen_avisos_nonce' ); ?>
<input type="text" name="mensaje">
<?php submit_button(); ?>
</form>
<?php
// Procesar el envío de forma segura.
if ( isset( $_POST['solusen_avisos_nonce'] ) ) {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'No tienes permisos.', 'solusen-avisos' ) );
}
check_admin_referer( 'solusen_avisos_accion', 'solusen_avisos_nonce' );
$mensaje = sanitize_text_field( wp_unslash( $_POST['mensaje'] ?? '' ) );
update_option( 'solusen_avisos_opciones', array( 'mensaje' => $mensaje, 'tipo' => 'info' ) );
}
Sanitizar al guardar y escapar al imprimir no son alternativas: son capas distintas. Sanitizar protege la base de datos; escapar protege el HTML en el momento de mostrarlo, aunque el dato venga de otro sitio.
| Dato | Al guardar | Al mostrar |
|---|---|---|
| Texto plano | sanitize_text_field() | esc_html() |
| HTML de entrada | wp_kses_post() | wp_kses_post() |
| URL | esc_url_raw() | esc_url() |
sanitize_email() | esc_html() | |
| Entero | absint() | absint() |
| Atributos / clases | sanitize_html_class() | esc_attr() |
Nunca confíes en $_GET ni en $_POST: usa siempre wp_unslash() y la función de sanitización adecuada. Y no uses extract() ni variables globales sin prefijo.
11. Guardar datos: options y tablas propias
Para ajustes pequeños, get_option()/update_option() es suficiente y está cacheado. Si necesitas muchos registros, relaciones o consultas, crea una tabla propia con dbDelta().
register_activation_hook( __FILE__, 'solusen_avisos_crear_tabla' );
function solusen_avisos_crear_tabla() {
global $wpdb;
$tabla = $wpdb->prefix . 'solusen_avisos';
$charset = $wpdb->get_charset_collate();
$sql = "CREATE TABLE {$tabla} (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
mensaje text NOT NULL,
creado datetime NOT NULL,
PRIMARY KEY (id)
) {$charset};";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
}
dbDelta() compara la definición con la tabla existente y aplica los cambios. Respeta su formato: dos espacios tras PRIMARY KEY, una columna por línea y tipos en minúsculas. Cuando cambies el esquema, sube la versión y ejecuta la migración una sola vez.
// Ejemplo de consulta con $wpdb y prepare().
global $wpdb;
$tabla = $wpdb->prefix . 'solusen_avisos';
$filas = $wpdb->get_results(
$wpdb->prepare( "SELECT * FROM {$tabla} WHERE id > %d ORDER BY creado DESC", 0 )
);
Usa siempre $wpdb->prepare() con marcadores (%d, %s, %f) cuando la consulta lleve variables. Concatenar directamente es una inyección SQL esperando a ocurrir.
12. Internacionalización
Si el plugin va a usarse en varios idiomas, envuelve todos los textos en funciones de traducción y carga el textdomain. El Text Domain de la cabecera debe coincidir con el que uses en el código.
add_action( 'init', 'solusen_avisos_cargar_textdomain' );
function solusen_avisos_cargar_textdomain() {
load_plugin_textdomain(
'solusen-avisos',
false,
dirname( plugin_basename( __FILE__ ) ) . '/languages'
);
}
// Uso en el código:
echo esc_html__( 'Guardar cambios', 'solusen-avisos' );
| Función | Cuándo usarla |
|---|---|
__() | Devuelve el texto traducido para usarlo en variables. |
_e() | Imprime el texto traducido directamente. |
esc_html__() | Devuelve y escapa para HTML. La opción más segura. |
_n() | Singular/plural según un número. |
esc_attr__() | Devuelve y escapa para atributos. |
Puedes generar el archivo .pot con WP-CLI:
wp i18n make-pot . languages/solusen-avisos.pot
13. REST API y AJAX
La REST API es la vía moderna para exponer datos del plugin. Registra rutas en rest_api_init y define siempre un permission_callback: si la ruta es pública, usa __return_true de forma consciente.
add_action( 'rest_api_init', 'solusen_avisos_rest' );
function solusen_avisos_rest() {
register_rest_route( 'solusen/v1', '/aviso', array(
'methods' => 'GET',
'callback' => 'solusen_avisos_rest_leer',
'permission_callback' => '__return_true',
) );
}
function solusen_avisos_rest_leer() {
$opciones = get_option( 'solusen_avisos_opciones', array() );
return rest_ensure_response( array(
'mensaje' => $opciones['mensaje'] ?? '',
'tipo' => $opciones['tipo'] ?? 'info',
) );
}
La ruta quedaría en /wp-json/solusen/v1/aviso. Para AJAX clásico, registra la acción con wp_ajax_ (usuarios logueados) y wp_ajax_nopriv_ (visitantes), y valida el nonce con check_ajax_referer().
14. Empaquetar, probar y publicar
Cuando el plugin funcione en local, toca empaquetarlo y probarlo en un entorno real. Un ZIP mal montado es la causa habitual de que un plugin no aparezca o falle al activarse.
Checklist antes de publicar
- El ZIP contiene una única carpeta con el nombre del plugin y el archivo principal dentro.
- No incluye
node_modules/,.git/,vendor/innecesario ni archivos de desarrollo. - Se activa sin errores ni warnings con
WP_DEBUGactivo. - Se desactiva y se desinstala limpio, sin dejar tablas ni opciones huérfanas.
- Funciona en las versiones mínimas declaradas (WordPress y PHP).
- Los textos están listos para traducir y el
readme.txtestá completo. - No hay rutas absolutas, credenciales ni claves API en el código.
- El formulario de ajustes valida capacidades y nonce.
# Crear el ZIP desde la carpeta de plugins
cd wp-content/plugins
zip -r solusen-avisos.zip solusen-avisos -x "*.git*" -x "*node_modules*"
# Activar y comprobar por WP-CLI
wp plugin activate solusen-avisos
wp plugin list
Si vas a publicarlo en el repositorio oficial de WordPress.org, necesitas una cuenta, un readme.txt válido, la licencia GPL y pasar la revisión. Para uso privado o de cliente, basta con entregar el ZIP y documentar la instalación.
15. Buenas prácticas y errores comunes
La mayoría de los problemas de un plugin se evitan con disciplina desde el primer archivo:
- Prefija todo (funciones, clases, opciones, hooks, tablas) para evitar colisiones.
- Comprueba
ABSPATHen cada archivo PHP. - No edites nunca el núcleo ni plugins de terceros: usa hooks.
- Separa lógica, vistas y assets en archivos distintos.
- Sanitiza al guardar y escapa al mostrar, siempre.
- Usa nonces y capacidades en cada acción del panel o AJAX.
- No borres datos del usuario en la desactivación; reserva la limpieza para la desinstalación.
- Versiona el plugin y usa esa versión en los assets encolados.
- Documenta el código y mantén un
readme.txtcon changelog. - Prueba en staging y con
WP_DEBUGantes de producción.
| Síntoma | Causa probable | Solución |
|---|---|---|
| El plugin no aparece en el listado | Falta la cabecera o el archivo no está en la carpeta correcta | Revisa Plugin Name y la ruta wp-content/plugins/. |
| Error 500 al activar | Error de sintaxis PHP o dependencia no cargada | Mira debug.log, corrige el error y revisa los require_once. |
| El shortcode se muestra como texto | El shortcode no está registrado o el contenido no pasa por do_shortcode() | Verifica add_shortcode() y el contexto de salida. |
| Los ajustes no se guardan | register_setting() mal configurado o falta el name del campo | Revisa el grupo, la opción y los nombres de los inputs. |
| El panel dice que no tienes permisos | Capacidad incorrecta o falta el nonce | Usa manage_options y settings_fields(). |
| El CSS/JS no se actualiza | Caché del navegador o versión fija | Sube la versión del plugin y púrgala caché. |
| Colisión con otro plugin | Funciones o hooks sin prefijo | Renombra con prefijo único y revisa el orden de prioridad. |
| Datos huérfanos tras desinstalar | No hay uninstall.php | Añade limpieza condicionada a WP_UNINSTALL_PLUGIN. |
La seguridad del plugin también afecta a la web completa. Si sospechas que una extensión comprometida ha abierto una puerta, revisa el análisis técnico de intrusiones en WordPress y la guía de ataques habituales.
16. Preguntas frecuentes sobre plugins de WordPress
¿Cuál es el archivo mínimo para crear un plugin?
Un único archivo PHP dentro de wp-content/plugins/ con una cabecera de comentario que incluya al menos Plugin Name. A partir de ahí puedes crecer con carpetas y más archivos.
¿Dónde se guardan las opciones de un plugin?
Con get_option()/update_option() se guardan en la tabla wp_options. Si necesitas muchos registros o consultas, crea una tabla propia con dbDelta() en la activación.
¿Qué diferencia hay entre acción y filtro?
Una acción ejecuta código en un punto concreto y no devuelve nada. Un filtro recibe un valor, lo modifica y lo devuelve para que WordPress siga usándolo. Ambos se registran con prioridad y número de argumentos.
¿Puedo crear un plugin sin tocar código?
Para cambios simples existen plugins de snippets, pero si la funcionalidad debe sobrevivir a cambios de tema y ser mantenible, lo correcto es un plugin propio o un mu-plugin.
¿Cómo evito colisiones con otros plugins?
Usa un prefijo único en funciones, clases, opciones, hooks y tablas. Además, envuelve el código en clases o namespaces y evita declarar funciones globales con nombres genéricos.
¿Qué es un mu-plugin?
Es un plugin obligatorio que vive en wp-content/mu-plugins/ y se carga automáticamente. No se puede desactivar desde el panel y se usa para funcionalidad crítica.
¿Cómo pruebo el plugin sin romper la web?
Usa un entorno local o de staging con WP_DEBUG y WP_DEBUG_LOG activos. Prueba activación, uso, desactivación y desinstalación antes de tocar producción.
¿Cómo publico el plugin en WordPress.org?
Necesitas una cuenta, un readme.txt válido, licencia GPL, código limpio y pasar la revisión del equipo de plugins. Para uso privado basta con distribuir el ZIP.
17. Conclusión
Crear un plugin de WordPress es más sencillo de lo que parece: un archivo con cabecera, unos hooks bien puestos y las APIs correctas. La diferencia entre un plugin que aguanta años y uno que se rompe en la primera actualización está en los detalles: prefijos, seguridad, sanitización, escaping y un ciclo de activación y desinstalación limpio.
Empieza pequeño, prueba en staging y añade complejidad solo cuando haga falta. Si tu web necesita una funcionalidad a medida, una integración o mantener un plugin existente, podemos ayudarte.
¿Necesitas un plugin a medida para tu WordPress?
Desarrollamos plugins propios, integraciones y mantenimiento para que tu web haga exactamente lo que necesitas sin depender de extensiones genéricas.
Documentación oficial de WordPress:
Plugin Handbook · Header requirements · Plugin security
