Como crear un sistema de notas editoriales por post con JS y PHP en WordPress

Contents

Introducción

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

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(
      

No hay notas. Añade la primera nota editorial:

) } function fetchNotes() { container.html(

Cargando notas...

) .post( ajaxUrl, { action: enotes_get, nonce: nonce, post_id: postId }, function( res ) { if ( res.success ) { renderNotes( res.data ) } else { container.html(

Error al cargar notas.

) } } ).fail( function() { container.html(

Error de conexión.

) } ) } function renderNotes( notes ) { var html =
if ( notes notes.length ) { html =
    notes.forEach( function( n ) { html =
  • html =
    Por: ( n.author_id anon ) ( n.created_at )
    html =
    nl2br( escapeHtml( n.content ) )
    html =
    html = html = html = html =
    html =
    Estado: n.status
    html =
  • } ) html =
} else { html =

No hay notas.

} // Formulario para añadir nueva nota html =
html = html =

html =
container.html( html ) } function addNote() { var content = ( #enote-new-content ).val() if ( ! content ! content.trim() ) { alert( Escribe el contenido de la nota. ) return } .post( ajaxUrl, { action: enotes_add, nonce: nonce, post_id: postId, content: content }, function( res ) { if ( res.success ) { fetchNotes() } else { alert( res.data res.data.message ? res.data.message : Error al añadir nota. ) } } ).fail( function() { alert( Error de conexión. ) } ) } function updateNote( noteId, data ) { var payload = { action: enotes_update, nonce: nonce, post_id: postId, note_id: noteId } if ( data.content !== undefined ) payload.content = data.content if ( data.status !== undefined ) payload.status = data.status .post( ajaxUrl, payload, function( res ) { if ( res.success ) { fetchNotes() } else { alert( res.data res.data.message ? res.data.message : Error al actualizar. ) } } ).fail( function() { alert( Error de conexión. ) } ) } function deleteNote( noteId ) { if ( ! confirm( ¿Eliminar esta nota? ) ) return .post( ajaxUrl, { action: enotes_delete, nonce: nonce, post_id: postId, note_id: noteId }, function( res ) { if ( res.success ) { fetchNotes() } else { alert( res.data res.data.message ? res.data.message : Error al eliminar. ) } } ).fail( function() { alert( Error de conexión. ) } ) } // Delegated events container.on( click, #enote-add-btn, function() { addNote() } ) container.on( click, .enote-edit, function() { var li = ( this ).closest( .enote-item ) var id = li.data( id ) var contentDiv = li.find( .enote-content ) var current = contentDiv.attr( data-content ) var textarea = contentDiv.html( textarea

) } ) container.on( click, .enote-cancel, function() { fetchNotes() } ) container.on( click, .enote-save, function() { var li = ( this ).closest( .enote-item ) var id = li.data( id ) var newContent = li.find( .enote-edit-text ).val() if ( ! newContent ! newContent.trim() ) { alert( Contenido vacío. ) return } updateNote( id, { content: newContent } ) } ) container.on( click, .enote-resolve, function() { var li = ( this ).closest( .enote-item ) var id = li.data( id ) var currentStatus = li.find( .enote-status ).text().indexOf( resuelta ) !== -1 li.find( .enote-status ).text().indexOf( resolved ) !== -1 ? resolved : open var newStatus = currentStatus === resolved ? open : resolved updateNote( id, { status: newStatus } ) } ) container.on( click, .enote-delete, function() { var li = ( this ).closest( .enote-item ) var id = li.data( id ) deleteNote( id ) } ) // Helpers function escapeHtml( text ) { if ( ! text ) return return text.replace( //g, amp ) .replace( //g, gt ) .replace( //g, quot ) .replace( //g, #039 ) } function nl2br( str ) { return str.replace( /n/g,
) } // Init fetchNotes() } )

Notas sobre el JS

Estilos CSS básicos

Archivo assets/css/admin-notes.css con estilos sencillos para la metabox:

.enotes-list ul { list-style: none margin:0 padding:0 }
.enote-item { border:1px solid #e1e1e1 padding:8px margin-bottom:8px background:#fff }
.enote-meta { font-size:12px color:#666 margin-bottom:6px }
.enote-content { margin-bottom:6px white-space:pre-wrap }
.enote-actions .button { margin-right:6px }
.enote-status { font-size:12px color:#333 margin-top:6px }
.enotes-empty { padding:8px }

Mostrar notas en el front-end (opcional)

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 = 

Notas editoriales

    foreach ( notes as n ) { html .=
  • Por: . esc_html( n[author_id] ) . ( . esc_html( n[created_at] ) . )
    html .=
    . wp_kses_post( nl2br( esc_html( n[content] ) ) ) .
    html .=
    Estado: . esc_html( n[status] ) .
  • } html .=
return content . html } add_filter( the_content, enotes_display_front )

Seguridad, rendimiento y mejoras

  1. 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.
  2. 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.
  3. Control de versiones: si quieres historial detallado por nota (versiones editadas), guarda un historial o entradas separadas por timestamp.
  4. Notificaciones: añade notificaciones por email o WebSocket cuando se crea o asigna una nota.
  5. 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

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.

Recursos



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *