
Er zijn verschillende plug-ins die iets soortgelijks doen, maar bijna allemaal zijn ze bedoeld voor wanneer je samen met klanten een website ontwerpt of doorneemt (of voor je eigen projecten), om het feedbackproces te versnellen door de mogelijkheid te bieden om notities toe te voegen in de editor voor berichten en pagina’s of op je WordPress-dashboard.
Het concept van deze functie, die ik hier voor een heel specifiek doel had, is in wezen vergelijkbaar, maar heeft een ander doel: overal een notitie weergeven die elke bezoeker kan zien en sluiten. Het kan worden gebruikt als waarschuwing of als oproep tot actie, aangezien er links kunnen worden toegevoegd (door de url in te voeren met https://), voor snelle toelichtingen of correcties terwijl je aan het corrigeren of bijwerken bent, of om extra informatie over een afbeelding toe te voegen. Je zou kunnen zeggen dat het zowel als informatieve notitie als waarschuwing dient.
Omdat ik van eenvoudige plug-ins en functies houd, is het gebruik ervan ook eenvoudig.
Zodra je de functie hebt toegevoegd, hetzij als snippet, hetzij in het bestand functions.php van je sjabloon, hoef je verder niets meer te doen. Een gebruiker die als beheerder is ingelogd, kan een notitie toevoegen door op ALT te drukken en met de linkermuisknop te klikken op de plek waar de notitie moet verschijnen.

De notities, die ook bij de afbeeldingen kunnen worden toegevoegd, zien er zo uit.

Ik ben hier nog mee bezig om het zo goed mogelijk af te stemmen op het punt waar je ALT + linkermuisknop gebruikt, want nu verschijnt de notitie een paar millimeter lager of hoger, afhankelijk van het element waarop je deze toevoegt.
Tijdens de eerste tests merkte ik dat de notities van hun oorspronkelijke plek verschoven wanneer ik de grootte van de browservensters aanpaste. Om dit op te lossen moest ik hulp zoeken. Het probleem werd opgelost door middel van het actief afluisteren van gebeurtenissen (scroll en resize) via JavaScript, dat de coördinaten dynamisch herberekent op basis van een vaste container (fixed). Nu blijven de notities volledig stil staan en precies op hun plaats, ongeacht de grootte van het scherm.
Je kunt zoveel notities toevoegen als je wilt, maar houd er rekening mee dat de laatste die je toevoegt altijd de vorige overschrijft als je deze er heel dichtbij of eroverheen plaatst, en dat je ze (nog) niet kunt verslepen, maar alleen kunt sluiten.

Het configuratiescherm is eveneens zeer eenvoudig. Het toont alleen een lijst met de titel van het bericht of de pagina waaraan notities zijn toegevoegd, en het aantal notities. Er is een knop om de URL te openen en een andere om alle notities die aan die pagina zijn toegevoegd te verwijderen.

Hieronder bevindt zich het opschoningsgedeelte, waar je alle notities in de database met één klik kunt verwijderen. Zowel de knop voor het volledig opschonen als die voor het afzonderlijk verwijderen wissen de rijen in de database volledig, zodat er geen rommel op de site achterblijft als je besluit de functie niet meer te gebruiken.
Er worden geen aangepaste tabellen aangemaakt. Alles wordt standaard opgeslagen met behulp van `update_option() ` in de standaardtabel ` wp_options ` onder de naam ` little_notes_page_[ID]`.
Een ander punt waaraan ik zoveel mogelijk aandacht besteed, is de prestatie. Als de gebruiker niet als beheerder is ingelogd en er geen notities op de pagina staan, wordt er helemaal geen code geladen. Dit heeft geen enkele invloed op de laadsnelheid voor je bezoekers.
Door een absoluutcanvas te gebruiken met `pointer-events: none`, zweven de notities op de webpagina zonder dat dit invloed heeft op de CSS, de marges of de thema-elementen (voorlopig getest in GeneratePress).
Hoewel ik geen fouten heb ontdekt, blijf ik het testen om de veiligheid te verbeteren en het te optimaliseren, met het oog op het omzetten ervan in een plug-in die ik in de WordPress-repository wil plaatsen. Daarom raad ik je aan om het voorlopig in een testomgeving uit te proberen.
Ik heb een paar verbeteringen in gedachten. Als je het eens wilt uitproberen, kun je een functie voorstellen die volgens jou ontbreekt of die nuttig en/of noodzakelijk zou zijn als het uiteindelijk een officiële plug-in voor de repository wordt. De voorlopige titel voor die mogelijke plug-in is Little Notes (Kleine notities of Eenvoudige notities).
Als ik ondertussen de code van de functie bijwerk, doe ik dat hier en pas ik de datum aan, zodat je weet dat er iets is gewijzigd.
Code
/**
* 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
}





