Plugins con shortcodes en Elementor: cuándo y cómo crearlos
Por Rolando Escobar, desarrollador WordPress para agencias · · 6 min de lectura
Los page builders agilizan el maquetado, pero las funcionalidades a medida construidas dentro de ellos son difíciles de probar, reutilizar o mover. Un widget HTML lleno de JavaScript en línea, o un widget atado a la API de un solo builder, termina perteneciendo a una página en lugar de al código del proyecto.
Construí desde cero el sitio de la Orange County Dental Society con bloques de WordPress, a partir de un diseño de Figma. Funcionalidades como las búsquedas que filtran contenido por etiquetas son plugins a medida, cada uno insertado en la página con un shortcode. El tema se encarga del diseño y los plugins, del comportamiento, y la misma división funciona en Elementor, donde un shortcode entra en cualquier diseño. Así se construye bien ese tipo de plugin, con Elementor como ejemplo de builder.
Shortcode o widget personalizado de Elementor
Ambos son válidos. La decisión depende de quién configura la funcionalidad y cuánto tiempo tiene que durar.
| Shortcode en un plugin | Widget personalizado de Elementor | |
|---|---|---|
| Funciona fuera de Elementor | Sí: editor de bloques, editor clásico, widgets, plantillas PHP | No |
| Pruebas | Una función PHP que puedes probar con la suite de pruebas de WordPress | Necesita Elementor cargado; es más difícil de aislar |
| Experiencia de edición | Atributos escritos a mano | Controles visuales con vista previa en vivo |
| Estilos | Tu CSS, que puede leer los colores globales del kit | Controles de estilo de Elementor por instancia |
| Actualizaciones del builder | No le afectan | Puede romperse cuando cambia la API de widgets |
Elige un widget cuando el equipo vaya a configurar la funcionalidad seguido y necesite controles visuales, como un slider con ajustes por diapositiva. Elige un shortcode cuando la funcionalidad sea sobre todo lógica (una búsqueda, una lista filtrada, una integración), cuando pueda sobrevivir al builder actual o cuando quieras pruebas automatizadas a su alrededor.
Mi propio portafolio ilustra ese último punto. Funciona con los elementos atómicos de Elementor 4, y el widget de menú anterior se veía mal dentro de los nuevos contenedores, así que el menú pasó a ser un shortcode [rw_primary_nav] que envuelve wp_nav_menu(). El builder cambió y al shortcode no le importó.
Registra los assets, pero no los cargues todavía
Empieza el plugin registrando su CSS y su JavaScript. Registrar le dice a WordPress que los archivos existen; no imprime nada.
<?php
/**
* Plugin Name: RW Resources
* Description: Resource list shortcode: [rw_resources tag="" limit="6" layout="grid"].
* Requires at least: 6.5
* Requires PHP: 8.1
* Text Domain: rw-resources
*/
defined( 'ABSPATH' ) || exit;
const RW_RESOURCES_VERSION = '1.2.0';
add_action( 'init', 'rw_resources_init' );
function rw_resources_init(): void {
$url = plugin_dir_url( __FILE__ );
// Solo registrar. Nada se carga hasta que el shortcode se renderiza.
wp_register_style( 'rw-resources', $url . 'assets/resources.css', [], RW_RESOURCES_VERSION );
wp_register_script(
'rw-resources',
$url . 'assets/resources.js',
[],
RW_RESOURCES_VERSION,
[ 'strategy' => 'defer', 'in_footer' => true ]
);
add_shortcode( 'rw_resources', 'rw_resources_shortcode' );
}
Registrar en init en lugar de wp_enqueue_scripts hace que los handles existan en todos los contextos donde puede ejecutarse el shortcode, incluidas las peticiones REST y el editor de Elementor. La estrategia defer, disponible desde WordPress 6.3, saca el script de la ruta crítica de renderizado.
Escribe el callback
function rw_resources_shortcode( $atts ): string {
$atts = shortcode_atts(
[
'tag' => '',
'limit' => 6,
'layout' => 'grid',
],
$atts,
'rw_resources' // Habilita el filtro shortcode_atts_rw_resources.
);
// Los atributos vienen de quien edita la página: valídalos como cualquier entrada.
$tag = sanitize_title( $atts['tag'] );
$limit = min( max( absint( $atts['limit'] ), 1 ), 24 );
$layout = in_array( $atts['layout'], [ 'grid', 'list' ], true ) ? $atts['layout'] : 'grid';
// Carga los assets solo en páginas que renderizan este shortcode.
wp_enqueue_style( 'rw-resources' );
wp_enqueue_script( 'rw-resources' );
$cache_key = 'rw_res_' . md5( wp_json_encode( [ $tag, $limit, $layout, rw_resources_cache_version() ] ) );
$html = get_transient( $cache_key );
if ( false === $html ) {
$args = [
'post_type' => 'resource',
'post_status' => 'publish',
'posts_per_page' => $limit,
'no_found_rows' => true,
];
if ( '' !== $tag ) {
$args['tax_query'] = [
[ 'taxonomy' => 'resource_tag', 'field' => 'slug', 'terms' => $tag ],
];
}
$html = rw_resources_markup( new WP_Query( $args ), $layout );
set_transient( $cache_key, $html, 12 * HOUR_IN_SECONDS );
}
return $html; // Devuelve, nunca hagas echo.
}
Aquí actúan cuatro reglas:
- Pasa el nombre del shortcode a
shortcode_atts(). El tercer argumento habilita el filtroshortcode_atts_rw_resources, para que otro código pueda cambiar los valores por defecto sin editar el tuyo. - Trata los atributos como entrada del usuario. Cualquiera que pueda editar una entrada puede escribir un shortcode, incluidos los colaboradores. Limita los números, usa listas de valores permitidos para las opciones y sanitiza los slugs. Los atributos de shortcode sin escapar son una causa frecuente de reportes de XSS almacenado en plugins de WordPress.
- Devuelve, nunca hagas echo. WordPress coloca la cadena devuelta donde estaba el shortcode. La salida con echo se imprime antes del contenido al que pertenece, lo que rompe el diseño y, en Elementor, queda fuera del contenedor del widget.
- Encola dentro del callback. Los assets se cargan solo en las páginas que renderizan el shortcode. Un script encolado tan tarde se imprime en el footer, y por eso se registró para el footer. Una hoja de estilos tardía también se imprime en el footer, así que mantén ese CSS pequeño o acepta un breve parpadeo sin estilos.
El capítulo de shortcodes del Plugin Handbook explica la API con más detalle.
Escapa en el punto de salida
function rw_resources_markup( WP_Query $query, string $layout ): string {
if ( ! $query->have_posts() ) {
return '<p class="rw-resources__empty">' . esc_html__( 'No resources found.', 'rw-resources' ) . '</p>';
}
$items = '';
foreach ( $query->posts as $post ) {
$items .= sprintf(
'<li class="rw-resources__item"><a href="%1$s">%2$s</a><p>%3$s</p></li>',
esc_url( get_permalink( $post ) ),
esc_html( get_the_title( $post ) ),
esc_html( get_the_excerpt( $post ) )
);
}
return sprintf(
'<ul class="rw-resources rw-resources--%1$s">%2$s</ul>',
esc_attr( $layout ),
$items
);
}
Cada valor dinámico se escapa según su contexto: esc_url() para el enlace, esc_html() para el texto y esc_attr() para la clase. Escapa tarde, en la función que arma el HTML, para que nada dependa de si los datos venían limpios. La lista es un <ul> real, que le indica a quien usa lector de pantalla cuántos elementos contiene, y cada elemento tiene un solo enlace cuyo texto es el título del recurso.
Guarda en caché la parte costosa
Un shortcode se ejecuta en cada vista de página que no sale de una caché de página. Si consulta la base de datos o llama a una API, guarda el resultado en caché. El callback guarda el HTML en un transient cuya clave incluye los atributos y un número de versión. Subir la versión cada vez que cambia un recurso invalida de una vez todas las variantes en caché:
function rw_resources_cache_version(): int {
return (int) get_option( 'rw_resources_cache_version', 1 );
}
function rw_resources_bump_cache(): void {
update_option( 'rw_resources_cache_version', rw_resources_cache_version() + 1, false );
}
// Guardar, enviar a la papelera o borrar un recurso invalida a la vez todas las listas en caché.
add_action( 'save_post_resource', 'rw_resources_bump_cache' );
add_action(
'deleted_post',
static function ( $post_id, $post ): void {
// Aquí $post es una fila cruda de la base de datos, no un WP_Post: no declares su tipo.
if ( isset( $post->post_type ) && 'resource' === $post->post_type ) {
rw_resources_bump_cache();
}
},
10,
2
);
Guarda en caché solo la salida que es igual para todos los visitantes. Cualquier cosa con un nonce, el nombre del usuario actual o el contenido del carrito debe renderizarse de nuevo. Los transients viejos caducan solos a las 12 horas, y una caché de objetos persistente, si el hosting la ofrece, los mantiene en memoria en lugar de en la tabla de opciones.
Elementor también tiene una función de Element Caching que guarda el HTML renderizado de un widget, con una opción de caché (Cache Settings) en la pestaña Avanzado de cada elemento. La documentación de Elementor indica que no se use en elementos que muestran información dinámica, como los shortcodes. Aquí hay una segunda razón: cuando se sirve el HTML en caché, tu callback no se ejecuta, así que el encolado que tiene adentro tampoco. Desactiva la caché de elementos en los widgets que contienen shortcodes interactivos y prueba la página después de activar cualquier caché.
Pruébalo como cualquier función PHP
class Test_RW_Resources_Shortcode extends WP_UnitTestCase {
public function set_up(): void {
parent::set_up();
wp_dequeue_script( 'rw-resources' );
}
public function test_limit_is_clamped(): void {
self::factory()->post->create_many( 30, [ 'post_type' => 'resource' ] );
$html = do_shortcode( '[rw_resources limit="500"]' );
$this->assertSame( 24, substr_count( $html, '<li ' ) );
}
public function test_titles_are_escaped(): void {
self::factory()->post->create( [ 'post_type' => 'resource', 'post_title' => '<b>Guide</b>' ] );
$html = do_shortcode( '[rw_resources]' );
$this->assertStringContainsString( '<b>Guide</b>', $html );
}
public function test_assets_load_only_when_used(): void {
$this->assertFalse( wp_script_is( 'rw-resources', 'enqueued' ) );
do_shortcode( '[rw_resources]' );
$this->assertTrue( wp_script_is( 'rw-resources', 'enqueued' ) );
}
}
Estas pruebas corren sobre la configuración estándar de PHPUnit para WordPress que genera wp scaffold plugin-tests. Cubren lo más importante: los atributos se limitan, la salida se escapa y los assets se cargan solo cuando se usa el shortcode. Ninguna necesita Elementor instalado.
Cómo usarlo dentro de un diseño de Elementor
- Agrega un widget Shortcode al contenedor donde va la funcionalidad y escribe
[rw_resources tag="onboarding" limit="6"]. El ancho, el espaciado y el título de la sección se quedan en Elementor, donde el diseñador espera encontrarlos. - Da estilo al marcado del plugin con CSS que tenga valores de respaldo y que lea los colores globales de Elementor donde existan, por ejemplo
color: var(--e-global-color-accent, #1d4ed8). Si cambias el color del kit, la funcionalidad lo sigue. - En el editor, Elementor vuelve a renderizar un widget por AJAX cuando lo modificas, así que su script puede no ejecutarse en el lienzo hasta que recargues. Evalúa el comportamiento interactivo en la vista previa o en el front end.
- Documenta los atributos en el readme del plugin. Cuando un atributo no es válido, puedes mostrar una pista solo al equipo editorial si compruebas
current_user_can( 'edit_posts' )antes de imprimirla.
Qué vigilar
- Agrega un prefijo a los shortcodes (
rw_resources, noresources) para que no choquen con otros plugins. - Nunca ejecutes
do_shortcode()sobre texto que los visitantes envían desde el front end. - Mantén ligero el marcado contenedor. Elementor ya envuelve el shortcode en su propio contenedor.
- Haz que el script encuentre las instancias por clase y no por ID, para que el shortcode pueda aparecer dos veces en una página.
- Si una funcionalidad necesita muchos ajustes, llévalos a una página de opciones en lugar de agregar un décimo atributo.
Este es el tipo de trabajo que hago para clientes: Desarrollo con Elementor