WP Manifestindependent plugin directory
manifest / analytics / bitter-post-views

Bitter Post Views

Modern post-views counter. Tracks views, shows an admin

★ 0stars

Install

Download source zip No release zip yet

This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/justindouglas16/bitter-post-views/archive/refs/heads/main.zip
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

A modern, lightweight post-views counter for WordPress built on PHP 8+ with a clean namespace architecture. Tracks page views, displays a sortable admin column, and optionally renders counts on the frontend.

Frontend display is off by default — view counting runs silently until you choose to show it.

Features

  • View tracking — counts views via AJAX (cache-friendly) or direct PHP increment
  • Admin column — sortable "Views" column on the Posts list screen
  • Frontend gating — global toggle to show or hide counts from visitors
  • Context controls — choose where counts appear: home, single, page, archive, search, other
  • Shortcode — [views] with optional id, prefix, and suffix attributes
  • Template tag — the_views() for theme developers
  • REST API — read-only views field on /wp/v2/posts
  • No jQuery — admin options page and frontend tracker use vanilla JS
  • Bot filtering — excludes known crawlers from inflating counts
  • Backward compatible — existing themes calling the_views(), get_most_viewed(), or get_least_viewed() continue to work
  • Clean uninstall — removes all post meta and plugin options on deletion

Requirements

  • WordPress 6.0+
  • PHP 8.0+

Installation

  1. Download or clone this repository into wp-content/plugins/bitter-post-views/
  2. Activate the plugin from Plugins → Installed Plugins
  3. Configure settings under Settings → Post Views
cd wp-content/plugins
git clone https://github.com/your-org/bitter-post-views.git

Configuration

Navigate to Settings → Post Views in your WordPress admin.

Setting Description Default
Display views on the frontend Master toggle for all frontend output Off
Count views from Guests only, everyone, or registered users only Everyone
Exclude bots Skip known crawlers and spiders On
Use AJAX Count via async request (works with page caching) On
Display on Home, single posts, pages, archive, search, other All off
Views template Output format using %VIEW_COUNT% token %VIEW_COUNT% views
Most/Least viewed template List item format with %POST_TITLE%, %POST_URL%, %VIEW_COUNT%, %POST_EXCERPT% <li><a href="%POST_URL%">%POST_TITLE%</a> — %VIEW_COUNT%</li>

Note: When the frontend toggle is off, view counting still runs in the background. Counts remain visible in the admin Posts list and through the REST API. Only visitor-facing output is suppressed.

Usage

Admin Column

A Views column appears automatically on the Posts → All Posts screen. Click the column header to sort posts by view count.

Shortcode

[views]
[views id="42"]
[views prefix="(" suffix=" reads)"]
Attribute Description Default
id Post ID (defaults to current post) Current post
prefix Text before the number (empty)
suffix Text after the number views

The shortcode respects the frontend toggle — it returns an empty string when frontend display is disabled.

Template Tag

<?php
// Echo the view count with default formatting.
the_views();

// Return instead of echo.
$count = the_views(display: false);

// Custom prefix and suffix.
the_views(prefix: '(', suffix: ' reads)');

// Bypass the frontend gate (useful in admin or custom contexts).
the_views(always: true);
?>

Query Functions

<?php
// Get the 10 most-viewed posts.
$popular = get_most_viewed();

// Get the 5 least-viewed posts of a custom post type.
$quiet = get_least_viewed([
    'post_type'      => 'book',
    'posts_per_page' => 5,
]);

// Loop through results.
foreach ($popular as $post) {
    $views = get_post_meta($post->ID, 'views', true);
    printf(
        '<a href="%s">%s</a> — %s views<br>',
        get_permalink($post),
        esc_html($post->post_title),
        number_format_i18n((int) $views)
    );
}
?>

REST API

The plugin registers a read-only views field on the post endpoint:

curl https://example.com/wp-json/wp/v2/posts?per_page=5&orderby=meta_value_num&meta_key=views&order=desc

Each post object in the response includes:

{
  "id": 42,
  "title": { "rendered": "Hello World" },
  "views": 1337
}

AJAX Counting (Cache Compatibility)

When Use AJAX is enabled, the plugin enqueues a small vanilla JS script on singular pages. It sends an async POST to admin-ajax.php after page load, making it compatible with full-page caching solutions like WP Super Cache, W3 Total Cache, and Varnish.

The AJAX endpoint is nonce-protected and validates the post ID before incrementing.

JavaScript Hook

After a successful AJAX count, the script calls window.bpvOnCounted(views) if defined. Use this to update a live counter on the page:

window.bpvOnCounted = function (views) {
  const el = document.querySelector(".my-view-counter");
  if (el) {
    el.textContent = views.toLocaleString() + " views";
  }
};

File Structure

bitter-post-views/
├── wp-postviews.php              # Bootstrap, constants, backward-compat wrappers
├── uninstall.php                 # Clean removal of meta + options
├── assets/
│   └── js/
│       ├── postviews-cache.js    # Frontend AJAX tracker (vanilla JS)
│       └── postviews-options.js  # Admin options page interactions (vanilla JS)
└── src/
    ├── Autoloader.php            # PSR-4 autoloader for Bitter\PostViews\
    ├── Plugin.php                # Main orchestrator
    ├── Settings.php              # Options management, defaults, migration
    ├── Admin/
    │   ├── AdminColumns.php      # Posts list column + sorting
    │   └── OptionsPage.php       # Settings page under Settings menu
    ├── Ajax/
    │   └── IncrementViews.php    # AJAX endpoint with nonce + bot checks
    ├── Frontend/
    │   ├── Renderer.php          # Shortcode, content filter, template tag
    │   └── Tracker.php           # Enqueues JS or does direct PHP increment
    └── Rest/
        └── ViewsField.php        # Read-only views field on REST API

Migration from Older Versions

If you are upgrading from an earlier post-views plugin that stored counts in the views post meta key, the migration is seamless:

  • Existing view counts are preserved (same meta key).
  • The frontend_enabled option is set to 0 on first upgrade, so nothing changes visually for your visitors until you explicitly enable it.
  • A version stamp is stored in the options to prevent repeated migrations.

Hooks and Filters

The plugin uses standard WordPress hooks. Key integration points:

Hook Type Purpose
manage_posts_columns Filter Adds the Views column
manage_posts_custom_column Action Renders the count
manage_edit-post_sortable_columns Filter Makes the column sortable
pre_get_posts Action Handles orderby=views
the_content Filter Appends view count to post content
wp_ajax_bpv_increment Action AJAX handler (logged-in)
wp_ajax_nopriv_bpv_increment Action AJAX handler (logged-out)
rest_api_init Action Registers the REST field

Security

  • All AJAX requests require a valid WordPress nonce (bpv_increment_nonce).
  • Post IDs are cast with absint() before any database operation.
  • The options page checks manage_options capability and verifies its own nonce.
  • Bot detection filters common crawler user-agent strings.
  • Uninstall uses $wpdb->delete() with proper type formatting.
  • All new source files declare strict_types=1.

License

GPL-2.0-or-later. See LICENSE for details.