Install
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.zipA 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 optionalid,prefix, andsuffixattributes - Template tag —
the_views()for theme developers - REST API — read-only
viewsfield 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(), orget_least_viewed()continue to work - Clean uninstall — removes all post meta and plugin options on deletion
Requirements
- WordPress 6.0+
- PHP 8.0+
Installation
- Download or clone this repository into
wp-content/plugins/bitter-post-views/ - Activate the plugin from Plugins → Installed Plugins
- 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_enabledoption is set to0on 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_optionscapability 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.