Skip to content

marcelbest/like-button-php-for-wordpress

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Like button for WordPress (PHP + JS, no plugin)

A small, accessible like button for classic WordPress themes — vanilla fetch, the WordPress AJAX API, and a like count in post meta. No plugin, no jQuery, no build step.

It is a small "did you like this?" signal, not a hard statistic: duplicate likes are kept down client-side via localStorage, while a nonce protects the endpoint server-side. The honest limits of that trade-off are spelled out below.

Full walkthrough (German): https://marcelbest.com/webdesign/2026/like-button-fuer-wordpress-ohne-plugin/ The blog post explains the how and why — and the deliberate trade-offs — in detail; this README is the quick start.

What it does

  • Renders a real <button> with an SVG icon and a small count badge.
  • On click, increments like_count in the post meta over admin-ajax.php.
  • Remembers the liked state per browser in localStorage and restores it on load, so a second click does not count again.
  • Announces the new count to screen readers via a polite aria-live region, with correct singular/plural.
  • Caps the visible count at 99+, server- and client-side.

Requirements

  • A classic theme (PHP functions.php).
  • PHP 7.4+ (developed against 8.x).
  • A browser with fetch and localStorage (all modern browsers).

Installation

  1. Copy the like-button/ folder into your theme, e.g. inc/like-button/ (it contains like-button.php, like-button.js, like-button.css).
  2. Require the PHP from your theme's functions.php:
    require get_theme_file_path( 'inc/like-button/like-button.php' );
  3. Output the button anywhere in a single-post template:
    echo yourtheme_get_like_button( get_the_ID() );
  4. Replace the yourtheme_ function prefix, the yourtheme-like nonce action, the yourthemeLike JS object and the yourtheme text domain with your own namespace.

The assets are enqueued on is_singular() only. Adjust that condition in yourtheme_like_enqueue() if you want the button elsewhere. If you place the folder somewhere other than inc/like-button/, point the asset base at it:

add_filter( 'yourtheme_like_asset_base', fn() => get_theme_file_uri( 'assets/like-button' ) );

Markup it produces

<button type="button" class="btn-like" data-post-id="42"
        aria-label="Like this post" 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>

Only the per-post datum (data-post-id) sits on the element. The endpoint, nonce and labels are passed once for the whole page via wp_localize_script.

Configuration (filters)

Filter Default Purpose
yourtheme_like_meta_key like_count Post meta key for the count
yourtheme_like_asset_base inc/like-button URI Base URL for the JS/CSS
yourtheme_like_button_html generated markup Final escape hatch to alter the button HTML

Labels are translatable through the yourtheme text domain — no filter needed.

Styling

Plain CSS, no framework. Set an accent colour with a custom property:

.btn-like { --like-accent: #e2533b; }

The accent drives the hover, focus and liked states. The visible .like-count badge is decorative (aria-hidden); the real announcement lives in the screen-reader-only .like-status region.

Accessibility

  • Real <button type="button">, not a clickable <div>/<a>.
  • aria-label describes the action; the icon is aria-hidden.
  • aria-pressed reflects the liked state (set client-side, since it only lives in localStorage).
  • The new count is announced once, as a full sentence, via a polite live region — only on a fresh like, never on page load, so screen readers are not spammed.
  • Visible focus state via the accent colour.

Note on aria-pressed: the button is one-way (you cannot un-like), while aria-pressed strictly implies a toggle. This is a known, accepted edge case; the pressed state is still the most useful signal here.

Caching: the nonce gotcha

With a full-page-cache plugin, the nonce baked into the cached HTML can age out — then wp_verify_nonce() fails and the like is rejected. Two ways out:

  • Exclude the button region from the cache (fragment / hole-punching), or
  • fetch a fresh nonce from an uncached request before sending the like.

Without an HTML cache (or with a plain object cache) this is a non-issue.

Either way the rejection is no longer silent: a refused like announces labelError in the live region, so the visitor is not left wondering whether the click registered.

Limits

  • No server-side duplicate protection. The lock lives in localStorage — clearing it, or using another browser, allows another like. Accepted for a light signal; a hard count would need IP/user-based throttling server-side.
  • The nonce is weaker for logged-out visitors. A WordPress nonce binds to a user session; anonymous visitors barely have one, so the nonce acts more like a time-limited token (~24 h) than full CSRF protection. Fine for this feature, but worth knowing before treating it as airtight.
  • Race conditions on update_post_meta are theoretically possible under simultaneous likes; negligible for ordinary traffic.

Inspiration

One of several starting points was Amr AbdElkarem's tutorial, Add an AJAX like button to WordPress without a plugin. This version is a ground-up rewrite that adds escaping, input validation, accessibility, i18n and a localStorage-based lock instead of a cookie.

License

GPL-2.0-or-later. See LICENSE.

About

An accessible AJAX like button for classic WordPress themes — vanilla JS, a post-meta counter, nonce-protected. No plugin, no jQuery, no build step.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages