De qué se compone el panel de administración de WordPress
El panel de administración de WordPress parece una sola aplicación, pero técnicamente es un conjunto de archivos PHP normales en el directorio wp-admin, unidos por un archivo de arranque y unas cuantas decenas de actions. Quien conoce el orden en que se ejecutan esas actions escribe plugins que cargan scripts solo donde hacen falta y que no rompen las pantallas de otros plugins.
Este artículo describe el directorio wp-admin, el recorrido de una petición por el panel, cómo se nombran las pantallas ($hook_suffix, WP_Screen) y en qué se diferencia el panel de la jerarquía de plantillas del tema. Los nombres de archivo y el orden de las actions proceden del código de WordPress 7.1.3 (la versión estable vigente en el momento de la comprobación) en el espejo WordPress/WordPress en GitHub.
Cuál es la estructura del directorio wp-admin en WordPress
El directorio wp-admin contiene los archivos de la parte de gestión del sitio. En el nivel superior hay un archivo por pantalla: edit.php es la lista de entradas, post.php y post-new.php gestionan la edición, upload.php es la Biblioteca de medios, options-general.php, options-writing.php y el resto de options-*.php son las subpáginas de Ajustes, y site-health.php es Salud del sitio. La dirección en la barra del navegador revela, por tanto, qué archivo se está ejecutando. Al lado están los subdirectorios network/ (administración de la red en multisitio), user/ (panel del usuario en multisitio) y maint/ (reparación de la base de datos).
Archivos principales del directorio wp-admin
admin.php - El archivo de arranque del panel. Todas las pantallas empiezan con require_once __DIR__ . '/admin.php';. Aquí:
- se define la constante
WP_ADMINy se cargawp-load.php - se incluyen las funciones de
wp-admin/includes/admin.php - la función
auth_redirect()envía a los visitantes sin sesión a la pantalla de acceso - se construye el menú y se determina la pantalla actual
index.php - El Escritorio, la página de inicio del panel. Muestra:
- los widgets del escritorio
- acciones rápidas
- noticias de WordPress
menu.php - La definición de la estructura del menú del panel:
- registro de los elementos del menú
- los permisos necesarios para cada sección
- iconos y etiquetas del menú
Subdirectorios de wp-admin
includes/ - Contiene todos los archivos de biblioteca:
class-wp-screen.php- la clase que gestiona las pantallas del paneltemplate.php- funciones auxiliares para plantillasmedia.php- gestión de mediosajax-actions.php- manejadores de AJAX
css/ - Las hojas de estilo del panel:
wp-admin.css- la hoja agregadora que importa las demáscommon.css- estilos comunes (el contenedor.wrap, los avisos.notice)colors/- esquemas de colorforms.css- estilos de formularios
Cada hoja de estilo existe en cuatro variantes: la fuente, la minificada (.min.css) y dos versiones para idiomas que se escriben de derecha a izquierda (-rtl.css, -rtl.min.css). Lo mismo ocurre con los scripts de js/: junto a common.js está common.min.js. En producción WordPress sirve las versiones .min, y la constante SCRIPT_DEBUG lo cambia a los archivos fuente, como se explica más abajo.
js/ - Archivos JavaScript:
common.js- funciones comunespost.js- la pantalla de edición de entradasmedia-upload.js- subida de medios
Qué hace WordPress al abrir una dirección de wp-admin
Tomemos la petición wp-admin/admin.php?page=mi-pagina, es decir, una pantalla añadida por un plugin. En la versión 7.1.3 el archivo admin.php recorre estos pasos en orden:
- Define
WP_ADMINe incluyewp-load.php, y con élwp-config.php, los plugins y el tema. A partir de aquíis_admin()devuelvetrue. - Incluye
wp-admin/includes/admin.php, es decir, toda la API del panel, y llama aauth_redirect(). - Incluye
wp-admin/menu.php. Este archivo construye los arrays$menuy$submenucon los elementos del núcleo y, al final, incluyewp-admin/includes/menu.php, que dispara la actionadmin_menu. Ahí es donde los plugins llaman aadd_menu_page(). - Dispara la action
admin_init. - A partir del parámetro
pagecalcula el nombre del hook de la página (get_plugin_page_hook()) y lo guarda en la variable global$hook_suffix. - Llama a
set_current_screen(), que crea el objetoWP_Screenpara esta pantalla. - Dispara la action
load-{$page_hook}, antes de enviar ningún HTML. - Incluye
admin-header.php, salvo que la dirección lleve el parámetronoheader. - Dispara la action con el nombre
$page_hook. Ahí es dondeadd_menu_page()enganchó antes el callback del plugin. - Incluye
admin-footer.phpy termina conexit.
De este orden se derivan dos cosas que en la práctica rompen plugins. Primero, admin_menu se ejecuta antes de admin_init, no después, así que el código que registra el menú no puede suponer que algo configurado en admin_init ya existe. Segundo, get_current_screen() devuelve null hasta el paso 6, porque la función solo devuelve el objeto global cuando es una instancia de WP_Screen. Llamarla en admin_init da null, mientras que en load-{$page_hook} ya devuelve un objeto que funciona.
La action load-{$page_hook} es el mejor sitio para procesar un formulario que redirige después de guardar. Las cabeceras HTTP aún no se han enviado, así que wp_safe_redirect() funciona, y el código se ejecuta solo en esa pantalla. Un comentario en admin.php da un ejemplo de nombre: para la página de un plugin añadida bajo Ajustes, el hook se llama load-settings_page_pluginbasename.
Qué se ejecuta en admin-header.php y admin-footer.php
La cabecera del panel también es una secuencia de actions, y su orden decide dónde enganchar estilos y scripts. En admin-header.php WordPress dispara, por orden:
admin_enqueue_scriptscon el argumento$hook_suffix, el lugar parawp_enqueue_script()ywp_enqueue_style(),admin_print_styles-{$hook_suffix}yadmin_print_styles,admin_print_scripts-{$hook_suffix}yadmin_print_scripts,admin_head-{$hook_suffix}yadmin_head,- después incluye
menu-header.php, que dibuja el menú lateral, y disparain_admin_header, - por último
admin_notices(onetwork_admin_noticesyuser_admin_noticesen multisitio) yall_admin_notices.
En admin-footer.php el orden es parecido: in_admin_footer, admin_footer, admin_print_footer_scripts-{$hook_suffix}, admin_print_footer_scripts y admin_footer-{$hook_suffix}. Las versiones con sufijo se ejecutan en una sola pantalla, así que sustituyen una condición if dentro del callback. La documentación del hook admin_enqueue_scripts lo describe como el hook adecuado para añadir scripts y estilos de la administración (pese al nombre, gestiona ambos tipos) y recomienda usar $hook_suffix para cargarlos solo en las pantallas que los usan.
Las pantallas del núcleo, como options-general.php, hacen lo mismo sin el parámetro page: empiezan con require_once __DIR__ . '/admin.php';, preparan los datos, incluyen ellas mismas admin-header.php, imprimen el formulario e incluyen admin-footer.php.
Cómo funciona el sistema de plantillas del panel de administración
WordPress no usa en el panel de administración un sistema de plantillas tradicional como el del front-end. En su lugar, se apoya en una estructura hecha de funciones callback y hooks.
Estructura de una pantalla del panel
Cada pantalla del panel tiene tres partes principales. El esquema de abajo muestra la estructura de una pantalla del núcleo, que incluye por sí misma la cabecera y el pie. El callback de la página de un plugin no lo hace, porque es admin.php quien incluye la cabecera y el pie por él (pasos 8 y 10 de arriba). admin.php incluye la cabecera con require_once, de modo que un segundo require_once en el callback no hace nada, pero un require o include normal imprime la cabecera del panel por segunda vez.
// Estructura de ejemplo de una pantalla del núcleo, p. ej. wp-admin/options-general.php
require_once __DIR__ . '/admin.php';
// 1. Cabecera (header)
require_once ABSPATH . 'wp-admin/admin-header.php';
// 2. Contenido de la página (content)
?>
<div class="wrap">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<!-- Aquí va el contenido propiamente dicho -->
</div>
<?php
// 3. Pie (footer)
require_once ABSPATH . 'wp-admin/admin-footer.php';Funciones callback de las páginas del panel
Cada página del panel tiene asignada una función callback que imprime su contenido:
// Registro de una página del panel
add_action('admin_menu', function() {
add_menu_page(
'Mi página', // Título de la página
'Mi página', // Etiqueta del menú
'manage_options', // Permiso
'mi-pagina', // Slug
'mi_pagina_callback', // Función callback
'dashicons-admin-generic',
30
);
});
// Función callback que imprime el contenido
function mi_pagina_callback() {
?>
<div class="wrap">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<form method="post" action="options.php">
<?php
settings_fields('mi_grupo_opciones');
do_settings_sections('mi-pagina');
submit_button('Guardar cambios');
?>
</form>
</div>
<?php
}Cómo nombra WordPress el hook_suffix de las páginas de plugins
El valor que devuelven add_menu_page() y add_submenu_page() es el $hook_suffix de la nueva pantalla. La misma cadena llega a admin_enqueue_scripts, a los hooks load-* y a WP_Screen, así que conviene saber de dónde sale. La calcula la función get_plugin_page_hookname() en wp-admin/includes/plugin.php según el patrón {tipo}_page_{slug}:
- una página de nivel superior añadida con
add_menu_page()recibe el tipotoplevel, es decir,toplevel_page_mi-pagina, - una subpágina bajo un menú del núcleo recibe un tipo derivado del nombre del archivo padre, por ejemplo
settings_page_mi-paginabajo Ajustes, - una subpágina bajo el menú de otro plugin recibe el tipo del array
$admin_page_hooks, dondeadd_menu_page()guardasanitize_title( $menu_title ).
El último punto es una trampa en sitios traducidos. Si el título del menú padre pasa por __(), al cambiar el idioma del sitio cambia también el sufijo de todas sus subpáginas. Un menú con el título __( 'Reports' ) da a su subpágina el sufijo reports_page_mis-informes, pero cuando la traducción del plugin convierte el título en “Informes” en un panel en español, el sufijo pasa a ser informes_page_mis-informes. El código que compara $hook con el primer valor escrito a mano deja entonces de funcionar. Es más seguro guardar el valor que devuelve add_submenu_page() en una propiedad de la clase y comparar con él, o comprobar get_current_screen()->id.
Para qué sirven WP_Screen y get_current_screen
El objeto WP_Screen describe la pantalla actual del panel. Entre sus propiedades públicas están id, base, parent_base, post_type, taxonomy y action. En la lista de páginas, id es edit-page, base es edit y post_type es page, así que una sola condición sobre post_type cubre la lista, la edición y la creación de un elemento nuevo de ese tipo.
De este objeto cuelgan también dos elementos de la interfaz que a menudo se escriben desde cero sin necesidad: las pestañas de ayuda (add_help_tab()) y las opciones de pantalla (add_option(), por ejemplo el número de elementos por página en una lista). Ambos deben añadirse en la action load-{$page_hook}, porque solo entonces existe el objeto de la pantalla y la cabecera con los botones “Ayuda” y “Opciones de pantalla” todavía no se ha impreso.
Qué clases body recibe una pantalla del panel
admin-header.php construye el atributo class de la etiqueta body a partir de varias fuentes. La primera clase es el $hook_suffix con cada carácter fuera de a-z0-9_- sustituido por un guion, así que la pantalla de un plugin lleva en el body la clase toplevel_page_mi-pagina. Después vienen, entre otras, post-type-{tipo} y taxonomy-{taxonomía} del objeto de la pantalla, admin-color-{esquema} de los ajustes del perfil del usuario, locale-{idioma}, branch-{versión} y version-{versión}, multisite y network-admin en una red, y wp-theme-{tema} con el nombre del tema activo. Las clases propias se añaden con el filtro admin_body_class. Así el CSS de un plugin puede apuntar a una sola pantalla con un selector de clase, sin cargar una hoja de estilo aparte.
Cómo funcionan los hooks en el panel de administración de WordPress
Los hooks son el mecanismo que permite cambiar el comportamiento del panel sin tocar los archivos del núcleo de WordPress.
Actions en el panel de administración
Las actions ejecutan código en un momento concreto:
// Añadir un script solo en el panel de administración
add_action('admin_enqueue_scripts', function($hook) {
// $hook contiene el identificador de la página actual
if ($hook === 'toplevel_page_mi-pagina') {
wp_enqueue_script(
'mi-pagina-js',
plugin_dir_url(__FILE__) . 'js/admin.js',
['jquery'],
'1.0.0',
true
);
}
});
// Modificar la cabecera de la administración
add_action('admin_head', function() {
?>
<style>
#wpadminbar { background: #1d2327; }
</style>
<?php
});
// Añadir un aviso
add_action('admin_notices', function() {
?>
<div class="notice notice-success is-dismissible">
<p>Tus cambios se han guardado.</p>
</div>
<?php
});Filtros en el panel de administración
Los filtros modifican datos antes de mostrarlos o guardarlos:
// Modificar el título de la página del panel
add_filter('admin_title', function($admin_title, $title) {
return $title . ' | Mi sitio';
}, 10, 2);
// Cambiar el texto del pie del panel
add_filter('admin_footer_text', function($text) {
return 'Hecho con <a href="https://wordpress.org">WordPress</a> | Soporte técnico: Mi empresa';
});
// Modificar las columnas de la lista de entradas
add_filter('manage_posts_columns', function($columns) {
$new_columns = [];
foreach ($columns as $key => $value) {
$new_columns[$key] = $value;
if ($key === 'title') {
$new_columns['mi_columna'] = 'Mi columna adicional';
}
}
return $new_columns;
});Qué clases CSS usa el panel de administración de WordPress
WordPress incluye un conjunto de clases CSS que mantiene un aspecto coherente en el panel.
Clases CSS de los contenedores del panel
/* Contenedor principal de la página */
.wrap {
margin: 10px 20px 0 2px;
}
/* Título de la página */
.wrap h1 {
font-size: 23px;
font-weight: 400;
margin: 0;
padding: 9px 0 4px;
line-height: 1.3;
}
/* Metaboxes */
.postbox {
min-width: 255px;
border: 1px solid #c3c4c7;
box-shadow: 0 1px 1px rgba(0,0,0,.04);
background: #fff;
}
/* Formularios */
.form-table {
border-collapse: collapse;
margin-top: 0.5em;
width: 100%;
clear: both;
}
.form-table th {
vertical-align: top;
text-align: left;
padding: 20px 10px 20px 0;
width: 200px;
line-height: 1.3;
font-weight: 600;
}Clases CSS de botones y componentes de la interfaz
/* Botones (wp-includes/css/buttons.css) */
.wp-core-ui .button-primary {
background: var(--wp-admin-theme-color, #3858e9);
border-color: var(--wp-admin-theme-color, #3858e9);
color: #fff;
text-decoration: none;
text-shadow: none;
}
/* Avisos (wp-admin/css/common.css) */
.notice {
background: #fff;
border: none;
border-left: 4px solid #c3c4c7;
box-shadow: none;
margin: 5px 15px 2px;
padding: 8px 12px;
}
.notice-success {
border-left-color: #4ab866;
}
.notice-error {
border-left-color: #cc1818;
}Los valores de arriba proceden de los archivos buttons.css y common.css de la versión 7.1.3. El color del botón principal no está fijado en el código: sale de la variable CSS --wp-admin-theme-color, que define el esquema de color elegido, y #3858e9 es solo el valor de reserva. Un plugin que fije el color del botón en su propia hoja de estilo desentonará en el panel de un usuario que haya elegido otro esquema en su perfil. Es más seguro usar las clases del núcleo (button button-primary, notice notice-success) y no sobrescribir sus colores.
Cómo crear páginas propias en el panel de administración
Abajo está el ejemplo funcional más corto de una página de opciones construida sobre la Settings API. Una guía completa sobre cómo ordenar menús, roles de cliente, esquemas de color y widgets del escritorio está en un artículo aparte sobre la personalización de la administración de WordPress.
Cómo añadir una página de opciones sencilla
class Mi_Pagina_Opciones {
public function __construct() {
add_action('admin_menu', [$this, 'anadir_pagina']);
add_action('admin_init', [$this, 'registrar_ajustes']);
}
public function anadir_pagina() {
add_options_page(
'Mis ajustes',
'Mis ajustes',
'manage_options',
'mis-ajustes',
[$this, 'mostrar_pagina']
);
}
public function registrar_ajustes() {
register_setting('mis_ajustes_grupo', 'mi_opcion_1');
register_setting('mis_ajustes_grupo', 'mi_opcion_2');
add_settings_section(
'mi_seccion',
'Ajustes principales',
[$this, 'mostrar_seccion'],
'mis-ajustes'
);
add_settings_field(
'mi_opcion_1',
'Primera opción',
[$this, 'mostrar_campo_1'],
'mis-ajustes',
'mi_seccion'
);
}
public function mostrar_pagina() {
?>
<div class="wrap">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<form action="options.php" method="post">
<?php
settings_fields('mis_ajustes_grupo');
do_settings_sections('mis-ajustes');
submit_button('Guardar ajustes');
?>
</form>
</div>
<?php
}
public function mostrar_campo_1() {
$valor = get_option('mi_opcion_1');
?>
<input type="text" name="mi_opcion_1" value="<?php echo esc_attr($valor); ?>" class="regular-text">
<?php
}
}
new Mi_Pagina_Opciones();Qué diferencia hay entre admin-ajax.php y admin-post.php
El panel tiene dos entradas separadas para peticiones que no dibujan una pantalla, y ambas se saltan admin.php.
admin-ajax.php atiende las peticiones AJAX. Define la constante DOING_AJAX, carga wp-load.php y la API del panel, envía la cabecera X-Robots-Tag: noindex, dispara admin_init y después la action wp_ajax_{action} para usuarios con sesión iniciada o wp_ajax_nopriv_{action} para visitantes. Si falta el parámetro action, la respuesta es 0 con el código 400, y si falta un manejador registrado, también 0 con 400. En las pantallas del panel la dirección de este archivo está disponible de inmediato en la variable JavaScript ajaxurl, que imprime admin-header.php. El Plugin Handbook describe todo el flujo en el capítulo sobre AJAX en plugins.
admin-post.php sirve para formularios enviados por POST normal tras los que debe venir una redirección. Dispara admin_init y después admin_post_{action} para usuarios con sesión iniciada o admin_post_nopriv_{action} para visitantes. Si no hay nada enganchado a esa action, el archivo termina con una respuesta vacía y el código 400. El hook está descrito en la página admin_post_{$action}.
Lo que comparten ambos archivos tiene consecuencias para el rendimiento. admin_init se ejecuta en cada petición a admin-ajax.php, también en las peticiones periódicas de la Heartbeat API (la action heartbeat está en la lista de actions del núcleo, y para visitantes existe wp_ajax_nopriv_heartbeat). El código pesado enganchado a admin_init sin la condición wp_doing_ajax() se ejecuta, por tanto, no solo al abrir una pantalla, sino en cada uno de esos latidos.
En qué se diferencia el panel de la jerarquía de plantillas del tema
La palabra “plantilla” en el título de este artículo se confunde con facilidad con las plantillas del tema. El front-end elige el archivo según la jerarquía de plantillas, que el Theme Handbook describe así:
“Cuando un visitante llega a cualquier página de un sitio WordPress, WordPress usa la query string para decidir qué plantilla debe usarse para mostrar la página. La query string contiene datos de la URL que ayudan a tomar esa decisión.”
Theme Handbook, Template hierarchy, traducción propia
Para los temas de bloques, la misma página de la documentación enumera tres lugares donde WordPress busca una plantilla, por orden de prioridad: las plantillas creadas por el usuario y guardadas en la base de datos como tipo de contenido wp_template, la carpeta /templates del tema hijo y la carpeta /templates del tema padre.
El panel no tiene ninguno de estos mecanismos. No hay archivo de reserva como el index.php del tema ni coincidencia por tipo de consulta. Lo que se muestra lo decide el nombre del archivo en wp-admin o el parámetro page, y la disposición de la pantalla es siempre la misma cabecera, el mismo menú y el mismo pie del núcleo. El tema no influye en el aspecto del panel, salvo porque su nombre acaba en la clase wp-theme-* del body. Por eso los estilos y scripts del panel se añaden con admin_enqueue_scripts y los del front-end con wp_enqueue_scripts. Confundir estas dos actions acaba con la hoja de estilo del panel en el front-end o al revés.
Cómo proteger el panel de administración de WordPress
Comprobar los permisos del usuario
Comprueba siempre los permisos del usuario:
// Comprobar si el usuario tiene los permisos adecuados
if (!current_user_can('manage_options')) {
wp_die(__('No tienes permiso para acceder a esta página.', 'text-domain'));
}
// Comprobar el nonce de la acción
if (!isset($_POST['mi_nonce']) || !wp_verify_nonce($_POST['mi_nonce'], 'mi_accion')) {
wp_die(__('Token de seguridad no válido.', 'text-domain'));
}Sanear los datos de los formularios del panel
// Sanear texto
$titulo = sanitize_text_field($_POST['titulo']);
// Sanear HTML
$content = wp_kses_post($_POST['content']);
// Sanear URL
$url = esc_url_raw($_POST['url']);
// Sanear email
$email = sanitize_email($_POST['email']);Cómo depurar el panel de administración de WordPress
Cómo activar WP_DEBUG en el panel
// En wp-config.php
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
define('SCRIPT_DEBUG', true); // Usa scripts sin minificarLa documentación Debugging in WordPress describe SCRIPT_DEBUG como una constante que obliga a WordPress a usar las versiones de desarrollo de los archivos CSS y JavaScript del núcleo en lugar de las minificadas, y la señala como útil al probar cambios en los archivos .js y .css incluidos. Su valor por defecto es false. En el panel esto significa que el navegador recibe common.js en lugar de wp-admin/js/common.min.js, y que un stack trace de la consola apunta a una línea legible en lugar de a una única línea larga del archivo .min. La misma página recuerda que las definiciones deben ir antes del comentario que cierra la edición en wp-config.php.
Registrar eventos del panel en debug.log
function log_admin_action($action, $data) {
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log("Admin Action [{$action}]: " . print_r($data, true));
}
}
// Uso
log_admin_action('user_login', ['user_id' => get_current_user_id()]);Qué recordar sobre el panel de administración de WordPress
Cinco puntos de esta anatomía que cuentan al escribir un plugin:
- Estructura de directorios: saber cómo están organizados los archivos acelera la depuración y los cambios.
- Sistema de plantillas: las funciones callback y los hooks controlan cómo se dibuja una pantalla.
- Seguridad: la comprobación de permisos y el saneamiento de datos son la base.
- Clases CSS: las clases del núcleo mantienen las pantallas del plugin en línea con el resto del panel.
- Depuración:
SCRIPT_DEBUGy el registro de depuración acortan el camino hasta la causa de un error.
Las modificaciones del panel sobreviven a las actualizaciones solo cuando usan los mecanismos integrados en lugar de esquivarlos. Preparamos y probamos estos cambios en staging dentro del desarrollo WordPress.







