Medium shows “4 min read” at the top of every article, and readers like to know how long a post will take before they start. You can show the estimated reading time in WordPress without a plugin: this snippet adds a small template function that counts the words of a post at about 200 words per minute, the figure Medium uses, and adds a little time for each image, as Medium does.
The snippet
<?php
/**
* Estimated reading time for theme templates: 200 words per minute plus time for images, like Medium.
*
* Template usage: dpc_the_read_time();
*/
// ===== Config =====
function dpc_read_time_config(): array {
return array(
'words_per_minute' => 200,
// Medium adds 12 s for the first image, 11 s for the second, down to 3 s. false = count words only.
'count_images' => true,
);
}
// ===== End of config =====
/**
* Reading time of a post in minutes (at least 1 for a post with any content).
*/
function dpc_read_time_minutes( $post = null ): int {
$post = get_post( $post );
if ( ! $post ) {
return 0;
}
$config = dpc_read_time_config();
$content = strip_shortcodes( (string) $post->post_content );
$text = html_entity_decode( wp_strip_all_tags( $content ), ENT_QUOTES, get_bloginfo( 'charset' ) );
$words = preg_split( '/\s+/u', trim( $text ), -1, PREG_SPLIT_NO_EMPTY );
$words = is_array( $words ) ? count( $words ) : 0;
$seconds = $words / max( 1, (int) $config['words_per_minute'] ) * 60;
if ( ! empty( $config['count_images'] ) ) {
$images = preg_match_all( '/<img\b/i', $content );
for ( $i = 0; $i < $images; $i++ ) {
$seconds += max( 3, 12 - $i );
}
}
return $seconds > 0 ? (int) ceil( $seconds / 60 ) : 0;
}
/**
* Reading time as text, e.g. "3 minutes". Empty string for a post without content.
*/
function dpc_read_time( $post = null ): string {
$minutes = dpc_read_time_minutes( $post );
if ( 0 === $minutes ) {
return '';
}
/* translators: %d: number of minutes. */
return sprintf( _n( '%d minute', '%d minutes', $minutes, 'dpc' ), $minutes );
}
/**
* Print the reading time, escaped, with an optional label.
*/
function dpc_the_read_time( $post = null, string $label = '' ): void {
$text = dpc_read_time( $post );
if ( '' === $text ) {
return;
}
if ( '' === $label ) {
$label = __( 'Read time:', 'dpc' );
}
printf( '<p class="dpc-read-time">%s %s</p>', esc_html( $label ), esc_html( $text ) );
}
Then call it in your theme template where the reading time should appear, for example in single.php below the title:
<?php dpc_the_read_time(); ?>
<?php // Or build your own markup: ?>
<span class="read-time"><?php echo esc_html( dpc_read_time() ); ?></span>
How it works
dpc_read_time_minutes()reads the rawpost_contentof the post, removes shortcodes, HTML tags and the<!-- wp:... -->block comments, and counts words on whitespace. The pattern is Unicode-aware, so words with accents count once, unlikestr_word_count().- With
count_imageson, each<img>adds time the way Medium does it: 12 seconds for the first, one second less for each next image, never less than 3 seconds. - The result is rounded up to whole minutes, so a short post shows 1 minute. A post without content returns 0 and prints nothing.
dpc_read_time()returns translatable text such as “3 minutes”.dpc_the_read_time()prints it with a label, both escaped withesc_html().- Without an argument the functions use the current post, so they work inside The Loop. Pass a post ID or object anywhere else.
The older version of this post read $post->ID without $post existing in the function, which is a warning on PHP 8 and the reason it only ever worked by accident. Its generic name reading_time() could also collide with a theme or plugin.
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 snippet (not the template part) into the PHP editor, edit the config and publish. Leave Action/Hook empty.
Everywhere loads the functions the same way functions.php does, before any template runs. The template part goes into your theme or child theme file. Wrap the call in if ( function_exists( 'dpc_the_read_time' ) ) if the code block might ever be disabled.
The same code also works unchanged in a small plugin or an mu-plugin.
Customise it
- Slower or faster readers: change
words_per_minute. Technical posts read closer to 150. - Words only: set
'count_images' => false. - Block themes have no PHP templates. Use the Post reading time shortcode in a Shortcode block instead.
Gotchas
- Content that blocks render on their own, such as Query Loops, synced patterns and gallery shortcodes, is not in
post_contentand is not counted. - Chinese, Japanese and Thai do not separate words with spaces, so the count is far too low. Count characters with
mb_strlen()and divide by about 500 instead.
In the Scripts Organizer library Import it with one click from Scripts Organizer.