
Existen varios plugins que hacen algo parecido, pero casi todos están pensados para cuando diseñas o revisas una web con clientes (o para tus propios proyectos) para agilizar el proceso de feedback ofreciendo la posibilidad de añadir notas en el editor de post y páginas o en el escritorio de tu WordPress.
El planteamiento de esta función, que tenía por aquí para un uso muy concreto, es parecido en esencia, pero con una utilidad distinta; mostrar una nota en cualquier sitio y que la pueda ver, y cerrar, cualquier visitante. Puede usarse como alerta, como llamada a la acción, ya que se pueden añadir enlaces (escribiendo la url con https://), para aclaraciones o correcciones rápidas mientras corriges o actualizas o para añadir información extra sobre una imagen. Puede decirse que sirve tanto de nota informativa como de aviso.
Como me gustan los plugins y funciones simples, su uso también lo es.
Una vez que añadas la función, bien sea como snippet o en el functions.php de tu plantilla, no hay que hacer nada más. El usuario que esté logeado como administrador podrá añadir una nota pulsando ALT y el botón izquierdo del ratón en el punto que quiera que aparezca la nota.

Las notas, que también se pueden añadir sobre las imágenes, tienen este aspecto.

Aún estoy afinando esto para que se ajuste lo máximo posible al punto donde se hace ALT + clic izquierdo, ya que ahora la nota aparece unos milímetros más abajo o más arriba dependiendo del elemento sobre el que se añada.
En las primeras pruebas detecté que al redimensionar las ventanas del navegador las notas se desplazaban de su sitio original. Para solucionar esto tuve que buscar ayuda porque las pistas que daban la IAs (Claude y Gémini) no eran suficientes. Se arregló recurriendo a una escucha activa de eventos (scroll y resize) mediante JavaScript que recalcula dinámicamente las coordenadas en base a un contenedor fijo (fixed). Ahora, las notas se quedan totalmente inmóviles y clavadas en su lugar exacto sin importar los cambios de tamaño de la pantalla.
Puedes poner tantas notas como quieras, teniendo en cuenta que la última que añadas siempre se superpondrá a la anterior si la pones muy cerca o encima y no se pueden arrastrar (aún), solo cerrarlas.

El panel de control también es mínimo. Solo muestra una lista con el título del post o página donde hay notas añadidas y la cantidad de notas. Un botón para visitar la URL y otro para borrar todas las notas añadidas en esa página.

Debajo está el área de limpieza donde se pueden eliminar todas las notas en la base de datos con un clic. Tanto el botón de purgado como el de borrado individual limpian por completo las filas de la base de datos, evitando dejar basura en el sitio si decides dejas de usar la función.
No se crean tablas personalizadas. Todo se almacena de forma nativa usando update_option() en la tabla estándar wp_options con el nombre little_notes_page_[ID]
Otra cuestión a la que presto toda la atención que puedo es al rendimiento. Si el usuario no está logeado como administrador y no hay notas en la página, no se carga absolutamente nada de código. Cero impacto en la velocidad de tus visitas.
Al usar un lienzo (canvas) absoluto con pointer-events: none, las notas flotan sobre la web sin alterar el CSS, los márgenes ni los elementos del tema (de momento probado en GeneratePress).
Si bien no he detectado errores, sigo probándolo para reforzar la seguridad y depurarlo con la intención de convertirlo en plugin para subirlo al repositorio de WordPress por lo que te recomiendo que de momento lo trastees en un entorno de pruebas.
Tengo un par de mejoras en mente. Si te animas a probarlo, puedes sugerir alguna funcionalidad que creas que le falta o que sería útil y/o necesaria si se convierte finalmente en un plugin oficial para el repositorio. El título provisional para ese probable plugin es Little Notes (Pequeñas notas o Notas simples).
Mientras tanto, si actualizo el código de la función lo haré aquí mismo y cambiaré la fecha para que sepas que se ha modificado. En la siguiente versión pretendo que, entre algunas mejoras y alguna pequeña corrección, se puedan añadir notas también desde la versión móvil.
Actualización 19/06/2026. Si prefieres esperar al plugin, se ha enviado hoy al repositorio para su revisión inicial.

Código
/**
* Función para sistema de notas adhesivas en front-end
* Prefijo único para PHP: little_notes / Prefijo único para CSS y JS: little-notes
* Las notas se añaden con ALT + clic izquierdo en cualquier punto logeado como admin
* Revisión 17/06/2026 - Versión con panel de control y borrado rápido desde admin en Ajustes/Little Notes
* Textos en inglés preparados para plugin
*/
if ( ! defined( 'ABSPATH' ) ) exit;
// 1. FRONT-END ASSETS & DATA LOADING
add_action( 'wp_enqueue_scripts', 'little_notes_cargar_assets' );
function little_notes_cargar_assets() {
$post_id = get_the_ID();
if ( ! $post_id ) return;
add_action( 'wp_footer', function() use ($post_id) {
$notas_actuales = get_option( 'little_notes_page_' . $post_id, array() );
$is_admin = current_user_can( 'manage_options' ) ? 1 : 0;
if ( ! $is_admin && empty( $notas_actuales ) ) return;
$nonce = wp_create_nonce( 'little_notes_seguridad' );
?>
<style id="little-notes-estilos">
#little-notes-canvas {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 999999;
}
.little-note-adhesiva {
position: absolute;
width: 220px;
min-height: 90px;
background: #fef5c1;
padding: 18px 12px 12px 12px;
box-shadow: 0 4px 10px rgba(0,0,0,0.18);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
font-weight: 500;
line-height: 1.4;
color: #2c3e50;
border-left: 5px solid #fade59;
transform: translate(-50%, -10px) rotate(-1deg);
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.little-note-adhesiva:hover {
transform: translate(-50%, -10px) rotate(0deg) scale(1.03);
box-shadow: 0 6px 14px rgba(0,0,0,0.22);
}
.little-note-texto a {
color: inherit !important;
text-decoration: underline !important;
word-break: break-all;
}
.little-note-pin {
position: absolute;
top: -8px;
left: 50%;
width: 14px;
height: 14px;
background: #e74c3c;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0,0,0,0.25);
transform: translateX(-50%);
}
.little-note-cerrar {
position: absolute;
top: 2px;
right: 6px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
color: #7f8c8d;
user-select: none;
line-height: 1;
}
.little-note-cerrar:hover { color: #c0392b; }
.little-note-texto {
word-wrap: break-word;
white-space: pre-wrap;
margin-top: 4px;
}
</style>
<div id="little-notes-canvas"></div>
<script id="little-notes-js">
document.addEventListener("DOMContentLoaded", function() {
const canvas = document.getElementById('little-notes-canvas');
const isAdmin = <?php echo $is_admin; ?>;
const notasDB = <?php echo json_encode( $notas_actuales ); ?>;
const closedNotas = JSON.parse(localStorage.getItem('little_notes_cerradas') || '[]');
const ajaxUrl = '<?php echo admin_url( 'admin-ajax.php' ); ?>';
// Buscamos el contenedor principal de GeneratePress o el cuerpo del contenido (.entry-content)
const mainContainer = document.querySelector('.entry-content') || document.querySelector('#content') || document.body;
const deleteTooltip = "<?php echo esc_js( __('Delete permanently for everyone', 'little-notes') ); ?>";
const closeTooltip = "<?php echo esc_js( __('Close note', 'little-notes') ); ?>";
const promptMessage = "<?php echo esc_js( __('Enter your revision note:', 'little-notes') ); ?>";
const confirmDeleteMsg = "<?php echo esc_js( __('Do you want to permanently delete this note from the database?', 'little-notes') ); ?>";
const saveErrorMsg = "<?php echo esc_js( __('Server error while saving.', 'little-notes') ); ?>";
let listaNotasInstanciadas = [];
if (Array.isArray(notasDB)) {
notasDB.forEach(nota => {
if (!closedNotas.includes(nota.id)) {
renderizarNota(nota);
}
});
}
if (isAdmin) {
document.addEventListener('click', function(e) {
if (e.altKey) {
e.preventDefault();
// Medimos la posición relativa al contenedor principal en píxeles y porcentaje
const containerRect = mainContainer.getBoundingClientRect();
const containerAbsoluteLeft = containerRect.left + window.scrollX;
const pixelXRelative = e.pageX - containerAbsoluteLeft;
const pctX = (pixelXRelative / containerRect.width) * 100;
const absoluteY = e.pageY; // El eje Y sigue siendo absoluto al scroll vertical
const texto = prompt(promptMessage);
if (texto && texto.trim() !== "") {
guardarNotaBD(pctX, absoluteY, texto);
}
}
});
}
function autoConvertirEnlaces(texto) {
const expresionUrl = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
return texto.replace(expresionUrl, function(url) {
return `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`;
});
}
function renderizarNota(nota) {
const div = document.createElement('div');
div.className = 'little-note-adhesiva';
div.id = nota.id;
div.style.pointerEvents = 'auto';
const textoProcesado = autoConvertirEnlaces(nota.texto);
const currentTooltip = isAdmin ? deleteTooltip : closeTooltip;
div.innerHTML = `
<div class="little-note-pin"></div>
<span class="little-note-cerrar" title="${currentTooltip}">×</span>
<div class="little-note-texto">${textoProcesado}</div>
`;
div.querySelector('.little-note-cerrar').addEventListener('click', function() {
if (isAdmin) {
if (confirm(confirmDeleteMsg)) {
borrarNotaBD(nota.id, div);
}
} else {
closedNotas.push(nota.id);
localStorage.setItem('little_notes_cerradas', JSON.stringify(closedNotas));
div.remove();
listaNotasInstanciadas = listaNotasInstanciadas.filter(n => n.id !== nota.id);
}
});
canvas.appendChild(div);
const objetoNota = { id: nota.id, element: div, pctX: nota.x, originalY: nota.y };
listaNotasInstanciadas.push(objetoNota);
posicionarElemento(objetoNota);
}
function posicionarElemento(objNota) {
const containerRect = mainContainer.getBoundingClientRect();
// Reconstruimos la coordenada X real multiplicando el porcentaje guardado por el ancho actual del bloque
const xCalculado = containerRect.left + (containerRect.width * (objNota.pctX / 100));
// Calculamos la Y restando la barra de scroll vertical del viewport fijo
const yCalculado = objNota.originalY - window.scrollY;
objNota.element.style.left = xCalculado + 'px';
objNota.element.style.top = yCalculado + 'px';
}
function actualizarPosiciones() {
listaNotasInstanciadas.forEach(posicionarElemento);
}
window.addEventListener('scroll', actualizarPosiciones, { passive: true });
window.addEventListener('resize', actualizarPosiciones, { passive: true });
function guardarNotaBD(x, y, texto) {
const formData = new FormData();
formData.append('action', 'little_notes_guardar');
formData.append('post_id', '<?php echo $post_id; ?>');
formData.append('x', x); // Aquí se envía el porcentaje elástico
formData.append('y', y);
formData.append('texto', texto);
formData.append('nonce', '<?php echo $nonce; ?>');
fetch(ajaxUrl, { method: 'POST', body: formData })
.then(res => res.json())
.then(res => {
if(res.success && res.data) {
renderizarNota(res.data);
} else {
alert(saveErrorMsg);
}
})
.catch(err => console.error("AJAX Error:", err));
}
function borrarNotaBD(id, elementoHtml) {
const formData = new FormData();
formData.append('action', 'little_notes_borrar');
formData.append('post_id', '<?php echo $post_id; ?>');
formData.append('nota_id', id);
formData.append('nonce', '<?php echo $nonce; ?>');
fetch(ajaxUrl, { method: 'POST', body: formData })
.then(res => res.json())
.then(res => {
if(res.success) {
elementoHtml.remove();
listaNotasInstanciadas = listaNotasInstanciadas.filter(n => n.id !== id);
}
});
}
});
</script>
<?php
}, 999);
}
// 2. INDEPENDENT AJAX PROCESSORS
add_action( 'wp_ajax_little_notes_guardar', 'little_notes_ajax_guardar_handler' );
function little_notes_ajax_guardar_handler() {
check_ajax_referer( 'little_notes_seguridad', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error( __('Unauthorized', 'little-notes') );
$post_id = intval( $_POST['post_id'] );
$notas = get_option( 'little_notes_page_' . $post_id, array() );
$nueva_nota = array(
'id' => uniqid('note_'),
'x' => floatval( $_POST['x'] ), // Guarda el valor flotante porcentual
'y' => floatval( $_POST['y'] ),
'texto' => sanitize_textarea_field( $_POST['texto'] )
);
$notas[] = $nueva_nota;
update_option( 'little_notes_page_' . $post_id, $notas );
wp_send_json_success( $nueva_nota );
}
add_action( 'wp_ajax_little_notes_borrar', 'little_notes_ajax_borrar_handler' );
function little_notes_ajax_borrar_handler() {
check_ajax_referer( 'little_notes_seguridad', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error( __('Unauthorized', 'little-notes') );
$post_id = intval( $_POST['post_id'] );
$nota_id = sanitize_text_field( $_POST['nota_id'] );
$notas = get_option( 'little_notes_page_' . $post_id, array() );
$notas = array_filter( $notas, function($n) use ($nota_id) {
return $n['id'] !== $nota_id;
});
if ( empty( $notas ) ) {
delete_option( 'little_notes_page_' . $post_id );
} else {
update_option( 'little_notes_page_' . $post_id, array_values($notas) );
}
wp_send_json_success();
}
// 3. ADMIN MENU & ACTIVE NOTES OVERVIEW
add_action( 'admin_menu', 'little_notes_crear_menu_admin' );
function little_notes_crear_menu_admin() {
add_options_page(
__('Little Notes Settings', 'little-notes'),
__('Little Notes', 'little-notes'),
'manage_options',
'little-notes-admin',
'little_notes_render_page_admin'
);
}
function little_notes_render_page_admin() {
if ( ! current_user_can( 'manage_options' ) ) return;
global $wpdb;
if ( isset($_POST['little_notes_delete_post_id']) ) {
check_admin_referer( 'little_notes_delete_post_action' );
$target_post_id = intval($_POST['little_notes_delete_post_id']);
delete_option( 'little_notes_page_' . $target_post_id );
echo '<div class="notice notice-success is-dismissible"><p><strong>' . esc_html__('Notes for this page have been successfully deleted.', 'little-notes') . '</strong></p></div>';
}
if ( isset($_POST['little_notes_purge_all']) ) {
check_admin_referer( 'little_notes_accion_purgar' );
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'little_notes_page_%'" );
echo '<div class="notice notice-success is-dismissible"><p><strong>' . esc_html__('Success! All revision notes have been cleanly deleted from the database.', 'little-notes') . '</strong></p></div>';
}
$resultados = $wpdb->get_results( "SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE 'little_notes_page_%'" );
?>
<div class="wrap">
<h1><?php _e('Little Notes — Revision Dashboard', 'little-notes'); ?></h1>
<p><?php _e('Below is a list of all pages and posts on your website currently containing active sticky revision notes.', 'little-notes'); ?></p>
<h2 class="title" style="margin-top:20px;"><?php _e('Pages with Active Notes', 'little-notes'); ?></h2>
<table class="wp-list-table widefat fixed striped table-view-list" style="margin-top:10px; max-width: 850px;">
<thead>
<tr>
<th style="font-weight:bold; width: 50%;"><?php _e('Content Title', 'little-notes'); ?></th>
<th style="font-weight:bold; width: 15%; text-align:center;"><?php _e('Number of Notes', 'little-notes'); ?></th>
<th style="font-weight:bold; width: 35%; text-align:center;"><?php _e('Actions', 'little-notes'); ?></th>
</tr>
</thead>
<tbody>
<?php if ( empty( $resultados ) ) : ?>
<tr>
<td colspan="3"><?php _e('No active revision notes found on any page.', 'little-notes'); ?></td>
</tr>
<?php else :
foreach ( $resultados as $fila ) {
$post_id = intval( str_replace( 'little_notes_page_', '', $fila->option_name ) );
$titulo = get_the_title( $post_id );
if ( ! $titulo ) $titulo = sprintf( __('Content #ID %d (Or deleted)', 'little-notes'), $post_id );
$datos_notas = maybe_unserialize( $fila->option_value );
$cantidad = is_array( $datos_notas ) ? count( $datos_notas ) : 0;
if ( $cantidad === 0 ) continue;
?>
<tr>
<td><strong><?php echo esc_html( $titulo ); ?></strong> <span class="description">(ID: <?php echo $post_id; ?>)</span></td>
<td style="text-align:center;"><span class="update-plugins count-<?php echo $cantidad; ?>" style="background:#fade59; color:#2c3e50; font-weight:bold; padding:2px 8px; border-radius:10px;"><?php echo $cantidad; ?></span></td>
<td style="text-align:center;">
<div style="display: flex; gap: 8px; justify-content: center; align-items: center;">
<a href="<?php echo esc_url( get_permalink( $post_id ) ); ?>" target="_blank" class="button button-small"><?php _e('View on Front-End', 'little-notes'); ?></a>
<form method="post" action="" style="margin:0;" onsubmit="return confirm('<?php echo esc_js( sprintf(__('Are you sure you want to delete all notes for: %s?', 'little-notes'), $titulo) ); ?>');">
<?php wp_nonce_field( 'little_notes_delete_post_action' ); ?>
<input type="hidden" name="little_notes_delete_post_id" value="<?php echo $post_id; ?>">
<input type="submit" class="button button-small delete" style="color: #b32d2e; border-color: #b32d2e;" value="<?php esc_attr_e('Delete notes', 'little-notes'); ?>">
</form>
</div>
</td>
</tr>
<?php
}
endif; ?>
</tbody>
</table>
<div style="margin-top: 40px; padding: 20px; border: 1px solid #cc0000; background: #fff; max-width: 760px; border-radius: 4px;">
<h3 style="color: #cc0000; margin-top:0;"><?php _e('Clean Uninstall Zone / Reset Database', 'little-notes'); ?></h3>
<p><?php _e('If you have finished the development phase and want to wipe out the data, or if you plan to deactivate this feature, use the button below. It will permanently delete all notes across all pages from the database.', 'little-notes'); ?></p>
<?php
$confirm_js = esc_js( __('Are you absolutely sure you want to delete ALL revision notes from the database? This action cannot be undone.', 'little-notes') );
?>
<form method="post" action="" onsubmit="return confirm('<?php echo $confirm_js; ?>');">
<?php wp_nonce_field( 'little_notes_accion_purgar' ); ?>
<input type="hidden" name="little_notes_purge_all" value="1">
<?php submit_button( __('Permanently Delete All Notes', 'little-notes'), 'delete', 'submit', false ); ?>
</form>
</div>
</div>
<?php
}











