⏲️ 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-twcfor 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 (
ABSPATHnot defined), the plugin stops. - This is standard security hygiene for plugins.
The Plugin Architecture
The entire plugin lives in a class:
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
postpost 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_countmeta - 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 postshz_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
Set Goals Link
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.cssassets/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.
World Record for Maintaining a Daily Posting Streak on WordPress
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.phpassets/dashboard.cssdashboard.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.cssassets/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_countmeta 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_postcheck- 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
posttype, 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
🧩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.
