How To Display a Word Count Box in the WP Admin Dashboard

⏲️ Estimated reading time: 28 min

Table of Contents

Track your WordPress publishing momentum with a Dashboard widget that shows your total word count across published posts, plus progress bars for multiple goals like 1M, 2M, and 5M words. This guide explains what the plugin does, how it works, how to install it, and how to configure it safely.


Display a Word Count Box

If you publish often, you eventually ask a simple question: “How many words did I publish on this site?”
WordPress does not show a global total by default. You can count per post, but that becomes useless when you have hundreds of posts.

This plugin fixes that problem in a clean way.

The HZ Total Word Count Dashboard plugin adds a Dashboard widget that shows:

  • Total words across published posts
  • Total number of published posts included
  • Average words per post
  • Progress bars toward up to three custom goals
  • One-click tools to refresh totals or recount everything

It stays fast because it does not recount every post on every load. Instead, it stores each post’s word count in post meta and sums them with one database query.


What This Plugin Does

This plugin creates a Dashboard widget in WP Admin → Dashboard that displays your publishing progress in a motivational, measurable way.

It focuses on practical metrics:

  • Total Word Count (all published posts)
  • Post Count (how many posts contribute)
  • Average Words Per Post (quick quality indicator)
  • Three Progress Bars (example goals: 1,000,000 / 2,000,000 / 5,000,000 words)

It also adds a settings field under Settings → Reading where you can edit those goals.


Why This Plugin Is Efficient

Many “word count” plugins are slow because they do this:

  • Load many posts
  • Parse content repeatedly
  • Count words live on every page load

That approach hurts admin performance.

This plugin avoids that with two smart choices:

  • Stores per-post word count in post meta (_hz_word_count)
  • Caches totals in a transient for 6 hours (hz_twc_total_words_v140)

That means the Dashboard widget can load quickly, even on big sites.


Who This Plugin Is For

This plugin is perfect if you:

  • Run a content site and publish often
  • Build a long-term SEO strategy and need consistency tracking
  • Want “gamified” goals like 1M or 5M words
  • Import posts sometimes and need a recount tool
  • Want speed and clean admin UX

Plugin Overview

The plugin header tells WordPress what this plugin is:

Plugin Header Explained

  • Plugin Name: HZ Total Word Count Dashboard
  • Description: Dashboard widget + progress goals + meta-based counting + recount tool
  • Version: 1.4.0
  • Text Domain: hz-twc for translations
  • License: GPLv2 or later (safe for WordPress ecosystem)

Safety Guard: Blocking Direct Access

The first safety check prevents direct file access:

  • If WordPress is not loaded (ABSPATH not defined), the plugin stops.
  • This is standard security hygiene for plugins.

The Plugin Architecture

The entire plugin lives in a class:

After donating, you will be redirected back and receive a download link (valid for 10 minutes).


Main Class: HZ_Total_Word_Count_Dashboard

The class organizes everything and prevents function name collisions.

It contains:

  • Constants (meta key, option name, nonce, transient key, capability)
  • Hooks (actions for saving posts, dashboard setup, AJAX)
  • Settings registration (adds fields to Reading settings)
  • Word count logic
  • Dashboard widget HTML output
  • AJAX handlers
  • Asset loading (CSS/JS on Dashboard only)
  • Migration logic (older option support)

Constants Explained

These constants define “fixed keys” used across the plugin:

META_KEY: _hz_word_count

This is where each post’s word count is saved.

Each published post gets a meta value like:

  • Post ID 123 → _hz_word_count = 1845

This makes totals fast to calculate.

OPTION_GOALS: hz_twc_goals

This stores your three goals in the options table as an array:

  • [1000000, 2000000, 5000000]

You can change them in Settings.

NONCE: hz_twc_nonce

This string is used to create and validate nonces for AJAX buttons.

It prevents unauthorized requests.

TRANSIENT_TOTAL: hz_twc_total_words_v140

This is the cache key for the totals array.

The v140 suffix is a clever trick.
If you change the data format in a future version, you can change the transient key and avoid conflicts.

CAP: edit_posts

This capability controls who can see and use the widget actions.

Editors and admins can usually pass it.
Subscribers cannot.

Hooks and When They Run

The constructor registers hooks that connect WordPress events to plugin logic:

admin_init: Register Settings

When WP Admin initializes settings:

  • The plugin registers the “goals” option.
  • It adds a settings field under Settings → Reading.

save_post: Keep Word Count Meta Updated

Whenever a post is saved, the plugin:

  • Ignores revisions
  • Only targets the post post type
  • If the post is published, it counts words and saves meta
  • If the post is not published, it removes meta
  • Clears the totals transient (so totals refresh)

This ensures totals stay accurate.

deleted_post: Cleanup Meta and Cache

If a post gets deleted:

  • Removes its _hz_word_count meta
  • Clears transient cache

transition_post_status: Invalidate Cache on Status Changes

If a post changes status (draft → publish, publish → trash, etc.):

  • Clears transient cache

This matters because totals only count published posts.

wp_dashboard_setup: Add the Widget

This hook is where WordPress lets you register dashboard widgets.

The plugin creates a widget titled:

  • “Total Word Count Progress”

wp_ajax_*: AJAX Buttons

Two AJAX actions exist:

  • hz_twc_recount_all → recount all published posts
  • hz_twc_clear_cache → clear transient cache and reload totals

admin_enqueue_scripts: Load CSS/JS Only on Dashboard

The plugin loads assets only when $hook === 'index.php'.

That means:

  • Dashboard loads the styling and button behavior
  • Other admin pages stay untouched and fast

plugins_loaded: Option Migration

The plugin checks if an old option exists:

  • hz_twc_goal (single goal)

If it finds it, it migrates it into the new 3-goal system.

This protects older installs.

How Word Counting Works

The most important part is the counting logic:

count_words_from_content()

This method transforms post content into clean text:

  • Removes shortcodes
  • Removes HTML tags
  • Normalizes whitespace
  • Splits by spaces and counts words

This gives a realistic “human reading” word count.

What It Counts and What It Ignores

It counts:

  • Regular text content
  • Text inside blocks (once rendered as content)

It ignores:

  • Shortcodes (because they may output dynamic content)
  • HTML structure
  • Empty whitespace

This is usually what you want for editorial tracking.

Why It Stores Word Counts in Post Meta

Storing counts in meta gives you three big advantages:

  • You avoid recounting the same post repeatedly
  • You can sum all meta values quickly
  • You can keep totals accurate by updating only when posts change

It also makes the recount tool simple.

Keeping Meta in Sync With Post Status

The plugin updates meta on post save:

Published Posts

If a post is publish, the plugin:

  • Counts words from post_content
  • Saves _hz_word_count

Non-Published Posts

If a post is draft, pending, private, trashed, etc.:

  • Deletes _hz_word_count

So totals never include non-public posts.

How Totals Are Calculated

The totals are fetched with one SQL query:

  • It joins posts and postmeta
  • It sums meta values
  • It counts the number of posts

Then it builds a totals array:

  • total_words
  • post_count
  • average
  • generated timestamp

Why a Single Query Matters

With thousands of posts, you want:

  • One database request
  • No PHP loops over all posts on every widget load

This plugin does that.

Transient Cache: Why It Exists

Totals are cached for 6 hours.

That means:

  • Dashboard loads fast
  • You avoid unnecessary database work

The cache is cleared when posts change, so the value stays accurate most of the time.

You can also force refresh manually.

Dashboard Widget: What Users See

The widget includes:

Total Words and Average

It displays:

  • Total words published
  • Average words per post
  • Post count included in totals

Numbers are formatted using number_format_i18n() so it matches site locale.

Progress Bars for Three Goals

The widget shows three progress bars:

  • Goal 1
  • Goal 2
  • Goal 3

Defaults are:

  • 1,000,000 words
  • 2,000,000 words
  • 5,000,000 words

You can change them in Settings.

Progress Bar Design

Each bar uses:

  • Width based on percent complete
  • A gradient background that changes as you progress
  • A percent label inside the bar
  • ARIA attributes for accessibility

It stays readable and motivates progress.

Widget Buttons and Actions

The widget adds three actions:

Refresh Totals

This triggers hz_twc_clear_cache.

It:

  • Deletes the transient
  • Regenerates totals
  • Returns JSON response
  • Updates UI via JS

Use this when you suspect the cache is stale.

Recount All Posts Now

This triggers hz_twc_recount_all.

It:

  • Queries all published posts
  • Counts words per post again
  • Updates meta for each
  • Clears transient
  • Returns the number of processed posts

This is perfect after:

  • Bulk imports
  • Content migrations
  • Massive edits
  • Theme or block changes that may affect stored content

This link opens:

  • Settings → Reading
  • Anchors to the goal section

That keeps configuration easy.

Settings: How to Configure the Goals

This plugin registers its goals setting under the Reading settings page.

Where to Find the Settings

Go to:

  • WP Admin → Settings → Reading

Look for:

  • Total Word Goals (up to three)

You will see three number inputs:

  • Goal 1
  • Goal 2
  • Goal 3

What Values Are Allowed

Sanitization rules:

  • Values must be integers
  • Minimum is 1000
  • Step is 1000
  • Any empty or invalid value becomes at least 1000

This prevents weird values like 0 or negative numbers.

Example Goal Setups

You can use different strategies:

Beginner Blogger Goals

  • 50,000
  • 100,000
  • 250,000

Serious SEO Publisher Goals

  • 500,000
  • 1,000,000
  • 2,000,000

“Lifetime Project” Goals

  • 1,000,000
  • 3,000,000
  • 10,000,000

The bars stay accurate as long as goals are realistic.

🛠️Install Guide: Step-by-Step

You can install this plugin like any normal WordPress plugin.

Method 1: Install as a Regular Plugin

This is the best method for most sites.

Step 1: Create a Plugin Folder

Inside your WordPress site:

  • wp-content/plugins/

Create a folder:

  • hz-total-word-count-dashboard

Step 2: Create the Main Plugin File

Inside that folder, create:

  • hz-total-word-count-dashboard.php

<?php
/**
 * Plugin Name: HZ Total Word Count Dashboard
 * Plugin URI: https://helpzone.blog/how-to-display-a-word-count-box-in-the-wp-admin-dashboard/
 * Description: Dashboard widget showing total word count across published posts and progress toward multiple goals (1M, 2M, 5M by default). Efficient: per-post word counts are stored in post meta and summed quickly. Includes a one-click recount tool.
 * Version: 1.4.0
 * Author: HelpZone
 * Author URI: https://helpzone.blog
 * License: GPLv2 or later
 * Text Domain: hz-twc
 */

if ( ! defined('ABSPATH') ) exit;

class HZ_Total_Word_Count_Dashboard {
    const META_KEY        = '_hz_word_count';
    const OPTION_GOALS    = 'hz_twc_goals'; // int[] of 3 goals
    const NONCE           = 'hz_twc_nonce';
    const TRANSIENT_TOTAL = 'hz_twc_total_words_v140';
    const CAP             = 'edit_posts';

    public function __construct() {
        // Settings
        add_action('admin_init', [$this, 'register_settings']);

        // Keep meta in sync
        add_action('save_post', [$this, 'update_post_wordcount_meta'], 20, 3);
        add_action('deleted_post', [$this, 'delete_post_wordcount_meta'], 10, 1);
        add_action('transition_post_status', [$this, 'maybe_invalidate_totals'], 10, 3);

        // Dashboard widget
        add_action('wp_dashboard_setup', [$this, 'register_widget']);

        // Ajax: recount + clear cache
        add_action('wp_ajax_hz_twc_recount_all', [$this, 'ajax_recount_all']);
        add_action('wp_ajax_hz_twc_clear_cache', [$this, 'ajax_clear_cache']);

        // Enqueue admin assets on Dashboard only
        add_action('admin_enqueue_scripts', [$this, 'admin_assets']);

        // Upgrade routine to migrate old single-goal option if present
        add_action('plugins_loaded', [$this, 'maybe_migrate_options']);
    }

    /** SETTINGS **/
    public function register_settings() {
        register_setting('reading', self::OPTION_GOALS, [
            'type' => 'array',
            'sanitize_callback' => function($val){
                $vals = is_array($val) ? array_values($val) : [];
                $out = [];
                foreach ([0,1,2] as $i) {
                    $num = isset($vals[$i]) ? absint($vals[$i]) : 0;
                    $out[$i] = max(1000, $num);
                }
                return $out;
            },
            'default' => [1000000, 2000000, 5000000],
        ]);

        add_settings_field(
            self::OPTION_GOALS,
            __('Total Word Goals (up to three)', 'hz-twc'),
            [$this, 'render_goals_field'],
            'reading',
            'default',
            [ 'label_for' => self::OPTION_GOALS ]
        );
    }

    public function render_goals_field() {
        $goals = $this->get_goals();
        echo '<div id="hz-twc-goals">';
        for ($i=0; $i<3; $i++) {
            $label = sprintf( esc_html__('Goal %d', 'hz-twc'), $i+1 );
            printf(
                '<p><label>%s: <input type="number" min="1000" step="1000" name="%1$s[%2$d]" value="%3$d" class="small-text" /></label></p>',
                esc_attr(self::OPTION_GOALS),
                $i,
                (int)$goals[$i]
            );
        }
        echo '<p class="description">'.esc_html__('Defaults: 1,000,000; 2,000,000; 5,000,000 words.', 'hz-twc').'</p>';
        echo '</div>';
    }

    private function get_goals() {
        $goals = get_option(self::OPTION_GOALS, [1000000, 2000000, 5000000]);
        if (!is_array($goals) || count($goals) !== 3) $goals = [1000000, 2000000, 5000000];
        // Normalize
        return [
            max(1, (int)$goals[0]),
            max(1, (int)$goals[1]),
            max(1, (int)$goals[2]),
        ];
    }

    /** WORD COUNT HELPERS **/
    public static function count_words_from_content($content) {
        $text = wp_strip_all_tags( strip_shortcodes( (string)$content ) );
        $text = trim( preg_replace('/\s+/u', ' ', $text) );
        if ($text === '') return 0;
        $words = preg_split('/\s+/u', $text);
        return is_array($words) ? count($words) : 0;
    }

    public function update_post_wordcount_meta($post_ID, $post, $update) {
        if ( wp_is_post_revision($post_ID) || 'post' !== $post->post_type ) return;

        if ( 'publish' === $post->post_status ) {
            $content = get_post_field('post_content', $post_ID);
            $wc = self::count_words_from_content($content);
            update_post_meta($post_ID, self::META_KEY, $wc);
        } else {
            delete_post_meta($post_ID, self::META_KEY);
        }
        delete_transient(self::TRANSIENT_TOTAL);
    }

    public function delete_post_wordcount_meta($post_ID) {
        delete_post_meta($post_ID, self::META_KEY);
        delete_transient(self::TRANSIENT_TOTAL);
    }

    public function maybe_invalidate_totals($new_status, $old_status, $post) {
        if ( $post->post_type === 'post' && ($new_status !== $old_status) ) {
            delete_transient(self::TRANSIENT_TOTAL);
        }
    }

    /** TOTALS **/
    public static function get_totals() {
        $cached = get_transient(self::TRANSIENT_TOTAL);
        if ( false !== $cached ) return $cached;

        global $wpdb;
        // Use single query to sum and count
        $sql = $wpdb->prepare(
            "SELECT SUM( CAST(pm.meta_value AS UNSIGNED) ) AS total_words, COUNT(p.ID) AS post_count
             FROM {$wpdb->posts} p
             INNER JOIN {$wpdb->postmeta} pm
                 ON pm.post_id = p.ID AND pm.meta_key = %s
             WHERE p.post_type = 'post' AND p.post_status = 'publish'",
            self::META_KEY
        );
        $row = $wpdb->get_row($sql);
        $total_words = (int) ($row && $row->total_words ? $row->total_words : 0);
        $post_count  = (int) ($row && $row->post_count ? $row->post_count : 0);

        $data = [
            'total_words' => $total_words,
            'post_count'  => $post_count,
            'average'     => $post_count ? (int) floor($total_words / $post_count) : 0,
            'generated'   => time(),
        ];
        set_transient(self::TRANSIENT_TOTAL, $data, 6 * HOUR_IN_SECONDS);
        return $data;
    }

    /** DASHBOARD WIDGET **/
    public function register_widget() {
        wp_add_dashboard_widget(
            'hz_total_word_count',
            __('Total Word Count Progress', 'hz-twc'),
            [$this, 'render_widget']
        );
    }

    private function progress_bar_html($total, $goal, $label='') {
        $goal  = max(1, (int)$goal);
        $pct   = min(100, round(($total / $goal) * 100, 2));
        $width = $pct;
        $gradient  = $pct <= 50
            ? 'linear-gradient(90deg, #3498db 0%, #2ecc71 ' . ( $pct * 2 ) . '%)'
            : 'linear-gradient(90deg, #3498db 0%, #2ecc71 50%, #27ae60 ' . ( 50 + (($pct-50)*2) ) . '%)';
        $human_goal = number_format_i18n($goal);
        ob_start();
        ?>
        <div class="hz-twc-bar-outer" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="<?php echo esc_attr($pct); ?>" aria-label="<?php echo esc_attr($label ?: 'Progress'); ?>">
            <div class="hz-twc-bar-inner" style="width: <?php echo esc_attr($width); ?>%; background: <?php echo esc_attr($gradient); ?>;">
                <span class="hz-twc-bar-label"><?php echo esc_html($pct); ?>%</span>
            </div>
        </div>
        <p class="hz-twc-goal"><small><?php echo esc_html($label); ?> <?php esc_html_e('Goal:', 'hz-twc'); ?> <?php echo esc_html($human_goal); ?> <?php esc_html_e('words', 'hz-twc'); ?></small></p>
        <?php
        return ob_get_clean();
    }

    public function render_widget() {
        if ( ! current_user_can(self::CAP) ) {
            echo '<p>'.esc_html__('You do not have permission to view this widget.', 'hz-twc').'</p>';
            return;
        }

        $totals = self::get_totals();
        $goals  = $this->get_goals();

        $human_total = number_format_i18n($totals['total_words']);
        $human_avg   = number_format_i18n($totals['average']);

        $nonce = wp_create_nonce(self::NONCE);
        ?>
        <div class="hz-twc-wrap">
            <p><strong><?php echo esc_html($human_total); ?></strong> <?php esc_html_e('words published across all posts.', 'hz-twc'); ?></p>
            <p><?php esc_html_e('Average per post:', 'hz-twc'); ?> <strong><?php echo esc_html($human_avg); ?></strong> (<?php echo esc_html( number_format_i18n($totals['post_count']) ); ?> <?php esc_html_e('posts', 'hz-twc'); ?>)</p>

            <?php
            echo $this->progress_bar_html($totals['total_words'], $goals[0], __('1M Progress -', 'hz-twc'));
            echo $this->progress_bar_html($totals['total_words'], $goals[1], __('2M Progress -', 'hz-twc'));
            echo $this->progress_bar_html($totals['total_words'], $goals[2], __('5M Progress -', 'hz-twc'));
            ?>

            <div class="hz-twc-actions">
                <button class="button" id="hz-twc-refresh" data-nonce="<?php echo esc_attr($nonce); ?>"><?php esc_html_e('Refresh totals', 'hz-twc'); ?></button>
                <button class="button button-primary" id="hz-twc-recount" data-nonce="<?php echo esc_attr($nonce); ?>"><?php esc_html_e('Recount all posts now', 'hz-twc'); ?></button>
                <a class="button-link hz-twc-settings" href="<?php echo esc_url( admin_url('options-reading.php#'.self::OPTION_GOALS) ); ?>"><?php esc_html_e('Set goals', 'hz-twc'); ?></a>
            </div>

            <p class="description"><?php esc_html_e('Totals are cached for 6 hours and refreshed when posts change. Use the buttons if you recently imported content.', 'hz-twc'); ?></p>
            <div id="hz-twc-status" aria-live="polite"></div>
        </div>
        <?php
    }

    /** AJAX HANDLERS **/
    public function ajax_clear_cache() {
        if ( ! current_user_can(self::CAP) ) wp_send_json_error(['message' => __('Permission denied.', 'hz-twc')], 403);
        check_ajax_referer(self::NONCE, 'nonce');
        delete_transient(self::TRANSIENT_TOTAL);
        $data = self::get_totals();
        wp_send_json_success(['message' => __('Totals refreshed.', 'hz-twc'), 'totals' => $data]);
    }

    public function ajax_recount_all() {
        if ( ! current_user_can(self::CAP) ) wp_send_json_error(['message' => __('Permission denied.', 'hz-twc')], 403);
        check_ajax_referer(self::NONCE, 'nonce');

        $args = [
            'post_type'      => 'post',
            'post_status'    => 'publish',
            'posts_per_page' => -1,
            'fields'         => 'ids',
            'no_found_rows'  => true,
        ];
        $ids = get_posts($args);
        $counted = 0;
        foreach ($ids as $post_id) {
            $content = get_post_field('post_content', $post_id);
            $wc = self::count_words_from_content($content);
            update_post_meta($post_id, self::META_KEY, $wc);
            $counted++;
        }
        delete_transient(self::TRANSIENT_TOTAL);
        wp_send_json_success([
            'message' => sprintf( __('Recount complete. Processed %d posts.', 'hz-twc'), $counted ),
            'processed' => $counted,
        ]);
    }

    /** ASSETS **/
    public function admin_assets($hook) {
        if ( 'index.php' !== $hook ) return; // only Dashboard
        wp_register_style('hz-twc-css', plugins_url('assets/dashboard.css', __FILE__), [], '1.1');
        wp_enqueue_style('hz-twc-css');
        wp_register_script('hz-twc-js', plugins_url('assets/dashboard.js', __FILE__), ['jquery'], '1.1', true);
        wp_enqueue_script('hz-twc-js');
        wp_localize_script('hz-twc-js', 'HZ_TWC', [
            'ajax'  => admin_url('admin-ajax.php'),
        ]);
    }

    /** MIGRATION **/
    public function maybe_migrate_options() {
        // Migrate old single-goal option if it exists
        $old_goal = get_option('hz_twc_goal', null);
        if ( $old_goal !== null ) {
            $existing = get_option(self::OPTION_GOALS, null);
            if ( $existing === null || !is_array($existing) ) {
                $g = absint($old_goal);
                update_option(self::OPTION_GOALS, [$g ?: 1000000, 2000000, 5000000]);
            }
            delete_option('hz_twc_goal');
        }
    }
}

new HZ_Total_Word_Count_Dashboard();

// Activation: ensure default goals exist
register_activation_hook(__FILE__, function() {
    if ( get_option(HZ_Total_Word_Count_Dashboard::OPTION_GOALS, false) === false ) {
        add_option(HZ_Total_Word_Count_Dashboard::OPTION_GOALS, [1000000,2000000,5000000]);
    }
});

Paste the code you provided into that file.

Step 3: Add the Assets Folder

The plugin expects:

  • assets/dashboard.css
  • assets/dashboard.js

So create:

  • wp-content/plugins/hz-total-word-count-dashboard/assets/

Then create:

  • dashboard.css

.hz-twc-wrap { text-align:left; }
.hz-twc-bar-outer { background:#eee; border-radius:6px; width:100%; height:28px; overflow:hidden; position:relative; margin-top:8px; }
.hz-twc-bar-inner { height:100%; display:flex; align-items:center; justify-content:center; color:#fff; font-weight:700; font-size:12px; text-shadow:0 1px 1px rgba(0,0,0,.25); }
.hz-twc-bar-label { padding:0 6px; }
.hz-twc-actions { margin-top:12px; display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
.hz-twc-goal { text-align:right; margin-top:4px; margin-bottom:2px; }
#hz-twc-status { margin-top:8px; }
  • dashboard.js

jQuery(function($){
  function notice(msg, type){
    const $s = $('#hz-twc-status');
    $s.removeClass().addClass(type ? 'notice notice-' + type : '').text(msg);
  }

  $('#hz-twc-refresh').on('click', function(e){
    e.preventDefault();
    const nonce = $(this).data('nonce');
    notice('Refreshing totals…','info');
    $.post(HZ_TWC.ajax, { action: 'hz_twc_clear_cache', nonce }, function(resp){
      if(resp && resp.success){
        notice(resp.data.message, 'success');
        location.reload();
      } else {
        notice(resp && resp.data && resp.data.message ? resp.data.message : 'Error', 'error');
      }
    });
  });

  $('#hz-twc-recount').on('click', function(e){
    e.preventDefault();
    if(!confirm('Recount all published posts now? This may take a while on large sites.')) return;
    const nonce = $(this).data('nonce');
    notice('Recount in progress…','info');
    $.post(HZ_TWC.ajax, { action: 'hz_twc_recount_all', nonce }, function(resp){
      if(resp && resp.success){
        notice(resp.data.message, 'success');
        location.reload();
      } else {
        notice(resp && resp.data && resp.data.message ? resp.data.message : 'Error', 'error');
      }
    });
  });
});

If you do not add these files, the plugin will still work, but:

  • It may look unstyled
  • Buttons may not update the UI smoothly

Step 4: Activate the Plugin

In WP Admin:

  • Plugins → Installed Plugins
  • Find HZ Total Word Count Dashboard
  • Click Activate

Step 5: Check the Dashboard Widget

Go to:

  • Dashboard → Home

You should see:

  • “Total Word Count Progress”

If you do not see it, scroll down and check Screen Options.


Method 2: Install via ZIP Upload

If you want to upload it as a ZIP:

Step 1: Create the ZIP Structure

Your ZIP should look like this:

  • hz-total-word-count-dashboard/
    • hz-total-word-count-dashboard.php
    • assets/
      • dashboard.css
      • dashboard.js

Step 2: Upload in WP Admin

Go to:

  • Plugins → Add New → Upload Plugin

Upload the ZIP and activate it.

Method 3: Install on a Client Site Safely

If you install for clients, you may want to:

  • Rename the folder to match branding
  • Keep the plugin header accurate
  • Add a changelog in a README file

This makes maintenance easier.

First Run: What Happens After Activation

On activation, the plugin runs:

  • register_activation_hook

It ensures default goals exist.
If the goals option is missing, it adds:

  • [1000000, 2000000, 5000000]

After that:

  • It starts storing word count meta when posts are saved
  • The Dashboard widget uses cached totals for speed

Best Practices After Installing

If your site already has many posts, you should do this once:

Run “Recount All Posts Now”

Because older posts may not have meta yet.

So after activation:

  • Go to Dashboard widget
  • Click Recount all posts now

That populates _hz_word_count for existing posts.

After that, everything stays automatic.

How the AJAX Security Works

The plugin protects AJAX requests with two layers:

Capability Checks

It checks:

  • current_user_can('edit_posts')

So only users allowed to edit posts can run actions.

Nonce Verification

It uses:

  • check_ajax_referer(self::NONCE, 'nonce')

The nonce is printed into the widget and attached to button clicks.

This blocks:

  • CSRF attempts
  • Unauthorized refresh or recount triggers

What the CSS and JS Should Do

Your plugin references:

  • assets/dashboard.css
  • assets/dashboard.js

Even if you keep them simple, you want two outcomes:

CSS Goals

  • Style the progress bars
  • Space the widget content nicely
  • Make the percent label readable

JS Goals

  • Listen to button clicks
  • Send AJAX requests to admin-ajax.php
  • Show status messages in #hz-twc-status
  • Optionally reload page or update HTML after success

If you already have working assets, keep them.
If you do not, you can build minimal ones later.

Troubleshooting and Common Issues

This plugin is stable, but a few things can confuse people.

The Widget Shows 0 Words

This usually means:

  • Your posts do not have _hz_word_count meta yet

Fix:

  • Click Recount all posts now

The Total Looks Wrong After Import

Imports often skip triggering save_post properly.

Fix:

  • Click Recount all posts now
  • Then click Refresh totals

The Widget Does Not Appear

Possible reasons:

  • Your user role cannot edit_posts
  • The widget is hidden in Screen Options
  • Dashboard page is customized by another plugin

Fix:

  • Check Screen Options
  • Test with an admin user
  • Temporarily disable dashboard customization plugins

Goals Settings Not Showing

If you do not see the goals field in Reading settings:

  • Another plugin may be heavily customizing Settings → Reading
  • Your admin user might lack capability to manage settings (rare)

Fix:

  • Switch temporarily to an admin account
  • Check for conflicts

Performance Notes for Large Sites

This plugin is already optimized, but consider:

Big Post Counts and Recount Time

The recount tool loops through all published posts.

On very large sites (10k+ posts), that can take time.

Tips:

  • Run recount during low traffic
  • Consider adding batching later (advanced improvement)
  • Keep PHP memory limits reasonable

Database Health

Totals use a join between posts and postmeta.

That is fine on most sites.

If you have extreme scale, indexing and DB tuning help, but most WordPress sites will never need it.

Customization Ideas

If you want to extend the plugin for your workflow, here are safe ideas:

Add More Goal Bars

Right now, the UI supports three.

You could upgrade it to:

  • A dynamic number of goals
  • A repeatable settings field

Include Custom Post Types

The plugin currently counts only post type.

You could count:

  • Pages
  • Custom post types like “news” or “reviews”

That would require adjusting:

  • save_post check
  • SQL query filter
  • Recount query args

Count Only Certain Categories

If you run multiple content silos, you might want:

  • Word counts for a category only

That requires a taxonomy filter and a different SQL approach.

Add Monthly Tracking

Another nice upgrade:

  • Words published this month
  • Words published last month
  • Rolling averages

That changes the query logic, but it can stay efficient with caching.

Pro & Contra

This plugin is a strong choice when you care about progress and speed.

Pro

  • Very fast totals (single SQL query + cache)
  • Keeps data accurate automatically on post changes
  • Three goals supported by default
  • Safe admin-only AJAX actions (capability + nonce)
  • Clean settings under Reading
  • Minimal footprint outside Dashboard

Contra

  • Counts only post type, not pages or CPTs
  • Recount loops through all posts and can be heavy on huge sites
  • Requires assets files for best UI polish
  • Word counting ignores shortcode output by design
Display a Total Word Count Box in the WordPress Admin Dashboard

After donating, you will be redirected back and receive a download link (valid for 10 minutes).


🧩Frequently Asked Questions

What does this plugin count exactly?
It counts words from the post_content of published posts, after removing HTML and shortcodes, then splitting remaining text into words.

Does it count pages too?
No. It counts only the post post type. You can modify the code to include pages or custom post types.

Why does it store word counts in post meta?
Because it is faster. The plugin updates a post’s word count when it changes, then totals become a quick sum.

How often are totals refreshed?
Totals are cached for 6 hours. The cache clears automatically when posts change, and you can also refresh manually.

When should I use “Recount all posts now”?
Use it after imports, migrations, bulk edits, or right after installing the plugin so older posts get their meta saved.

Who can see or use the widget?
Users with the edit_posts capability. Usually admins and editors.

Can this slow down my admin dashboard?
The widget itself is light. The only heavy operation is “Recount all posts now,” because it processes every published post.

Why do I see 0 words after activation?
Because older posts do not have _hz_word_count meta yet. Run the recount tool once.

Can I change the goals from 1M/2M/5M?
Yes. Go to Settings → Reading and set Goal 1, Goal 2, and Goal 3.

Is it safe to use on a production site?
Yes, it follows good practices: capability checks, nonce verification, sanitized settings, caching, and minimal admin-only assets.

Final Notes to Keep Your Momentum High
A publishing goal feels real when you can measure it.
This plugin makes your effort visible every time you open the Dashboard. That small feedback loop keeps you consistent, especially when you chase big SEO milestones like 1M or 5M total words.


⚠️ DISCLAIMER


This article is for educational purposes and reflects practical research and WordPress development best practices. Always test plugins on a staging site before using them in production. If you manage a high-traffic site, consult a qualified developer or system administrator for performance and security validation.

🔔 For more tutorials like this, consider subscribing to our blog.
📩 Do you have questions or suggestions? Leave a comment or contact us!
🏷️ Tags: WordPress plugin, Dashboard widget, Word count, WordPress admin, WordPress development, WP post meta, WordPress transients, Yoast SEO, Content strategy, Blogging goals
📢 Hashtags: #WordPress #WordPressPlugin #WordPressTips #Blogging #ContentMarketing #SEO #YoastSEO #WebDevelopment #SitePerformance #BloggingTips


📚 Sources

WordPress Developer Documentation Topics Used
Dashboard Widgets
Settings API
Post Meta API
WordPress AJAX in Admin
Transients API
Capability Checks and Nonces
Practical WordPress Admin Optimization Patterns
Common plugin hardening practices (ABSPATH checks, nonce verification)
Real-world workflow needs for content publishers (imports, recount tools, progress tracking)


Simple FAST Code

📘 Display a Simple Code Total Word Count Box in the WordPress Admin Dashboard

🧩 Why Add a Word Count Widget?

Knowing the total for your published content gives you insights into:

  • Your overall content volume
  • How much content is being produced over time
  • Content audit benchmarks
  • SEO optimization efforts

This is especially useful for multi-author blogs, agencies, and editorial teams.


🛠️ Step-by-Step: Add the Word Count Box with PHP

Follow these steps to insert a custom widget that counts all words from your published posts:

1. Open Your Theme’s functions.php File

Navigate to:
/wp-content/themes/your-theme/functions.php

Or use a code snippets plugin to insert the following code safely.

2. Insert This PHP Code

add_action('wp_dashboard_setup', 'hz_add_word_count_dashboard_widget');

function hz_add_word_count_dashboard_widget() {
    wp_add_dashboard_widget('hz_total_word_count', 'Total Word Count (All Posts)', 'hz_display_word_count_widget');
}

function hz_display_word_count_widget() {
    $total_words = 0;

    $all_posts = get_posts(array(
        'numberposts' => -1,
        'post_type'   => 'post',
        'post_status' => 'publish',
        'fields'      => 'ids'
    ));

    foreach ($all_posts as $post_id) {
        $content = get_post_field('post_content', $post_id);
        $word_count = str_word_count(strip_tags($content));
        $total_words += $word_count;
    }

    echo '<p><strong>Total Words:</strong> ' . number_format($total_words) . '</p>';
}

🖥️ Where Will the Widget Appear?

Once saved, go to Dashboard → Home in your WordPress admin area. You will see a box titled “Total Word Count (All Posts)”, displaying the number of words across all published posts.


🧪 Optional: Count Words from Pages or Custom Post Types

If you want to extend this to pages or custom post types, modify the 'post_type' => 'post' to an array like this:

'post_type' => array('post', 'page', 'your_custom_type'),

🚫 Don’t Forget Security and Performance

If you have thousands of posts, this could slow down your dashboard slightly. Consider caching the result with a transient if performance is affected.

Report an issue (max 5 words):

We store the message, post link, time, and IP (for abuse prevention). No account required.

Want to support us? Let friends in on the secret and share your favorite post!

1 online now

Live Referrers

No external referrers recorded for this post.

Photo of author

Flo

How To Display a Word Count Box in the WP Admin Dashboard

Published

Update

Welcome to HelpZone.blog, your go-to hub for expert insights, practical tips, and in-depth guides across technology, lifestyle, business, entertainment, and more! Our team of passionate writers and industry experts is dedicated to bringing you the latest trends, how-to tutorials, and valuable advice to enhance your daily life. Whether you're exploring WordPress tricks, gaming insights, travel hacks, or investment strategies, HelpZone is here to empower you with knowledge. Stay informed, stay inspired because learning never stops! 🚀

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.