A WordPress post reading time shortcode shows readers how long an article takes, such as “4 min read”, next to the title or at the top of the post. [reading_time] estimates it from the post’s word count and works inside the post content, in a Shortcode block in a block theme template and in PHP templates.
The snippet
<?php
/**
* [reading_time] shortcode: estimated reading time of a post.
* Usage: [reading_time], [reading_time wpm="250"], [reading_time post_id="42" label="false"]
*/
add_action( 'init', 'dpc_reading_time_register' );
function dpc_reading_time_register() {
add_shortcode( 'reading_time', 'dpc_reading_time_shortcode' );
}
/**
* Reading time in whole minutes, from the raw post content.
* Returns 0 for a post without words.
*/
function dpc_get_reading_time( $post = null, $words_per_minute = 200 ) {
$post = get_post( $post );
if ( ! $post ) {
return 0;
}
// Raw content: no the_content filter, so no recursion when the shortcode sits in the post itself.
$text = strip_shortcodes( $post->post_content );
$text = wp_strip_all_tags( $text ); // Also removes the <!-- wp:... --> block comments.
$text = html_entity_decode( $text, ENT_QUOTES, get_bloginfo( 'charset' ) );
$words = preg_split( '/\s+/u', trim( $text ), -1, PREG_SPLIT_NO_EMPTY );
$count = is_array( $words ) ? count( $words ) : 0;
return $count > 0 ? (int) ceil( $count / max( 1, (int) $words_per_minute ) ) : 0;
}
function dpc_reading_time_shortcode( $atts ) {
$atts = shortcode_atts(
array(
'wpm' => 200,
'post_id' => 0,
'label' => 'true',
),
$atts,
'reading_time'
);
$wpm = absint( $atts['wpm'] );
if ( $wpm < 50 || $wpm > 1000 ) {
$wpm = 200;
}
$post = absint( $atts['post_id'] ) ? get_post( absint( $atts['post_id'] ) ) : get_post();
if ( ! $post || ( ! is_post_publicly_viewable( $post ) && ! current_user_can( 'read_post', $post->ID ) ) ) {
return '';
}
$minutes = dpc_get_reading_time( $post, $wpm );
if ( 0 === $minutes ) {
return '';
}
$text = wp_validate_boolean( $atts['label'] )
/* translators: %d: number of minutes. */
? sprintf( _n( '%d min read', '%d min read', $minutes, 'dpc' ), $minutes )
: (string) $minutes;
return '<span class="dpc-reading-time">' . esc_html( $text ) . '</span>';
}
Use it
[reading_time]prints<span class="dpc-reading-time">4 min read</span>for the current post.wpmsets words per minute (50 to 1000, default 200). Anything else falls back to 200.label="false"prints only the number, for your own wording around it.post_id="42"measures another post, for example in a sidebar or a related-posts list.- In a PHP template:
echo do_shortcode( '[reading_time]' );, ordpc_get_reading_time()for the bare number.
How it works
- The words come from the raw
post_content. The shortcode never runs thethe_contentfilter, which would render the shortcode again inside itself and loop until PHP runs out of memory, which is the bug in the older version of this post. strip_shortcodes()removes shortcodes,wp_strip_all_tags()removes HTML and the<!-- wp:... -->block comments, so markup and block settings are not counted as words.- Words are split on whitespace with a Unicode-aware pattern, so accented words count correctly, unlike
str_word_count(). - The result is rounded up, so a short post shows 1 minute, and a post with no words prints nothing.
- Attributes go through
shortcode_atts(),absint()andwp_validate_boolean(), the output throughesc_html(). A private or draft post passed inpost_idonly shows for users who can read it.
The older version also had a gt attribute that always ran because the string “false” is truthy; it is no longer needed and is ignored if old shortcodes still pass it.
Add it with Scripts Organizer
In Scripts Organizer go to Code Blocks → Add New and give the block a name. Set Trigger location to Everywhere, tick PHP under Script location, paste the code into the PHP editor and publish. Leave Action/Hook empty. Everywhere registers the shortcode on every request, including the REST requests the block editor uses to preview a Shortcode block; Conditions would only register it on matching front-end pages.
The same code also works unchanged in a small plugin or an mu-plugin.
Customise it
- Different wording: change
'%d min read'(both strings in_n()), for example to'%d minute read'and'%d minutes read'. - Automatically above every post, without a shortcode: hook a filter on
the_contentthat prependsdo_shortcode( '[reading_time]' )whenis_singular( 'post' ) && in_the_loop().
Already using Read time without the plugin? This version adds a template helper and attribute validation. For another small shortcode, see Fluid Type Preview shortcode.
Gotchas
- Text that blocks render dynamically (query loops, reusable patterns, synced patterns) is not in
post_contentand is not counted. - Chinese, Japanese and Thai don’t separate words with spaces, so the count is far too low; count characters with
mb_strlen()and divide by about 500 instead. - The tag
reading_timehas no prefix so existing shortcodes keep working. If another plugin registers the same tag, the one added last wins; rename it inadd_shortcode()and inshortcode_atts()if needed.
In the Scripts Organizer library Import it with one click from Scripts Organizer.