Meter código en functions.php del theme funciona hasta que cambias de theme y todo lo que construiste desaparece de un día para otro. Si la funcionalidad no depende del diseño — un shortcode, un formulario, un widget de catálogo — debería vivir en un plugin propio. Aquí construimos uno desde cero, enfocado en el front-end.
Estructura mínima de un plugin
Un plugin de WordPress es, como mínimo, una carpeta dentro de wp-content/plugins/ con un archivo PHP que tenga el comentario de cabecera que WordPress lee para mostrarlo en la lista de plugins.
<?php
/**
* Plugin Name: Mi Plugin Front
* Description: Plugin de ejemplo orientado al front-end.
* Version: 1.0.0
* Author: Carlos Moreno
* Text Domain: mi-plugin-front
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Salir si se accede directamente al archivo
}
define( 'MPF_VERSION', '1.0.0' );
define( 'MPF_PATH', plugin_dir_path( __FILE__ ) );
define( 'MPF_URL', plugin_dir_url( __FILE__ ) );El chequeo de ABSPATH es igual de importante que en PrestaShop o cualquier otro CMS PHP: sin él, cualquiera podría acceder al archivo directamente por URL y ver errores o, peor, ejecutar código fuera de contexto.
Mostrar contenido en el front con un shortcode
Un shortcode es la forma más simple de inyectar HTML dinámico en cualquier página o entrada sin tocar el theme. La función que registras debe devolver un string, nunca hacer echo directamente — si imprimes en pantalla, el contenido aparece en el lugar equivocado cuando WordPress procesa el contenido completo.
add_shortcode( 'mpf_banner', 'mpf_render_banner' );
function mpf_render_banner( $atts ) {
$atts = shortcode_atts(
[
'texto' => 'Envío gratis en compras superiores a $150.000',
'enlace' => '#',
],
$atts,
'mpf_banner'
);
ob_start();
?>
<div class="mpf-banner">
<a href="<?php echo esc_url( $atts['enlace'] ); ?>">
<?php echo esc_html( $atts['texto'] ); ?>
</a>
</div>
<?php
return ob_get_clean();
}Con eso, escribir [mpf_banner texto="Oferta de hoy"] en cualquier entrada o página ya renderiza el bloque. El uso de ob_start()/ob_get_clean() en vez de concatenar strings hace que el HTML sea legible cuando el bloque crece.
Cargar CSS y JS solo donde se necesitan
El error más común en plugins de front es encolar el CSS y el JS en todas las páginas del sitio, así no se usen. El hook correcto es wp_enqueue_scripts (nunca wp_head directamente), y conviene comprobar si el contenido realmente va a mostrarse antes de cargar nada.
add_action( 'wp_enqueue_scripts', 'mpf_register_assets' );
function mpf_register_assets() {
global $post;
if ( ! is_a( $post, 'WP_Post' ) || ! has_shortcode( $post->post_content, 'mpf_banner' ) ) {
return;
}
wp_enqueue_style(
'mpf-banner',
MPF_URL . 'assets/banner.css',
[],
MPF_VERSION
);
wp_enqueue_script(
'mpf-banner',
MPF_URL . 'assets/banner.js',
[],
MPF_VERSION,
true
);
}El cuarto parámetro de wp_enqueue_style/script (MPF_VERSION) no es decorativo: es lo que fuerza a los navegadores a descargar la versión nueva del archivo cuando subes una actualización. Si lo dejas fijo o vacío, los usuarios van a seguir viendo CSS viejo cacheado.
Pasar datos de PHP a JavaScript
Cuando el JS necesita algo que solo PHP conoce — la URL del admin-ajax, un nonce de seguridad, un texto traducido — la forma correcta es wp_localize_script, nunca imprimir variables JS sueltas con wp_head.
wp_localize_script( 'mpf-banner', 'mpfData', [
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'mpf_nonce' ),
] );// banner.js
document.addEventListener('DOMContentLoaded', function () {
var banner = document.querySelector('.mpf-banner a');
if (!banner) return;
banner.addEventListener('click', function () {
fetch(mpfData.ajaxUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'action=mpf_track_click&nonce=' + mpfData.nonce,
});
});
});El nonce no es opcional en ninguna petición admin-ajax que modifique algo. Sin él, cualquier sitio externo podría disparar la misma acción contra tu instalación de WordPress con una petición fabricada a mano.
Activación y desinstalación
Si el plugin crea una opción, una tabla o un rol, debe limpiarlo cuando se desinstala. register_activation_hook corre una sola vez al activar; register_uninstall_hook corre cuando el usuario lo elimina desde el panel, no simplemente al desactivarlo.
register_activation_hook( __FILE__, function () {
add_option( 'mpf_version', MPF_VERSION );
} );
register_uninstall_hook( __FILE__, 'mpf_uninstall' );
function mpf_uninstall() {
delete_option( 'mpf_version' );
}Buenas prácticas
- Prefija todo: funciones, hooks, opciones de base de datos. mpf_ en este ejemplo evita choques con otros plugins que definan render_banner() o algo igual de genérico.
- Escapa siempre la salida: esc_html() para texto, esc_url() para enlaces, esc_attr() para atributos HTML. Ningún dato que venga de shortcode_atts o de la base de datos es confiable por defecto.
- No uses $wpdb->query() con variables concatenadas — usa $wpdb->prepare() siempre que la consulta incluya datos externos.
- Versiona los assets con la versión del plugin, no con una fecha fija ni con time(): así el caché se invalida solo cuando realmente cambia el archivo.
Un plugin de front bien hecho se nota en lo que no hace: no carga JS en páginas donde no se usa, no imprime notices en pantallas que no son suyas, no deja basura en la base de datos cuando se desinstala.