Doch genau die interessanten Dinge lassen die meisten Anleitungen weg: Sicherheit, Barrierefreiheit, Übersetzbarkeit und die einzige echte Designentscheidung, nämlich wie man verhindert, dass jemand zehnmal hintereinander klickt.
Dazu kommt eine ehrliche Einordnung, die über allem steht: So ein Like-Button und dessen Zähler sind ein kleines «Gefällt mir»-Signal, keine harte Statistik. Niemand sollte damit Entscheidungen treffen, niemand wird betrogen, wenn die Zahl nicht ganz stimmt. Sie gibt nur eine grobe Richtung vor. Das klingt nebensächlich, ist aber der Punkt, an dem sich entscheidet, welche Kompromisse in Ordnung sind.
Wie der Like-Button funktioniert, was die üblichen Tutorials weglassen und wie ich ihn in meinem Theme umgesetzt habe, liest du hier.
Warum kein Plugin?
Es gibt Plugins für einen Like-Button, zum Beispiel WP ULike, mit Voting-Buttons, Statistik-Dashboard und eigener Einstellungsseite. Wenn du ein fertiges Theme nutzt und ohne Aufwand eine Reaktion unter deine Beiträge bekommen willst, ist das oder eine andere Plugin-Lösung die richtige Wahl.
Für ein eigenes Classic Theme ist es das nicht unbedingt. Denn ein Plugin weiss vom eigenen Styling und den vorhandenen Mechanismen meist nichts – was es ausgibt, ist generisch und generisch ist hier das Problem.
Ein Like-Button besteht nur aus wenigen Teilen: eine Funktion fürs Markup, ein AJAX-Handler, etwas JavaScript und CSS. Eine Plugin-Abhängigkeit mit eigenen Datenbanktabellen, eigenem Markup und eigenem Styling einzuführen – das man anschliessend wieder überschreiben müsste –, steht dazu in keinem Verhältnis. Wer sein Theme kennt, baut den Like-Button so, wie er hineinpasst: das Icon aus demselben Set, der Hover-Zustand über denselben Tooltip-Mechanismus, das Styling an denselben Variablen.
Wie der Like-Button funktioniert
Die Mechanik ist überschaubar – und genau dieser Teil stammt aus verschiedenen Tutorials. Der Like-Button trägt die Post-ID als data-Attribut. Ein Klick schickt per fetch einen POST-Request an admin-ajax.php, den eingebauten AJAX-Endpunkt von WordPress. Dort zählt eine PHP-Funktion den Wert in den Post-Meta um eins hoch und gibt den neuen Stand als JSON zurück. Das JavaScript aktualisiert damit die Zahl am Button – ohne die Seite neu zu laden.
const body = new FormData();
body.append( 'action', 'yourtheme_like_post' );
body.append( 'post_id', postId );
body.append( 'nonce', nonce );
fetch( ajaxUrl, { method: 'POST', body: body } )
.then( ( res ) => res.json() )
.then( ( result ) => {
// result.data.count: der neue Zählerstand
} );
Der Zählerstand liegt in den Post-Meta unter dem Schlüssel like_count – kein eigenes Schema, keine zusätzliche Tabelle. Ob jemand schon geliked hat, merkt sich der Browser lokal über localStorage. Genau dort liegt eine der interessanten Entscheidungen – aber dazu später mehr.
So weit das Grundgerüst. Das ist genau die Basis-Version, bei der die meisten Anleitungen aufhören. Ab hier wird es jetzt interessant.
Was verschiedene Tutorials weglassen
Die Basis-Version funktioniert – aber sie überspringt das, was einen Codeschnipsel von etwas unterscheidet, das man wirklich ausliefern sollte.
Sicherheit
admin-ajax.php ist ein öffentlicher Endpunkt – jeder kann ihn ansprechen, auch ohne je den Like-Button gesehen zu haben. Viele Tutorials zählen einfach hoch, was hereinkommt. Der Like-Button trägt deshalb eine Nonce, die der Handler vor dem Zählen prüft, und die Eingaben werden validiert: die Post-ID über intval() und einen get_post()-Check, die Nonce über sanitize_text_field( wp_unslash() ). Likes auf nicht existierende Beiträge oder ohne gültige Nonce werden abgewiesen.
function yourtheme_like_post() {
$nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
$post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
if ( ! wp_verify_nonce( $nonce, 'yourtheme-like' ) ) {
wp_send_json_error( 'Invalid nonce' );
}
if ( ! $post_id || ! get_post( $post_id ) ) {
wp_send_json_error( 'Invalid post ID' );
}
$like_count = (int) get_post_meta( $post_id, 'like_count', true );
$like_count++;
update_post_meta( $post_id, 'like_count', $like_count );
wp_send_json_success( array( 'count' => $like_count ) );
}
Übersetzbarkeit
Die Like-Button-Texte sollten nicht hartcodiert sein, sondern über __() übersetzbar gemacht werden. Weil sie für jeden Like-Button identisch sind, gehen sie einmal pro Seite über wp_localize_script ins JavaScript – statt an jedem Like-Button als data-Attribut zu kleben.
Unnötiges jQuery
Viele Anleitungen setzen $.ajax voraus – und damit jQuery. Die Library steckt sogar im WordPress-Core, geladen wird sie im Frontend aber nur, wenn ein Theme oder Plugin sie einbindet. Ein fetch mit FormData macht dasselbe – ohne Abhängigkeit, und wer jQuery sonst nicht braucht, spart sich das Laden ganz.
Cookies oder localStorage
Die eine echte Designentscheidung: Wie verhindert man, dass jemand zehnmal hintereinander liked? Eingeloggt ist niemand, an einen Benutzer lässt sich der Like also nicht binden. Es braucht einen Eintrag im Browser.
Einer von mehreren Ausgangspunkten für meine Umsetzung war das Tutorial von Amr AbdElkarem, Add an AJAX like button to WordPress without a plugin. Es wird ein serverseitiges Cookie gesetzt und beim nächsten Request mitgelesen. Das klingt nach dem solideren Weg, weil der Server mitprüft. Ist es aber nicht.
Ein Cookie ist genauso löschbar und fälschbar wie ein Eintrag im localStorage – einmal geleert oder in einem anderen Browser geöffnet, und man kann erneut liken. Echten Schutz bietet keines von beiden. Dafür hat das Cookie echte Nachteile: Es wird bei jedem Request mitgeschickt, auch wenn es niemanden interessiert. Es beisst sich mit Full-Page-Caching – eine Seite, die Cookies setzt oder darauf variiert, ist schwerer zu cachen, genau das Caching, das gleich noch ein Thema wird. Beim Datenschutz nehmen sich die beiden dagegen nichts: In der EU braucht jede nicht zwingend nötige Speicherung auf dem Gerät eine Einwilligung, Cookie wie localStorage. In der Schweiz verlangt Art. 45c des Fernmeldegesetzes (FMG), über die Bearbeitung und ihren Zweck zu informieren und auf die Möglichkeit zur Ablehnung hinzuweisen.
Bitte beachte: Ich bin Webdesigner, kein Jurist – dies ist keine verlässliche Rechtsberatung. Informiere dich selbst. Hier einige Quellen: EDPB-Leitlinien 2/2023 zu Art. 5(3) ePrivacy, EDÖB-Leitfaden zu Cookies und Steiger Legal zum revDSG.
Darum der Eintrag im localStorage, pro Beitrag unter einem eigenen Schlüssel:
// Nach erfolgreichem Like merken:
localStorage.setItem( 'liked_' + postId, '1' );
// Beim Laden den Zustand wiederherstellen:
if ( localStorage.getItem( 'liked_' + postId ) ) {
// Button als «geliked» markieren
}
Das ist kein Kompromiss aus Faulheit, sondern die passende Wahl für das, was der Like-Button ist: ein kleines Signal. Kein Request-Overhead, kein Cache-Konflikt – bei derselben, geringen Manipulationssicherheit wie das Cookie.
Caching und die Nonce
Die Nonce, die den Like-Button absichert, hat einen Haken, sobald ein Full-Page-Cache im Spiel ist. Solche Plugins liefern fertiges HTML aus, das eine Weile gespeichert bleibt – inklusive der Nonce, die zum Zeitpunkt des Cachings gültig war. Nonces haben aber eine begrenzte Lebensdauer von rund einem Tag. Läuft sie ab, während die Seite im Cache liegt, schlägt wp_verify_nonce() beim nächsten Like fehl, und der Klick wird abgewiesen – obwohl alles korrekt aussieht.
Zwei Wege führen daran vorbei. Entweder nimmt man den Button-Bereich vom Cache aus (Fragment Caching oder Hole-Punching), sodass die Nonce bei jedem Aufruf frisch erzeugt wird. Oder das JavaScript holt sich die Nonce über einen separaten, ungecachten Request, kurz bevor es den Like sendet.
Ohne HTML-Cache – oder mit reinem Objekt-Cache, der die Seite nicht als Ganzes einfriert – ist das kein Thema. Aber es ist genau die Art Fehler, die im lokalen Test meist nicht auftaucht und erst auf der Live-Site mit aktivem Cache zuschlägt. Darum sollte es erwähnt werden, auch wenn es die meisten kleinen Setups nie betrifft.
Barrierefreiheit
Der Punkt, der fast immer fehlt. Ein Like-Button wird in vielen Tutorials als klickbares <div> oder <a> erstellt – das sieht im ersten Moment gleich aus, ist für Tastatur und Screenreader aber meist nicht korrekt bedienbar oder schlicht das falsche Element für diese Aktion. In dieser Version wird ein echtes <button type="button"> Element verwendet: es ist fokussierbar, mit Enter und Leertaste bedienbar und wird von Screenreadern als Schaltfläche erkannt.
<button type="button" class="btn-like" data-post-id="42"
aria-label="Diesen Beitrag liken" aria-pressed="false">
<span class="like-icon" aria-hidden="true"><svg>…</svg></span>
<span class="like-count" aria-hidden="true">7</span>
<span class="like-status visually-hidden" aria-live="polite"></span>
</button>
Dahinter stecken vier Teile. Der erste Teil: Das aria-label beschreibt die Aktion, das rein dekorative Icon ist aria-hidden. Der zweite Teil: Das aria-pressed hält den gelikten Zustand fest – es wird per JavaScript gesetzt, weil der Server gar nicht weiss, ob dieser Browser schon geliked hat; die Information lebt nur im localStorage – wie bereits zuvor erklärt.
Der dritte Teil ist die Ansage durch Screenreader. Das sichtbare Zähler-Badge ist aria-hidden, sonst würde ein Screenreader bei jeder Änderung nur die nackte Zahl vorlesen. Stattdessen gibt es eine zweite, visuell versteckte Region mit aria-live="polite". Nach einem erfolgreichen Like füllt das JavaScript sie mit einem ganzen Satz, der den neuen Stand nennt – im korrekten Singular oder Plural («1 Like» gegenüber «2 Likes»). Und diese Region wird nur bei einem frischen Like aktiv, nicht bei jedem Seitenaufruf, sonst würde jeder neue Besuch auf der Seite den Satz vorlesen.
Bleibt der vierte Teil: ein sichtbarer Fokus-Zustand, damit man den Button auch bei Tastaturnavigation sieht. Eine Kleinigkeit, die fast überall vergessen wird. Wobei Barrierefreiheit eigentlich eine Pflichtübung und nicht die Kür sein sollte.
In der Schweiz verpflichtet das Behindertengleichstellungsgesetz (BehiG) bisher vor allem öffentliche Stellen zu barrierefreien digitalen Angeboten – private Anbieter noch nicht. Mit der Teilrevision des BehiG (Botschaft des Bundesrats von Ende 2024) soll diese Pflicht auf öffentlich zugängliche digitale Dienstleistungen ausgeweitet werden, frühestens ab 2027; Massstab sind die WCAG (Stufe AA) und der Schweizer eCH-0059. Wer den EU-Markt bedient, fällt ohnehin schon unter den seit Juni 2025 geltenden European Accessibility Act.
Auf meiner Website habe ich versucht, die Barrierefreiheit bereits durchgängig mitzudenken.
Ein verwandtes, aber anderes Thema: Inhalte nicht nur für Menschen mit assistiver Technik, sondern auch für KI besser lesbar zu machen – dafür gibt es eigene Wege, etwa llms.txt für WordPress – ohne Plugin.
Wie ich es im Theme umgesetzt habe
Im Repo ist alles framework-neutral gehalten: ein selbst-initialisierendes Script, schlichtes CSS mit einer Custom-Property für die Akzentfarbe, Feedback über die aria-live-Region. In meinem Theme sieht es an drei Stellen anders aus – nicht besser, sondern an die vorhandene Umgebung angepasst.
Erstens das Feedback. Mein Theme nutzt Bootstrap, also läuft das sichtbare «schon geliked»-Feedback über einen manuell gesteuerten Bootstrap-Tooltip statt über ein neutrales Einblenden. In diesem Fall manuell, damit Show und Hide im Code kontrolliert werden – und mit animation: false, damit der Tooltip sofort da ist, ohne Ein- und Ausblenden.
/* global bootstrap */
export function createManualTooltip( el, title ) {
const instance = new bootstrap.Tooltip( el, {
trigger: 'manual',
animation: false,
title: title,
} );
el.addEventListener( 'mouseenter', () => instance.show() );
el.addEventListener( 'mouseleave', () => instance.hide() );
el.addEventListener( 'focus', () => instance.show() );
el.addEventListener( 'blur', () => instance.hide() );
return instance;
}
Zweitens das Styling. Statt einer einzelnen CSS-Variable hängt der Button bei mir direkt an den Brand-Variablen des Themes – dieselbe Akzentfarbe, derselbe Fokus-Stil wie überall sonst. Das ist genau der Integrationsvorteil aus dem ersten Abschnitt dieses Beitrags, in der Praxis.
Drittens die Einbindung. Im Theme ist das Like-Modul ein ES-Modul mit einer init( scope )-Funktion, die mein Theme-Bootstrap zusammen mit allen anderen Modulen aufruft – scoped und mehrfach initialisierbar, etwa nach AJAX-Nachladen. Das Repo macht daraus ein eigenständiges Script, das sich selbst verdrahtet, damit es ohne mein Modulsystem läuft.
Beide Versionen erzeugen denselben Like-Button. Der Unterschied ist nur, wie tief dieser im Drumherum eingewoben ist – und genau aus diesem Grund gibt es eine neutrale Basis-Version im Repo.
Die ehrlichen Grenzen
Kein Feature ohne Grenzen, und die sollten benannt werden – sonst verkauft man eine perfekte Lösung, die keine ist.
Der Mehrfach-Schutz lebt im localStorage, nicht auf dem Server. Wenn man den Browser-Speicher leert oder einen anderen Browser öffnet, kann man erneut liken. Das ist kein Versehen, sondern bewusst akzeptiert. Wenn man wirklich hart zählen wollte, käme man um eine serverseitige Sperre per IP oder Benutzer nicht herum – mit allem, was daran hängt, von Speicherung bis Datenschutz.
Auch die Nonce ist für nicht eingeloggte Besucher schwächer, als sie klingt. WordPress bindet eine Nonce an die Benutzer-Session – anonyme Besucher haben aber alle dieselbe (Benutzer-ID 0), wie die Doku selbst festhält. Die Nonce wirkt damit eher als zeitlich begrenztes Token denn als echter CSRF-Schutz. Für einen Like ist das in Ordnung – nur sollte man es nicht als wasserdicht verkaufen.
aria-pressed ist streng genommen ein Attribut für Umschalter, und der Button ist einseitig: einmal geliked, bleibt geliked. Das ist ein bekannter Grenzfall. Ich nehme aria-pressed trotzdem, weil der gelikte Zustand die nützlichste Information für einen Screenreader ist – aber es ist eine Abwägung, keine reine Lehre.
Und schliesslich die Theorie: Zählen zwei Besucher im exakt selben Moment, könnten sich die update_post_meta-Aufrufe theoretisch in die Quere kommen. Bei üblichem Verkehr ist das vernachlässigbar; bei einem Like-Zähler erst recht.
Der vollständige Code
Der vollständige, framework-neutrale Code liegt in einem öffentlichen Repository auf GitHub – inklusive README, Changelog und Lizenz.
Fazit
Der Like-Button selbst ist schnell integriert. Aber Schnelligkeit war nie das Thema. Interessant wird es bei dem, was die Tutorials weglassen: dass der Endpunkt öffentlich ist, dass jemand mit Tastatur oder Screenreader unterwegs sein könnte, dass eine Nonce im Cache veraltet, dass «ein Like pro Browser» eben nur «pro Browser» heisst und nicht mehr.
Nichts davon macht aus dem kleinen «Gefällt mir»-Signal eine harte Statistik – das war auch nie das Ziel. Aber es ist der Unterschied zwischen einem Codeschnipsel, den man irgendwo herkopiert, und etwas, das man selbst verstanden hat und vertreten kann. Genau das ist der Teil, der mir Spass macht und dir vielleicht ein wenig Nutzen bringt.
FAQ
Nein. Ein Like-Button ist eine Handvoll Theme-Funktionen – Markup, ein AJAX-Handler, etwas JavaScript und CSS. Ein Plugin lohnt sich, wenn du ein fertiges Theme nutzt und ohne Aufwand eine Reaktion willst; für ein eigenes Classic Theme ist es Overkill.
Ja, mit etwas Aufwand. Die Mehrfach-Sperre liegt im localStorage des Browsers – wenn man ihn leert oder einen anderen Browser benutzt, kann man erneut liken. Das ist bewusst akzeptiert: Der Zähler ist ein kleines «Gefällt mir»-Signal, keine harte Statistik. Mehr dazu im Abschnitt Cookies oder localStorage.
Meistens ja. Ein Full-Page-Cache kann aber die abgesicherte Nonce «überleben», sodass ein Like abgewiesen wird. Die zwei Auswege stehen im Abschnitt Caching und die Nonce.
Ja. Er ist ein echtes <button> Element, mit Tastatur bedienbar, beschreibt sich über aria-label, hält den gelikten Zustand über aria-pressed und meldet den neuen Stand über eine aria-live-Region an Screenreader. Mehr dazu im Abschnitt Barrierefreiheit.

Kommentare