Hi, Nice to see you again!
Recently I have learned more about WordPress and feel extremely excited about it so I would like to share this positive energy to all of you. That’s why I think it would be fun to make simple plugins… I mean, to have a solution and to build it in a way that is comprehensive would be fun, even though there are plenty of plugins for view counters already!
And this is the episode 01: to make a simple view count plugin. Hopefully I can keep you until the end of the article 🤣
What we will complete in this episode:
- Logs view count of different post type into a custom table, exclude admin and post author from the count and exclude articles that are not published
- Setup a user settings page in admin area to let user select whether or not to display view count and place it at the bottom or top of the article’s content
- Query the top 5 most-viewed posts with a shortcode so that user can include in widgets or theme
- Delete view count row of a post after the post gets deleted
My setup:
- LocalWP to create WordPress test site locally (PHP 8.1.9, MySQL 8.0.16)
- FakerPress plugin to create dummy text
- Twenty Twenty-One theme
- GitHub to store and version-control the plugin folder
Table of contents
- 1. The Structure Of A Plugin
- 2. Making WordPress Custom Table: Logs View Counts of Different Post Types
- 3. Create A Class To Handle Database Insert, Update And Delete
- 4. Build a settings page that user can decide whether to display views count in front end, and where to display it
- 5. Logs view counts when page load
- 6. Display view count snippet on front end
- 7. Display top-viewed articles with shortcode
- 8. Delete view count records when the article got deleted with deleted_post hook
- 9. Add view counts custom column to admin’s posts page views
- 10. [GitHub] Full code
- 11. Key Takeaways and some final thoughts
1. The Structure Of A Plugin
For concepts of a plugin, please refer to WordPress official page about making plugins.
A plugin is a neat way to add custom functions without touching the WordPress core and not even the theme.
Theme should be for style, and plugin for function. Yes we can add custom functions to the functions.php file of the theme as well but after a while it is difficult to scale and extremely annoying to maintain as every kind of functions are stuffed in one file.
For this project, let’s call the plugin AK View Count (yes it is by my name but you can change it to yours also 🤣).
Before starting to code, let’s plan a bit about our plugin. The structure that I suggest:
- [Folder] AK View Count
- [Folder] includes: contains classes to handle custom table and hook functions
- class-ak-view-counter.php: handling hooks and functions that do not directly touch the database
- class-ak-view-counter-db.php: inserting, updating and deleting rows using class $wpdb
- class-ak-view-counter-admin.php: adding a view count as custom column in Pages and Posts admin view
- class-ak-view-counter-settings.php: add a submenu tool page that users can provide settings to turn on/off the display of view count and select the location (if displaying) in front end
- [Folder] templates: contain html template
- top-views.php
- settings.php
- [File] ak-view-counter.php: contains information about the plugin and initial function to create table
- [File – optional but recommended] changelog.md: logs about changes of each version
- [Folder] includes: contains classes to handle custom table and hook functions
Probably good enough… Or we always can change later if there are more optimized way to structure our code which we feel comfortable to work with.
Here’s a screenshot of it (please ignore .idea folder, it is generated by PHPStorm).

Let’s get down to code.
2. Making WordPress Custom Table: Logs View Counts of Different Post Types
There is a great article about making custom table by DeliciousBrain: Creating a Custom Table with PHP in WordPress that I highly recommend to read.
Now you may wonder why we don’t just log the view count by adding a post meta to each posts / pages and just display it? It is much simpler than creating a custom table because:
- Can use at once update_post_meta() and get_post_meta() of WordPress’s built in functions without writing extra codes ro handle database stuff,
- Automatically get deleted when the article is deleted as it is a post meta
Yes, I agree that it is very fast way to make this work, and it should be considered as one method, too. But when there are thousands of posts, a separate table of view counts would make the process of querying and filtering among post types and getting top results faster.
The custom table’s structure would look like:

Based on your need, there could be timestamp column also, in case you want to log the latest timestamp a post gets view, or the publish date of the post so that the most-viewed posts can be queried and filtered by the time it is published.
But in this article, let’s just start with this simple structure, storing only post_id, post_type and views.
To create the table, add a function with register_activation_hook(), to create a view count table when the plugin is activated.
In ak-view-counter.php file:
<?php
/**
* Plugin Name: AK View Counter
* Description: Dead simple counter for WordPress posts and pages
* Version: 1.0.1
* Author: Anh Karppinen
* Text Domain: ak-view-counter
**/
if (!defined('ABSPATH')) exit;
define('AK_VIEW_COUNTER_DIR', plugin_dir_path(__FILE__));
register_activation_hook(__FILE__, 'ak_create_view_count_table');
/**
* Create view count custom table when plugin is activated
*/
function ak_create_view_count_table() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$page_views_table = "CREATE TABLE IF NOT EXISTS `{$wpdb->base_prefix}ak_viewcount` (
viewcount_id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
post_id bigint(20) UNSIGNED NOT NULL,
post_type varchar(100) NOT NULL,
views bigint(20) UNSIGNED NOT NULL,
PRIMARY KEY (viewcount_id),
UNIQUE `post_id` (post_id),
KEY `post_type` (post_type)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($page_views_table);
}
require_once(AK_VIEW_COUNTER_DIR . '/includes/class-ak-view-counter-db.php');
require_once(AK_VIEW_COUNTER_DIR . '/includes/class-ak-view-counter.php');
require_once(AK_VIEW_COUNTER_DIR . '/includes/class-ak-view-counter-settings.php');
require_once(AK_VIEW_COUNTER_DIR . '/includes/class-ak-view-counter-admin.php');
Now go back to WP dashboard, install the plugin and activate it. Then go to your database and check if the custom table is successfully created.

3. Create A Class To Handle Database Insert, Update And Delete
In file class-ak-view-counter-db.php, let’s visual the handler like:
<?php
/**
* Handling view count data from custom table
*/
if (!defined('ABSPATH')) exit;
class AK_View_Counter_Db {
const VIEW_COUNT_TABLE = 'ak_viewcount';
public static function count_up(int $post_id, string $post_type) {}
public static function get_view_count(int $post_id) {}
public static function get_top_views(string $post_type) {}
public static function delete_view_count_row(int $post_id) {}
}
For counting up, we’ll write a SQL command to insert 1 new view when no duplicated post_id is found. As we set the post_id to be UNIQUE in the database schema, we can use ON DUPLICATE KEY UPDATE, which means if no same unique key was found: create new row; if found: update:
/**
* Counts up
* @param int $post_id
* @param string $post_type
* @return false|int
*/
public static function count_up(int $post_id, string $post_type) {
if(empty($post_id) || empty($post_type)) return false;
global $wpdb;
$sql = "INSERT INTO " . $wpdb->base_prefix . self::VIEW_COUNT_TABLE . " (post_id, post_type, views) VALUES (%d, %s, %d) ON DUPLICATE KEY UPDATE views = views + %d";
return $wpdb->query( $wpdb->prepare($sql, $post_id, $post_type, 1, 1));
}
For the rest of this database middleman we can use functions of $wpdb class.
/**
* Get the view count of a specific post
* @param int $post_id
* @return false|int
*/
public static function get_view_count(int $post_id) {
if(empty($post_id)) return false;
global $wpdb;
$query = $wpdb->get_results($wpdb->prepare("SELECT views FROM " . $wpdb->base_prefix . self::VIEW_COUNT_TABLE . " WHERE post_id = %d",
$post_id
), ARRAY_A);
if(empty($query)) return 0;
return (int) wp_list_pluck($query, 'views')[0];
}
Function to get the top views (maximum 5):
/**
* Get top five most-viewed by post type
* @param string $post_type
* @return array
*/
public static function get_top_views(string $post_type = '') {
global $wpdb;
$table = $wpdb->base_prefix . self::VIEW_COUNT_TABLE;
if($post_type) {
$top_views_query = $wpdb->prepare("SELECT post_id, views FROM " . $table . " WHERE post_type = %s ORDER BY views DESC LIMIT 5", $post_type);
} else {
$top_views_query = "SELECT post_id, views FROM " . $table . " ORDER BY views DESC LIMIT 5";
}
return $wpdb->get_results($top_views_query, ARRAY_A);
}
And the last one to delete a row in our table:
/**
* Remove the row of view count by post id
* @param int $post_id
* @return false|int
*/
public static function delete_view_count_row(int $post_id) {
if(empty($post_id)) return false;
global $wpdb;
return $wpdb->delete($wpdb->base_prefix . self::VIEW_COUNT_TABLE, ['post_id' => $post_id], ['%d']);
}
So that’s it, that’s all the functions to communicate with the custom table in the database! We can later use them through out the plugin, in other classes.
The $wpdb class by default return an object. I prefer array so I set it to ARRAY_A, but you don’t have to. However, if no ARRAY_A provided, you need to access data like $tv->post_id instead of $tv[‘post_id’].
4. Build a settings page that user can decide whether to display views count in front end, and where to display it
It is quite straightforward to make a settings page. All we need to do is to hook into admin_init and admin_menu to add our custom settings page under the Tools in admin area and link the form input’s submission to wp_options table.
In class-ak-view-counter-settings.php, add these code:
<?php
/**
* Handling user settings / options
*/
if(!defined('ABSPATH')) exit;
class AK_View_Counter_Settings {
public static function init() {
add_action('admin_menu', array(__CLASS__, 'add_view_count_option_page'));
add_action('admin_init', array(__CLASS__, 'register_options'));
}
/**
* Add an option page that user can select the location of the view counter
* Page can be access as a sub-menu under Tools Menu
*/
public static function add_view_count_option_page() {
add_submenu_page(
'tools.php',
'AK View Counter',
'AVC Settings',
'administrator',
'ak-view-counter-settings',
array(__CLASS__, 'register_counter_settings')
);
}
/**
* Include settings page HTML template
*/
public static function register_counter_settings() {
include_once(AK_VIEW_COUNTER_DIR . '/templates/settings.php');
}
/**
* Add recognization for setting fields that will be added to options
*/
public static function register_options() {
register_setting('ak-view-count', 'ak_view_count_display');
register_setting('ak-view-count', 'ak_view_count_location');
}
}
AK_View_Counter_Settings::init();
In template file settings.php:
<?php
/**
* Settings page UI
* Let user turns on/off view counter from front end and selects the location of view counter
*/
if(!defined('ABSPATH')) exit;
$is_display = get_option('ak_view_count_display');
$display_position = get_option('ak_view_count_location');
?>
<div class="wrap">
<h1>AK View Counter Settings</h1>
<form method="post" action="options.php">
<?php settings_fields('ak-view-count'); ?>
<?php do_settings_sections('ak-view-counter-settings'); ?>
<table class="form-table">
<tr valign="top">
<th scope="row">Display</th>
<td>
<input type="checkbox"
name="ak_view_count_display"
value="1"
<?php if($is_display) echo 'checked'; ?> />
</td>
</tr>
<tr valign="top">
<th scope="row">Location</th>
<td>
<input type="radio"
id="ak_view_count_location"
name="ak_view_count_location"
value="top"
<?php if($display_position === 'top') echo 'checked'; ?> />
<label for="ak_view_count_location">Top</label><br>
<input type="radio"
id="ak_view_count_location"
name="ak_view_count_location"
value="bottom"
<?php if($display_position === 'bottom') echo 'checked'; ?> />
<label for="ak_view_count_location">Bottom</label>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
</div>
And now if you go to Dashboard > Tools, you can see the submenu here.

Confirm that when changing the Display and Location, and clicking Submit, the page reloads and successfully remember your choice. To have a deeper look, let’s access the database and see that the option has been saved nicely.

Later on, we will access these settings to decide the condition to show the view count.
5. Logs view counts when page load
Now that we have completed building the database functions and settings page, let’s come to the main part.
First, let’s define the skeleton to class-ak-view-counter.php, which will defines functions to:
- Count up,
- Display view counts in front end,
- Register shortcodes for listing top views articles,
- Remove view count row after an article is deleted
For those functions, the WordPress hooks that we are going to use:
- wp: fires once the WordPress environment has been set up, when we can access the post by get_the_ID()
- deleted_post: fires after a post has been deleted from the database (not trash)
- init: fires after WordPress finish loading, to register the shortcode
- the_content: a filter to modify the article’s content before displaying it, to put in our views snippet
Again, let’s first wrap everything up in a class.
<?php
/**
* Register view count into database
*/
if(!defined( 'ABSPATH')) exit;
if(!class_exists('AK_View_Counter_Db')) return;
class AK_View_Counter {
public static function init() {
add_action('wp', array(__CLASS__, 'count_up'));
add_action('deleted_post', array(__CLASS__, 'remove_view_count'));
add_action('init', array(__CLASS__, 'register_top_view_shortcode'));
add_filter('the_content', array(__CLASS__, 'show_views'));
}
/**
* Increase views
* Happens after WP environment has finished setup and is deciding which template to load
* Post ID and post type are accessible at this point
*/
public static function count_up() {
// Do not count if this is admin area or not single page
// Do not count if page author or admin is visiting
// Do not count posts that are not published
// Finally call for count_up function
}
/**
* Display html code to show view count in front end
*/
public static function show_views($content) {
// Don't show if it is not a single page
// Don't show if user selects not to show
}
/**
* Register a shortcode so that user can include it in for example widgets
* Shortcode: [ak-top-views]
*/
public static function register_top_view_shortcode() {
add_shortcode( 'ak-top-views', array(__CLASS__, 'display_top_views') );
}
/**
* Get shortcode attributes and display the list on front end
* @param $atts
* @return false|string
*/
public static function display_top_views($atts) {}
/**
* Delete the view count data together with the deletion of a post
* @params $post_id
*/
public static function remove_view_count($post_id) {}
}
AK_View_Counter::init();
Following the plan, function count_up would look like this:
/**
* Increase views
* Happens after WP environment has finished setup
* Post ID and post type are accessible at this point
*/
public static function count_up() {
// Do not count if this is admin area or not single page
if(is_admin() || ! is_singular()) return;
$post_id = get_the_ID();
if(!$post_id) return;
// Do not count if page author or admin is visiting
$post_author = get_post_field('post_author', $post_id);
$current_user_id = get_current_user_id();
if($current_user_id == $post_author) return;
// Do not count posts that are not published
$post_status = get_post_status();
if($post_status !== 'publish') return;
// Finally call for count_up function
AK_View_Counter_Db::count_up($post_id, get_post_type());
}
At this moment, the database should log view count everytime a post / page is loaded.

6. Display view count snippet on front end
Now we have the data, how to show up on front end in a single post? We already create a hook function:
add_filter( 'the_content', array(__CLASS__, 'show_views' ) );
Now map it:
/**
* Display html code to show view count in front end
* @params $content
*/
public static function show_views($content) {
// Don't show if it is not a single page
if(!is_singular()) return $content;
// Don't show if user wants to hide
$is_display = get_option('ak_view_count_display');
if(!$is_display) return $content;
$location = get_option('ak_view_count_location');
$views = AK_View_Counter_Db::get_view_count(get_the_ID());
$view_count_text = "<div class='ak-view-counter'>Views: " . $views . '</div>';
if($location) {
if($location === 'top') {
return $view_count_text . $content;
} else {
return $content . $view_count_text;
}
}
return $content;
}
Try to test changing the Display and Location in Settings page. It should display / not display before / after the post content like selected. If not… um there probably are some bugs around.

7. Display top-viewed articles with shortcode
Show the template of top views:
/**
* Display the list on front end
* @param $atts
* @return false|string
*/
public static function display_top_views($atts) {
$atts = shortcode_atts( array(
'post_type' => '',
), $atts );
$top_view = AK_View_Counter_Db::get_top_views($atts['post_type']);
if(!$top_view) echo "No views.";
// Setup the array which contain titles and links of selected top views
$top_view_articles = [];
foreach($top_view as $tv) {
$top_view_articles[] = array(
'title' => apply_filters('the_title', get_the_title($tv['post_id'])),
'link' => esc_url(get_the_permalink($tv['post_id'])),
'view' => (int) $tv['views']
);
}
ob_start();
include(AK_VIEW_COUNTER_DIR . '/templates/top-views.php');
return ob_get_clean();
}
The template for top views (top-views.php):
<?php
/**
* Top view count shortcode template
*/
if(!defined('ABSPATH')) exit;
if($top_view_articles) :
?>
<ul>
<?php foreach($top_view_articles as $key=>$articles) : ?>
<li>
<?php echo $key + 1; ?>.
<a href="<?php echo $articles['link']; ?>">
<?php echo $articles['title']; ?>
</a>
<li>
<?php endforeach; ?>
</ul>
<?php endif;
After this step, the plugin should be successfully display the top five articles where the shortcode [ak-top-views] is inputted. Let’s go to Widgets section, select a Shortcode type of widget and add [ak-top-views] there. It should show up where the widget is.

8. Delete view count records when the article got deleted with deleted_post hook
When a post / page / any custom post type gets deleted from the database, we should remove the record also from view counts table, so that the database don’t get bloated with unused records after a time of using.
We already provide a function for deleted_post hook:
add_action( 'deleted_post', array(__CLASS__, 'remove_view_count' ) );
And we can map the action with the removal action in AK_View_Counter_Db class.
/**
* Delete the view count data together with the deletion of a post
* @params $post_id
*/
public static function remove_view_count($post_id) {
AK_View_Counter_Db::delete_view_count_row($post_id);
}
It’s quite easy to test the function. Just try to delete (not just trash) an article which already have results in wp_ak_viewcount table and the delete actions should remove the row with corresponding post_id also.
9. Add view counts custom column to admin’s posts page views
The last thing to do is to add into admin column view, a custom column that list the view counts. It would look like this column:

To be able to achieve this, we will use two hooks in admin area. For post:
- manage_posts_columns: filters the columns displayed in the Posts list table.
- manage_posts_custom_column: fires when WordPress renders each column in Posts list table
And for page:
- manage_page_posts_columns
- manage_page_posts_custom_column
If you have another custom post type, do it for them also:
- manage_{custom-post-type-slug}_posts_custom_column
- manage_{custom-post-type-slug}_posts_custom_column
In class-ak-view-counter-admin.php file:
<?php
/**
* Handling admin area
*/
if ( ! defined( 'ABSPATH' ) ) exit;
if ( ! class_exists('AK_View_Counter_Db')) return;
class AK_View_Counter_Admin {
public static function init() {
add_filter('manage_posts_columns', array(__CLASS__, 'add_view_count_column'));
add_filter('manage_page_posts_columns', array(__CLASS__, 'add_view_count_column'));
add_action('manage_posts_custom_column', array(__CLASS__, 'view_count_column_content'), 10, 2);
add_action('manage_page_posts_custom_column', array(__CLASS__, 'view_count_column_content'), 10, 2);
}
/**
* Register view count column in admin view
*/
public static function add_view_count_column($columns) {
$columns['view_count'] = 'Views';
return $columns;
}
/**
* Put values to view count column
*/
public static function view_count_column_content($column, $post_id) {
if('view_count' === $column) {
echo AK_View_Counter_Db::get_view_count($post_id);
}
}
}
AK_View_Counter_Admin::init();
That’s it! Come back to the listing table to see that the Views column has been there!
10. [GitHub] Full code
GitHub repo: https://github.com/vuongngocanh189/ak-view-count-plugin/blob/main/README.md
11. Key Takeaways and some final thoughts
After this articles, we have practiced:
- How to create custom table and handle database actions with $wpdb class
- How to create a settings page in WordPress
- How to create a shortcode to show top viewed articles
- How to use WordPress hooks in admin area and in front end to add our custom function
Please notice this is just like a starting brick to a ready-to-use plugin. To make it ready for production, it would need to be more secure, for example to avoid counts from bot visits, or to limit the views from each IP address, and so on. And of course, we haven’t made any styling at all.
But I hope you enjoy this and don’t be hesitate to join the discussion!
Leave a Reply