Este artículo explica, paso a paso y con todo lujo de detalles, cómo crear un sistema de notas editoriales por post en WordPress usando PHP y JavaScript. La idea es dotar a cada entrada (post) de un área donde editores y autores puedan añadir, editar, marcar como resuelta y borrar notas internas que ayuden en el flujo editorial. El sistema se implementa como un mini-plugin (o código en functions.php) que añade una metabox en el editor, gestiona los datos en post meta y comunica el cliente (JS) con el servidor (AJAX) con las medidas de seguridad recomendadas (nonces, capacidades, sanitización).
Requisitos previos
Instalación de WordPress 5.x o superior.
Conocimientos básicos de PHP, JS y la API de WordPress (hooks, AJAX, metaboxes).
Acceso a los archivos del tema o posibilidad de instalar un plugin personalizado.
Arquitectura y decisiones de diseño
Almacenamiento: utilizaremos post meta con una clave única (por ejemplo _editorial_notes). Cada entrada almacena un array ordenado de notas.
Formato de cada nota: array asociativo con campos: id (uniq), author_id, content, created_at, updated_at, status (openresolved).
Interacción: AJAX administrativo (acciones wp_ajax_), con comprobaciones de capability y nonces.
UI: una metabox en el editor clásico y soporte para el editor de bloques (siempre visible en el sidebar si se desea). En este tutorial nos centramos en la metabox clásica en pantalla de edición.
Instalación: estructura de archivos recomendada
Crea una carpeta de plugin, por ejemplo editorial-notes, con estos ficheros:
editorial-notes.php (archivo principal del plugin)
/assets/js/admin-notes.js (código JS para UI y AJAX)
/assets/css/admin-notes.css (estilos básicos)
Código: plugin mínimo completo (PHP)
A continuación el archivo principal del plugin. Pégalo en editorial-notes/editorial-notes.php.
ID ) . >
echo
Cargando notas...
echo
}
/ Enqueue scripts y estilos en admin /
function enotes_admin_enqueue( hook ) {
// Solo en post.php o post-new.php para post types post
if ( ! in_array( hook, array( post.php, post-new.php ), true ) ) {
return
}
wp_enqueue_style( enotes-admin-css, plugin_dir_url( __FILE__ ) . assets/css/admin-notes.css )
wp_enqueue_script( enotes-admin-js, plugin_dir_url( __FILE__ ) . assets/js/admin-notes.js, array( jquery ), null, true )
// Datos locales para JS
wp_localize_script( enotes-admin-js, ENOTES,
array(
ajax_url => admin_url( admin-ajax.php ),
nonce => wp_create_nonce( enotes_nonce ),
post_id => get_the_ID(),
)
)
}
/ Helpers: obtener y guardar notas (en postmeta) /
function enotes_get_notes( post_id ) {
notes = get_post_meta( post_id, ENOTES_META_KEY, true )
if ( ! is_array( notes ) ) {
notes = array()
}
return notes
}
function enotes_save_notes( post_id, notes ) {
update_post_meta( post_id, ENOTES_META_KEY, notes )
}
/ Generador de ID simple /
function enotes_generate_id() {
return uniqid( enote_, true )
}
/ AJAX: obtener notas /
function enotes_ajax_get() {
check_ajax_referer( enotes_nonce, nonce )
post_id = isset( _POST[post_id] ) ? intval( _POST[post_id] ) : 0
if ( ! post_id ! current_user_can( edit_post, post_id ) ) {
wp_send_json_error( array( message => Permisos insuficientes. ), 403 )
}
notes = enotes_get_notes( post_id )
wp_send_json_success( notes )
}
/ AJAX: añadir nota /
function enotes_ajax_add() {
check_ajax_referer( enotes_nonce, nonce )
post_id = isset( _POST[post_id] ) ? intval( _POST[post_id] ) : 0
content = isset( _POST[content] ) ? wp_kses_post( trim( _POST[content] ) ) :
if ( ! post_id ! current_user_can( edit_post, post_id ) ) {
wp_send_json_error( array( message => Permisos insuficientes. ), 403 )
}
if ( empty( content ) ) {
wp_send_json_error( array( message => Contenido vacío. ), 400 )
}
notes = enotes_get_notes( post_id )
note = array(
id => enotes_generate_id(),
author_id => get_current_user_id(),
content => content,
created_at => current_time( mysql ),
updated_at => current_time( mysql ),
status => open,
)
notes[] = note
enotes_save_notes( post_id, notes )
wp_send_json_success( note )
}
/ AJAX: actualizar nota (editar contenido o marcar resuelta) /
function enotes_ajax_update() {
check_ajax_referer( enotes_nonce, nonce )
post_id = isset( _POST[post_id] ) ? intval( _POST[post_id] ) : 0
note_id = isset( _POST[note_id] ) ? sanitize_text_field( _POST[note_id] ) :
content = isset( _POST[content] ) ? wp_kses_post( trim( _POST[content] ) ) : null
status = isset( _POST[status] ) ? sanitize_key( _POST[status] ) : null
if ( ! post_id ! current_user_can( edit_post, post_id ) ) {
wp_send_json_error( array( message => Permisos insuficientes. ), 403 )
}
if ( empty( note_id ) ) {
wp_send_json_error( array( message => ID de nota faltante. ), 400 )
}
notes = enotes_get_notes( post_id )
found = false
foreach ( notes as n ) {
if ( n[id] === note_id ) {
if ( content !== null ) {
n[content] = content
n[updated_at] = current_time( mysql )
}
if ( status !== null in_array( status, array( open, resolved ), true ) ) {
n[status] = status
n[updated_at] = current_time( mysql )
}
found = true
updated_note = n
break
}
}
if ( ! found ) {
wp_send_json_error( array( message => Nota no encontrada. ), 404 )
}
enotes_save_notes( post_id, notes )
wp_send_json_success( updated_note )
}
/ AJAX: eliminar nota /
function enotes_ajax_delete() {
check_ajax_referer( enotes_nonce, nonce )
post_id = isset( _POST[post_id] ) ? intval( _POST[post_id] ) : 0
note_id = isset( _POST[note_id] ) ? sanitize_text_field( _POST[note_id] ) :
if ( ! post_id ! current_user_can( edit_post, post_id ) ) {
wp_send_json_error( array( message => Permisos insuficientes. ), 403 )
}
if ( empty( note_id ) ) {
wp_send_json_error( array( message => ID de nota faltante. ), 400 )
}
notes = enotes_get_notes( post_id )
new_notes = array()
deleted = false
foreach ( notes as n ) {
if ( n[id] === note_id ) {
deleted = true
continue
}
new_notes[] = n
}
if ( ! deleted ) {
wp_send_json_error( array( message => Nota no encontrada. ), 404 )
}
enotes_save_notes( post_id, new_notes )
wp_send_json_success( array( deleted => true ) )
}
/ Opcional: guardado al guardar el post para mantener consistencia /
function enotes_save_post_meta( post_id ) {
// No hacemos nada específico aquí: las notas ya se guardan vía AJAX.
return
}
?>
Explicación rápida del PHP
Las notas se almacenan en postmeta bajo la clave _editorial_notes como un array serializado.
Se registran cuatro endpoints AJAX: get, add, update y delete. Todos requieren nonce y capability edit_post.
Se expone un único contenedor en la metabox la UI la pinta el JS y llama a los endpoints.
Código: JavaScript (UI y llamadas AJAX)
Guárdalo en assets/js/admin-notes.js. Este archivo gestiona la UI dentro de la metabox: lista notas, formulario, acciones de editar/resolver/borrar.
jQuery( function( ) {
var container = ( #enotes-container )
var postId = container.data( post-id ) ENOTES.post_id
var ajaxUrl = ENOTES.ajax_url
var nonce = ENOTES.nonce
function renderEmpty() {
container.html(
Si quieres que los editores vean las notas también en la vista pública (por ejemplo para revisión), añade esta función al plugin o al theme. Aquí sólo se muestra a usuarios con capability de edición del post.
/ Mostrar notas en front-end (podría colocarse en single.php o como hook) /
function enotes_display_front( content ) {
if ( ! is_singular( post ) ) {
return content
}
global post
if ( ! current_user_can( edit_post, post->ID ) ) {
return content
}
notes = enotes_get_notes( post->ID )
if ( empty( notes ) ) {
return content
}
html =
return content . html
}
add_filter( the_content, enotes_display_front )
Seguridad, rendimiento y mejoras
Seguridad: siempre verifica nonces (check_ajax_referer), verifica capabilities con current_user_can(edit_post, post_id) y sanitiza entradas con wp_kses_post / sanitize_text_field según convenga. Escapa salidas con esc_html, esc_attr, wp_kses_post.
Rendimiento: almacenar muchas notas en postmeta puede crecer. Si esperas cientos de notas por post, considera crear una tabla personalizada (wpdb) para respuestas más eficientes y consultas paginadas.
Control de versiones: si quieres historial detallado por nota (versiones editadas), guarda un historial o entradas separadas por timestamp.
Notificaciones: añade notificaciones por email o WebSocket cuando se crea o asigna una nota.
Privacidad: recuerda que estas notas son internas evita mostrarlas a usuarios no autorizados y protege backups.
Alternativa: tabla personalizada
Si necesitas escalabilidad, crea una tabla con columns: id, post_id, author_id, content, created_at, updated_at, status. Usa dbDelta para crear la tabla al activar el plugin y emplea wpdb para operaciones CRUD. Ventaja: consultas más rápidas, índices, menor serializado. Desventaja: más código y migraciones.
Depuración y pruebas
Activa WP_DEBUG para ver errores en desarrollo.
Prueba con usuarios con diferentes roles (autor, editor, administrador) para verificar permisos.
Simula condiciones de fallo: content vacío, post_id inválido, nonces inválidos.
Revisa la consola del navegador para errores JS y la pestaña Network para las llamadas AJAX.
Conclusión
Con este enfoque tienes un sistema sencillo y funcional de notas editoriales por post usando la API estándar de WordPress (metaboxes, post meta y AJAX). Es un buen punto de partida: la arquitectura permite mejorar fácilmente con una tabla personalizada, integración con el editor de bloques (sidebar) o con notificaciones avanzadas. Aplica las buenas prácticas de seguridad y sanitización y adapta la UI según tu flujo editorial.