
Ya tenemos un tipo de contenido Recursos y una taxonomía Nivel. Ahora queremos guardar datos concretos de cada recurso: el enlace de descarga y la versión. No tiene sentido escribirlos dentro del texto: queremos campos propios, que podamos mostrar siempre en el mismo lugar y con el mismo formato.
Para eso existen los campos personalizados (post meta): datos extra asociados a una entrada. Y la forma clásica de editarlos es una meta box, una caja en la pantalla de edición.
Antes de empezar
- Seguimos trabajando en el plugin
cwp-recursos.phpde las dos entradas anteriores de Estructuras Avanzadas. - Plugins como ACF (Advanced Custom Fields) hacen esto sin código. Hacerlo a mano una vez te enseña cómo se guardan datos de forma segura en WordPress, algo que vas a necesitar en cualquier plugin.
Paso 1. Agrega la caja
add_action( 'add_meta_boxes', 'cwp_recursos_agregar_caja' );
function cwp_recursos_agregar_caja() {
add_meta_box(
'cwp_recurso_datos', // identificador
'Datos del recurso', // título de la caja
'cwp_recursos_dibujar_caja', // función que la dibuja
'cwp_recurso', // en qué tipo de contenido
'side' // ubicación: 'side' o 'normal'
);
}
function cwp_recursos_dibujar_caja( $post ) {
wp_nonce_field( 'cwp_recurso_guardar', 'cwp_recurso_nonce' );
$url = get_post_meta( $post->ID, '_cwp_url_descarga', true );
$version = get_post_meta( $post->ID, '_cwp_version', true );
?>
<p>
<label for="cwp_url_descarga"><strong>Enlace de descarga</strong></label><br>
<input type="url" id="cwp_url_descarga" name="cwp_url_descarga"
value="<?php echo esc_attr( $url ); ?>" class="widefat" placeholder="https://">
</p>
<p>
<label for="cwp_version"><strong>Versión</strong></label><br>
<input type="text" id="cwp_version" name="cwp_version"
value="<?php echo esc_attr( $version ); ?>" class="widefat" placeholder="1.0.0">
</p>
<?php
}
Abre un recurso para editar: en el panel derecho (en el editor de bloques, dentro de la pestaña del recurso, más abajo) aparece la caja Datos del recurso.
get_post_meta( $id, $clave, true ): lee un dato. Eltruepide un solo valor en vez de una lista.- Las claves empiezan con guion bajo (
_cwp_version). Así WordPress las oculta del panel genérico de «Campos personalizados» y nadie las edita por error desde ahí. esc_attr(): escapamos el valor al ponerlo dentro del atributovalue.wp_nonce_field(): agrega un campo oculto con un código de un solo uso. Lo explicamos en el paso siguiente.
Paso 2. Guarda los datos (con las 4 comprobaciones)
Esta es la parte importante. Antes de guardar cualquier dato que viene de un formulario, hay que comprobar cuatro cosas:
add_action( 'save_post_cwp_recurso', 'cwp_recursos_guardar_caja' );
function cwp_recursos_guardar_caja( $post_id ) {
// 1. ¿El formulario salió realmente de nuestra caja?
if ( ! isset( $_POST['cwp_recurso_nonce'] )
|| ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['cwp_recurso_nonce'] ) ), 'cwp_recurso_guardar' ) ) {
return;
}
// 2. ¿Es un guardado automático? Entonces no tocar nada.
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
// 3. ¿El usuario puede editar esta entrada?
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
// 4. Limpiar y guardar cada campo.
if ( isset( $_POST['cwp_url_descarga'] ) ) {
$url = esc_url_raw( wp_unslash( $_POST['cwp_url_descarga'] ) );
if ( $url ) {
update_post_meta( $post_id, '_cwp_url_descarga', $url );
} else {
delete_post_meta( $post_id, '_cwp_url_descarga' );
}
}
if ( isset( $_POST['cwp_version'] ) ) {
$version = sanitize_text_field( wp_unslash( $_POST['cwp_version'] ) );
update_post_meta( $post_id, '_cwp_version', $version );
}
}
Por qué cada comprobación
- Nonce: protege contra CSRF. Sin él, un atacante podría preparar una página que, al ser visitada por un administrador con sesión iniciada, envíe un formulario falso a tu sitio y cambie datos sin que lo note. El nonce demuestra que el formulario lo generó tu panel, para ese usuario, hace poco.
- Autoguardado: WordPress guarda borradores automáticamente cada cierto tiempo, sin enviar nuestros campos. Si no lo excluimos, podríamos borrar datos.
- Permisos: que alguien pueda enviar el formulario no significa que pueda editar esa entrada.
current_user_can( 'edit_post', $id )lo verifica. - Limpieza:
esc_url_raw()deja solo direcciones válidas (descarta, por ejemplo, unjavascript:);sanitize_text_field()quita etiquetas y caracteres raros.wp_unslash()quita las barras invertidas que WordPress agrega a los datos de formularios.
El gancho save_post_cwp_recurso (con el nombre del tipo de contenido al final) solo se ejecuta al guardar recursos. Así no gastamos recursos revisando cada entrada o página que se guarde.
Paso 3. Muestra los datos en el sitio
add_filter( 'the_content', 'cwp_recursos_mostrar_datos', 20 );
function cwp_recursos_mostrar_datos( $contenido ) {
if ( ! is_singular( 'cwp_recurso' ) || ! in_the_loop() || ! is_main_query() ) {
return $contenido;
}
$url = get_post_meta( get_the_ID(), '_cwp_url_descarga', true );
$version = get_post_meta( get_the_ID(), '_cwp_version', true );
if ( ! $url ) {
return $contenido;
}
$texto = $version ? 'Descargar (versión ' . $version . ')' : 'Descargar';
return $contenido . sprintf(
'<p class="cwp-descarga"><a href="%s" rel="nofollow noopener" target="_blank">%s</a></p>',
esc_url( $url ),
esc_html( $texto )
);
}
Otra vez la regla: limpiar al guardar y escapar al mostrar. Aunque limpiamos al guardar, escapamos de nuevo al mostrar, porque el dato pudo llegar a la base de datos por otro camino (una importación, otro plugin).
Paso 4 (opcional). Que el editor de bloques también los conozca
Si quieres leer o editar estos campos desde bloques o desde la API REST, regístralos:
add_action( 'init', function () {
register_post_meta( 'cwp_recurso', '_cwp_version', array(
'type' => 'string',
'single' => true,
'show_in_rest' => true,
'sanitize_callback' => 'sanitize_text_field',
'auth_callback' => function () {
return current_user_can( 'edit_posts' );
},
) );
} );
Con esto, el campo queda documentado y protegido también fuera de nuestra meta box.
Resumen de seguridad para cualquier formulario
- Nonce al dibujar (
wp_nonce_field) y al guardar (wp_verify_nonce). - Permisos con
current_user_can. - Limpiar con
sanitize_*oesc_url_rawantes de guardar. - Escapar con
esc_html,esc_attroesc_urlal mostrar.
Si te quedas con estas cuatro reglas, ya escribes código más seguro que muchos plugins publicados.
¿Qué campos necesitan tus contenidos? Comparte tu caso en el foro y conversemos si conviene hacerlo a mano, con ACF o con bloques.





