How to Show Post Views Count in WordPress Without a Plugin

September 11, 2025
How to Show Post Views Count in WordPress Without a Plugin

You can track and display post views using a few lines of code—no plugin needed. The idea is simple: store a counter in post meta, increment it on each single-post view (with basic bot/duplicate protection), and output the value in your theme.


Step 1: Count Views (functions.php or a small must-use plugin)

This snippet increments a post_views meta key when a single post is viewed. It skips obvious bots and avoids double-counting with a short-lived cookie per post.

<?php
/**
 * Increment post views safely.
 */
function my_count_post_views() {
    if ( is_admin() || is_feed() || is_preview() ) {
        return;
    }

    if ( ! is_singular( 'post' ) ) {
        return;
    }

    global $post;
    if ( empty( $post ) || empty( $post->ID ) ) {
        return;
    }

    // --- Basic bot check (very light) ---
    $ua = isset($_SERVER['HTTP_USER_AGENT']) ? strtolower($_SERVER['HTTP_USER_AGENT']) : '';
    $bots = array( 'bot', 'crawl', 'spider', 'slurp', 'facebookexternalhit', 'mediapartners-google' );
    foreach ( $bots as $needle ) {
        if ( strpos( $ua, $needle ) !== false ) {
            return;
        }
    }

    // --- Avoid double count for the same visitor (cookie per post) ---
    $cookie_key = 'pv_' . $post->ID;
    $already    = isset($_COOKIE[$cookie_key]);
    if ( $already ) {
        return;
    }

    // Lifetime for "no double count": 12 hours (adjust)
    $expire = time() + 12 * HOUR_IN_SECONDS;
    setcookie( $cookie_key, '1', $expire, COOKIEPATH ?: '/', COOKIE_DOMAIN, is_ssl(), true );

    // --- Increment meta counter ---
    $count = (int) get_post_meta( $post->ID, 'post_views', true );
    $count++;
    update_post_meta( $post->ID, 'post_views', $count );
}
add_action( 'wp', 'my_count_post_views' );

Why this works: The wp action runs after WordPress resolves the main query, so is_singular('post') is reliable. The cookie prevents inflating counts from refreshes. The bot check is intentionally minimal—good enough for basic filtering without heavy CPU cost.


Step 2: Output the Views Anywhere (template tag + shortcode)

<?php
/**
 * Get formatted post views.
 */
function my_get_post_views( $post_id = null ) {
    $post_id = $post_id ?: get_the_ID();
    $count   = (int) get_post_meta( $post_id, 'post_views', true );
    return number_format_i18n( $count );
}

/**
 * Echo helper (for templates).
 */
function my_the_post_views( $label = 'Views' ) {
    echo '<span class="post-views">' . esc_html( $label ) . ': ' . esc_html( my_get_post_views() ) . '</span>';
}

/**
 * Shortcode: [post_views label="Views"]
 */
function my_post_views_shortcode( $atts ) {
    $atts  = shortcode_atts( array( 'label' => 'Views' ), $atts, 'post_views' );
    return '<span class="post-views">' . esc_html( $atts['label'] ) . ': ' . esc_html( my_get_post_views() ) . '</span>';
}
add_shortcode( 'post_views', 'my_post_views_shortcode' );

Use it:

  • In templates (e.g., single.php): <?php my_the_post_views(); ?>
  • In the editor: [post_views label="Reads"]

Step 3: (Optional) Show a Views Column in Admin & Make It Sortable

<?php
// Add column.
function my_views_column_head( $columns ) {
    $columns['post_views'] = 'Views';
    return $columns;
}
add_filter( 'manage_post_posts_columns', 'my_views_column_head' );

// Fill column.
function my_views_column_content( $column, $post_id ) {
    if ( $column === 'post_views' ) {
        echo esc_html( my_get_post_views( $post_id ) );
    }
}
add_action( 'manage_post_posts_custom_column', 'my_views_column_content', 10, 2 );

// Make sortable.
function my_views_column_sortable( $columns ) {
    $columns['post_views'] = 'post_views';
    return $columns;
}
add_filter( 'manage_edit-post_sortable_columns', 'my_views_column_sortable' );

// Sort logic.
function my_views_orderby( $query ) {
    if ( ! is_admin() || ! $query->is_main_query() ) return;
    if ( $query->get( 'orderby' ) === 'post_views' ) {
        $query->set( 'meta_key', 'post_views' );
        $query->set( 'orderby', 'meta_value_num' );
    }
}
add_action( 'pre_get_posts', 'my_views_orderby' );

Step 4: (Optional) List “Most Viewed” Posts on the Frontend

<?php
$popular = new WP_Query( array(
    'post_type'      => 'post',
    'posts_per_page' => 5,
    'meta_key'       => 'post_views',
    'orderby'        => 'meta_value_num',
    'order'          => 'DESC',
) );

if ( $popular->have_posts() ) :
    echo '<ol class="popular-posts">';
    while ( $popular->have_posts() ) : $popular->the_post();
        echo '<li><a href="' . esc_url( get_permalink() ) . '">' . esc_html( get_the_title() ) . '</a> (' . esc_html( my_get_post_views() ) . ')</li>';
    endwhile;
    echo '</ol>';
    wp_reset_postdata();
endif;
?>

Step 5: Quick Styles (Optional)

.post-views { color:#555; font-size:0.95rem; }
.popular-posts { margin:0; padding-left:1.2em; }

Tips & Notes

  • Caching: If you use full-page caching, the counter may not run on cached hits. Consider AJAX or server-side bypasses for the counter if accuracy is critical.
  • Privacy: This approach stores only an integer per post. It doesn’t track users.
  • Performance: Post meta is fine for moderate traffic. For very high traffic, consider a transient or an external store (e.g., Redis) and batch-flush to post meta.
  • Scope: The code counts only single post views (is_singular('post')). Adjust if you need to count pages or CPTs.

Summary

  1. Add a counter that increments a post_views meta key on single-post views, with cookie & bot guard.
  2. Display counts via a template tag or the [post_views] shortcode.
  3. (Optional) Surface a sortable “Views” column in the admin and build “Most Viewed” lists.

That’s it—lightweight, flexible, and no plugin required.

Avatar

Written by

satoshi

I’ve been building and customizing WordPress themes for over 10 years. In my free time, you’ll probably find me enjoying a good football match.