A woocommerce product view counter without a plugin is two small hooks: one to increment a post-meta counter on every product-page render, one to display the current count on the product summary. Works with stock WooCommerce, persists through requests, and gives you a meta_value_num column to sort product listings by popularity. This guide covers the basic setup, cache-plugin compatibility via AJAX, and the filter-out-admin pattern.
Last verified: 2026-04-23 on WooCommerce 9.x with WordPress 6.5. Originally published 2023-05-02, rewritten and updated 2026-04-23.
TL;DR
// Increment on product page render
add_action( 'woocommerce_before_single_product', 'how7o_increment_product_view' );
function how7o_increment_product_view() {
if ( current_user_can( 'edit_posts' ) ) {
return; // skip editors/admins
}
$product_id = get_the_ID();
$count = (int) get_post_meta( $product_id, '_product_view_count', true );
update_post_meta( $product_id, '_product_view_count', $count + 1 );
}
// Display on the product summary
add_action( 'woocommerce_single_product_summary', 'how7o_display_product_view', 25 );
function how7o_display_product_view() {
$count = (int) get_post_meta( get_the_ID(), '_product_view_count', true );
if ( $count > 0 ) {
printf(
'<p class="product-view-count">%s</p>',
esc_html( sprintf( _n( '%d view', '%d views', $count, 'how7o' ), $count ) )
);
}
}
The increment
add_action( 'woocommerce_before_single_product', 'how7o_increment_product_view' );
function how7o_increment_product_view() {
if ( current_user_can( 'edit_posts' ) ) {
return;
}
$product_id = get_the_ID();
$count = (int) get_post_meta( $product_id, '_product_view_count', true );
update_post_meta( $product_id, '_product_view_count', $count + 1 );
}
woocommerce_before_single_product fires once per product-page render. The current_user_can('edit_posts') check skips editors, authors, and admins — their views would inflate counts when they’re editing products. (int) casts the meta value so + 1 works whether the row exists (returns a numeric string) or not (returns '').

The display
add_action( 'woocommerce_single_product_summary', 'how7o_display_product_view', 25 );
function how7o_display_product_view() {
$count = (int) get_post_meta( get_the_ID(), '_product_view_count', true );
if ( $count > 0 ) {
printf(
'<p class="product-view-count">%s</p>',
esc_html( sprintf( _n( '%d view', '%d views', $count, 'how7o' ), $count ) )
);
}
}
Priority 25 slots the counter between the price (priority 20) and the excerpt (priority 30). Common WooCommerce summary priorities: 5 title, 10 rating, 20 price, 30 excerpt, 40 add-to-cart, 50 meta — pick the slot that matches your layout.
The _n() helper picks the singular/plural form — “1 view” vs “42 views” — using WordPress’s translation-aware pluralization.
Ordering products by popularity
$args = array(
'post_type' => 'product',
'posts_per_page' => 12,
'meta_key' => '_product_view_count',
'orderby' => 'meta_value_num',
'order' => 'DESC',
);
$popular = new WP_Query( $args );
Once you’re collecting view counts, you can surface a “Most Viewed” section on shop pages. See ordering posts by meta value for the full WP_Query pattern (including the EXISTS / NOT EXISTS trick for products that haven’t been viewed yet).
Page caching — switch to AJAX
// Register an AJAX endpoint
add_action( 'wp_ajax_how7o_view_product', 'how7o_ajax_product_view' );
add_action( 'wp_ajax_nopriv_how7o_view_product', 'how7o_ajax_product_view' );
function how7o_ajax_product_view() {
if ( empty( $_POST['id'] ) ) {
wp_send_json_error();
}
$product_id = absint( $_POST['id'] );
$count = (int) get_post_meta( $product_id, '_product_view_count', true );
update_post_meta( $product_id, '_product_view_count', $count + 1 );
wp_send_json_success();
}
// Inline JS on the product page
add_action( 'woocommerce_before_single_product', function () {
$product_id = get_the_ID();
?>
<script>
(function() {
fetch('<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'action=how7o_view_product&id=<?php echo (int) $product_id; ?>'
});
})();
</script>
<?php
} );
Page-cache plugins serve pre-rendered HTML, so woocommerce_before_single_product never fires for cached visitors. The AJAX variant loads the cached page, then the browser fires a POST to admin-ajax.php which increments the counter. The fetch is fire-and-forget — no UI updates needed for the increment itself.
Frequently asked questions
Two actions: woocommerce_before_single_product to increment a meta counter, and woocommerce_single_product_summary to display it. Store the count in post meta (_product_view_count) so it persists across requests. No plugin, no extra table — WordPress handles the storage via existing post meta.
No — page caching (WP Rocket, W3 Total Cache, LiteSpeed) serves pre-rendered HTML without firing woocommerce_before_single_product, so the counter stops incrementing. For cache-compatible counting, move the increment to an AJAX endpoint that the product page calls after render, or use a JS beacon to admin-ajax.php. Cached pages and in-PHP counters don’t mix.
Yes for both, usually. Add if ( current_user_can('edit_posts') ) return; to skip editors and admins (their views would inflate the count when editing). For bots, check $_SERVER['HTTP_USER_AGENT'] against a short blocklist (googlebot, bingbot, yandex) or use a library like jaybizzle/crawler-detect. Purely-public counts without either filter drift far from actual interest.
_product_view_count slow down queries? Not on its own. Every view adds one row to wp_postmeta, and WordPress caches it automatically. Ordering a product list by view count (meta_key => '_product_view_count' + orderby => 'meta_value_num') is fast if the meta_value column is indexed, which it is by default. Problems show up only at million-product scale — for those, move counting to a dedicated table.
woocommerce_single_product_summary with priority 25? That hook fires multiple times during the product summary rendering with different priorities — 5 (title), 10 (rating), 20 (price), 30 (excerpt), 40 (add-to-cart), 50 (meta). Priority 25 slots the view count between price and excerpt, which reads naturally. Pick whichever priority matches where you want the count to appear visually.
Related guides
- How to Order Posts by Meta Value in WordPress — the sort pattern for a “most viewed” listing.
- How to Include SKU in WooCommerce Search — another filter-driven product customization.
- How to Automatically Add a Product to Cart on Visit in WooCommerce — another template_redirect style of product-flow tweak.
- How to Deregister or Remove a CSS File in WordPress — hook-priority discipline, same family of tweak.
References
WooCommerce single-product hooks reference: woocommerce.com/document/woocommerce-action-hook-reference.