I’ll build this as a complete reading-time.php helper file for your RX theme: secure, configurable, shortcode-ready, block/widget-friendly, SEO/meta-ready, and easy to call from templates.
My dear friend, create this file:
rx-theme/inc/reading-time.php
Then include it from functions.php:
require_once get_template_directory() . '/inc/reading-time.php';
Here is an advanced complete reading-time.php file for your RX Theme.
<?php
/**
* RX Theme Reading Time System
*
* File: inc/reading-time.php
* Theme: RX Theme
*
* Features:
* - Automatic reading time calculation
* - Word count
* - Character count
* - Paragraph count
* - Heading count
* - Image adjustment time
* - Video/audio adjustment time
* - Shortcode support
* - Template function support
* - Post meta caching
* - REST API support
* - Schema/meta helper
* - Admin column support
* - Content filter auto display
* - Gutenberg/block friendly
* - Translation ready
*
* Usage:
* echo rx_get_reading_time();
* echo rx_reading_time_badge();
* [rx_reading_time]
* [rx_word_count]
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* ---------------------------------------------------------
* Constants
* ---------------------------------------------------------
*/
if ( ! defined( 'RX_READING_TIME_VERSION' ) ) {
define( 'RX_READING_TIME_VERSION', '1.0.0' );
}
if ( ! defined( 'RX_READING_TIME_META_KEY' ) ) {
define( 'RX_READING_TIME_META_KEY', '_rx_reading_time_data' );
}
/**
* ---------------------------------------------------------
* Default Settings
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_reading_time_defaults' ) ) {
function rx_reading_time_defaults() {
$defaults = array(
'words_per_minute' => 200,
'image_seconds' => 12,
'video_seconds' => 30,
'audio_seconds' => 20,
'minimum_minutes' => 1,
'rounding_method' => 'ceil', // ceil, round, floor
'auto_display' => false,
'auto_display_position' => 'before', // before, after
'allowed_post_types' => array( 'post' ),
'show_icon' => true,
'show_word_count' => false,
'show_label' => true,
'label_single' => 'min read',
'label_plural' => 'min read',
'word_label' => 'words',
'enable_cache' => true,
'enable_admin_column' => true,
'enable_rest_field' => true,
'enable_shortcode' => true,
'enable_schema_meta' => true,
'exclude_shortcodes' => true,
'exclude_html' => true,
'exclude_code_blocks' => true,
'exclude_tables' => false,
'css_class' => 'rx-reading-time',
'badge_class' => 'rx-reading-time-badge',
'icon_html' => '<span class="rx-reading-time__icon" aria-hidden="true">⏱</span>',
);
return apply_filters( 'rx_reading_time_defaults', $defaults );
}
}
/**
* ---------------------------------------------------------
* Get Setting
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_reading_time_get_setting' ) ) {
function rx_reading_time_get_setting( $key = '', $fallback = null ) {
$defaults = rx_reading_time_defaults();
$options = get_theme_mod( 'rx_reading_time_settings', array() );
if ( ! is_array( $options ) ) {
$options = array();
}
$settings = wp_parse_args( $options, $defaults );
$settings = apply_filters( 'rx_reading_time_settings', $settings );
if ( empty( $key ) ) {
return $settings;
}
return isset( $settings[ $key ] ) ? $settings[ $key ] : $fallback;
}
}
/**
* ---------------------------------------------------------
* Check Supported Post Type
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_reading_time_is_supported_post_type' ) ) {
function rx_reading_time_is_supported_post_type( $post_id = 0 ) {
$post_id = $post_id ? absint( $post_id ) : get_the_ID();
if ( ! $post_id ) {
return false;
}
$post_type = get_post_type( $post_id );
$allowed = rx_reading_time_get_setting( 'allowed_post_types', array( 'post' ) );
if ( ! is_array( $allowed ) ) {
$allowed = array( 'post' );
}
return in_array( $post_type, $allowed, true );
}
}
/**
* ---------------------------------------------------------
* Clean Content Before Counting
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_reading_time_clean_content' ) ) {
function rx_reading_time_clean_content( $content = '' ) {
$content = (string) $content;
if ( '' === trim( $content ) ) {
return '';
}
$exclude_shortcodes = rx_reading_time_get_setting( 'exclude_shortcodes', true );
$exclude_html = rx_reading_time_get_setting( 'exclude_html', true );
$exclude_code_blocks = rx_reading_time_get_setting( 'exclude_code_blocks', true );
$exclude_tables = rx_reading_time_get_setting( 'exclude_tables', false );
if ( $exclude_code_blocks ) {
$content = preg_replace( '#<pre[^>]*>.*?</pre>#is', ' ', $content );
$content = preg_replace( '#<code[^>]*>.*?</code>#is', ' ', $content );
$content = preg_replace( '#```.*?```#is', ' ', $content );
}
if ( $exclude_tables ) {
$content = preg_replace( '#<table[^>]*>.*?</table>#is', ' ', $content );
}
if ( $exclude_shortcodes ) {
$content = strip_shortcodes( $content );
} else {
$content = do_shortcode( $content );
}
$content = wp_strip_all_tags( $content );
if ( $exclude_html ) {
$content = html_entity_decode( $content, ENT_QUOTES, get_bloginfo( 'charset' ) );
}
$content = preg_replace( '/\s+/u', ' ', $content );
return trim( $content );
}
}
/**
* ---------------------------------------------------------
* Count Words
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_reading_time_count_words' ) ) {
function rx_reading_time_count_words( $content = '' ) {
$content = rx_reading_time_clean_content( $content );
if ( '' === $content ) {
return 0;
}
if ( function_exists( 'wp_word_count_type' ) ) {
$type = wp_word_count_type();
} else {
$type = 'words';
}
if ( 'characters_excluding_spaces' === $type || 'characters_including_spaces' === $type ) {
$count = mb_strlen( $content );
} else {
$count = str_word_count( wp_strip_all_tags( $content ) );
}
return absint( apply_filters( 'rx_reading_time_word_count', $count, $content ) );
}
}
/**
* ---------------------------------------------------------
* Character Count
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_reading_time_count_characters' ) ) {
function rx_reading_time_count_characters( $content = '', $include_spaces = true ) {
$content = rx_reading_time_clean_content( $content );
if ( ! $include_spaces ) {
$content = preg_replace( '/\s+/u', '', $content );
}
return function_exists( 'mb_strlen' ) ? mb_strlen( $content ) : strlen( $content );
}
}
/**
* ---------------------------------------------------------
* Paragraph Count
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_reading_time_count_paragraphs' ) ) {
function rx_reading_time_count_paragraphs( $content = '' ) {
preg_match_all( '#<p[^>]*>.*?</p>#is', $content, $matches );
if ( ! empty( $matches[0] ) ) {
return count( $matches[0] );
}
$content = trim( wp_strip_all_tags( $content ) );
if ( '' === $content ) {
return 0;
}
$parts = preg_split( "/\n\s*\n/u", $content );
return count( array_filter( $parts ) );
}
}
/**
* ---------------------------------------------------------
* Heading Count
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_reading_time_count_headings' ) ) {
function rx_reading_time_count_headings( $content = '' ) {
preg_match_all( '#<h[1-6][^>]*>.*?</h[1-6]>#is', $content, $matches );
return ! empty( $matches[0] ) ? count( $matches[0] ) : 0;
}
}
/**
* ---------------------------------------------------------
* Image Count
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_reading_time_count_images' ) ) {
function rx_reading_time_count_images( $content = '' ) {
preg_match_all( '#<img[^>]*>#is', $content, $matches );
return ! empty( $matches[0] ) ? count( $matches[0] ) : 0;
}
}
/**
* ---------------------------------------------------------
* Video Count
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_reading_time_count_videos' ) ) {
function rx_reading_time_count_videos( $content = '' ) {
$count = 0;
preg_match_all( '#<video[^>]*>.*?</video>#is', $content, $video_matches );
preg_match_all( '#<iframe[^>]*(youtube|vimeo|dailymotion|facebook|tiktok)[^>]*>.*?</iframe>#is', $content, $iframe_matches );
$count += ! empty( $video_matches[0] ) ? count( $video_matches[0] ) : 0;
$count += ! empty( $iframe_matches[0] ) ? count( $iframe_matches[0] ) : 0;
return absint( apply_filters( 'rx_reading_time_video_count', $count, $content ) );
}
}
/**
* ---------------------------------------------------------
* Audio Count
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_reading_time_count_audio' ) ) {
function rx_reading_time_count_audio( $content = '' ) {
$count = 0;
preg_match_all( '#<audio[^>]*>.*?</audio>#is', $content, $audio_matches );
preg_match_all( '#\
]*\]#is', $content, $shortcode_matches );
$count += ! empty( $audio_matches[0] ) ? count( $audio_matches[0] ) : 0;
$count += ! empty( $shortcode_matches[0] ) ? count( $shortcode_matches[0] ) : 0;
return absint( apply_filters( 'rx_reading_time_audio_count', $count, $content ) );
}
}
/**
* ---------------------------------------------------------
* Calculate Reading Time Data
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_calculate_reading_time_data' ) ) {
function rx_calculate_reading_time_data( $content = '', $post_id = 0 ) {
$words_per_minute = absint( rx_reading_time_get_setting( 'words_per_minute', 200 ) );
if ( $words_per_minute < 50 ) {
$words_per_minute = 200;
}
$image_seconds = absint( rx_reading_time_get_setting( 'image_seconds', 12 ) );
$video_seconds = absint( rx_reading_time_get_setting( 'video_seconds', 30 ) );
$audio_seconds = absint( rx_reading_time_get_setting( 'audio_seconds', 20 ) );
$word_count = rx_reading_time_count_words( $content );
$character_count = rx_reading_time_count_characters( $content, true );
$paragraph_count = rx_reading_time_count_paragraphs( $content );
$heading_count = rx_reading_time_count_headings( $content );
$image_count = rx_reading_time_count_images( $content );
$video_count = rx_reading_time_count_videos( $content );
$audio_count = rx_reading_time_count_audio( $content );
$text_seconds = $word_count > 0 ? ( $word_count / $words_per_minute ) * 60 : 0;
$media_seconds = ( $image_count * $image_seconds ) + ( $video_count * $video_seconds ) + ( $audio_count * $audio_seconds );
$total_seconds = $text_seconds + $media_seconds;
$raw_minutes = $total_seconds / 60;
$rounding = rx_reading_time_get_setting( 'rounding_method', 'ceil' );
switch ( $rounding ) {
case 'floor':
$minutes = floor( $raw_minutes );
break;
case 'round':
$minutes = round( $raw_minutes );
break;
case 'ceil':
default:
$minutes = ceil( $raw_minutes );
break;
}
$minimum_minutes = absint( rx_reading_time_get_setting( 'minimum_minutes', 1 ) );
if ( $word_count > 0 && $minutes < $minimum_minutes ) {
$minutes = $minimum_minutes;
}
$data = array(
'post_id' => absint( $post_id ),
'minutes' => absint( $minutes ),
'raw_minutes' => round( $raw_minutes, 2 ),
'total_seconds' => absint( $total_seconds ),
'word_count' => absint( $word_count ),
'character_count' => absint( $character_count ),
'paragraph_count' => absint( $paragraph_count ),
'heading_count' => absint( $heading_count ),
'image_count' => absint( $image_count ),
'video_count' => absint( $video_count ),
'audio_count' => absint( $audio_count ),
'words_per_minute' => absint( $words_per_minute ),
'calculated_at' => current_time( 'mysql' ),
'version' => RX_READING_TIME_VERSION,
);
return apply_filters( 'rx_calculated_reading_time_data', $data, $content, $post_id );
}
}
/**
* ---------------------------------------------------------
* Get Reading Time Data
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_get_reading_time_data' ) ) {
function rx_get_reading_time_data( $post_id = 0, $force_refresh = false ) {
$post_id = $post_id ? absint( $post_id ) : get_the_ID();
if ( ! $post_id ) {
return rx_calculate_reading_time_data( '', 0 );
}
$post = get_post( $post_id );
if ( ! $post ) {
return rx_calculate_reading_time_data( '', 0 );
}
$enable_cache = rx_reading_time_get_setting( 'enable_cache', true );
if ( $enable_cache && ! $force_refresh ) {
$cached = get_post_meta( $post_id, RX_READING_TIME_META_KEY, true );
if ( is_array( $cached ) && isset( $cached['version'] ) && RX_READING_TIME_VERSION === $cached['version'] ) {
return $cached;
}
}
$data = rx_calculate_reading_time_data( $post->post_content, $post_id );
if ( $enable_cache ) {
update_post_meta( $post_id, RX_READING_TIME_META_KEY, $data );
}
return $data;
}
}
/**
* ---------------------------------------------------------
* Clear Cache When Post Saves
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_reading_time_clear_cache_on_save' ) ) {
function rx_reading_time_clear_cache_on_save( $post_id ) {
if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
return;
}
delete_post_meta( $post_id, RX_READING_TIME_META_KEY );
}
add_action( 'save_post', 'rx_reading_time_clear_cache_on_save' );
}
/**
* ---------------------------------------------------------
* Get Reading Time Text
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_get_reading_time' ) ) {
function rx_get_reading_time( $post_id = 0, $args = array() ) {
$data = rx_get_reading_time_data( $post_id );
$defaults = array(
'show_icon' => rx_reading_time_get_setting( 'show_icon', true ),
'show_label' => rx_reading_time_get_setting( 'show_label', true ),
'show_word_count' => rx_reading_time_get_setting( 'show_word_count', false ),
'before' => '',
'after' => '',
);
$args = wp_parse_args( $args, $defaults );
$minutes = isset( $data['minutes'] ) ? absint( $data['minutes'] ) : 0;
if ( $minutes <= 0 ) {
return '';
}
$label = 1 === $minutes
? rx_reading_time_get_setting( 'label_single', 'min read' )
: rx_reading_time_get_setting( 'label_plural', 'min read' );
$text = '';
if ( $args['show_icon'] ) {
$text .= rx_reading_time_get_setting( 'icon_html', '<span aria-hidden="true">⏱</span>' ) . ' ';
}
$text .= sprintf(
'%s',
number_format_i18n( $minutes )
);
if ( $args['show_label'] ) {
$text .= ' ' . esc_html( $label );
}
if ( $args['show_word_count'] && ! empty( $data['word_count'] ) ) {
$text .= ' <span class="rx-reading-time__words">(';
$text .= esc_html( number_format_i18n( $data['word_count'] ) . ' ' . rx_reading_time_get_setting( 'word_label', 'words' ) );
$text .= ')</span>';
}
$text = $args['before'] . $text . $args['after'];
return apply_filters( 'rx_get_reading_time_text', $text, $data, $post_id, $args );
}
}
/**
* ---------------------------------------------------------
* Echo Reading Time
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_the_reading_time' ) ) {
function rx_the_reading_time( $post_id = 0, $args = array() ) {
echo wp_kses_post( rx_get_reading_time( $post_id, $args ) );
}
}
/**
* ---------------------------------------------------------
* Reading Time Badge HTML
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_reading_time_badge' ) ) {
function rx_reading_time_badge( $post_id = 0, $args = array() ) {
$post_id = $post_id ? absint( $post_id ) : get_the_ID();
if ( ! $post_id ) {
return '';
}
$data = rx_get_reading_time_data( $post_id );
if ( empty( $data['minutes'] ) ) {
return '';
}
$defaults = array(
'class' => rx_reading_time_get_setting( 'badge_class', 'rx-reading-time-badge' ),
'show_icon' => rx_reading_time_get_setting( 'show_icon', true ),
'show_word_count' => rx_reading_time_get_setting( 'show_word_count', false ),
'aria_label' => true,
);
$args = wp_parse_args( $args, $defaults );
$classes = sanitize_html_class( $args['class'] );
$aria = '';
if ( $args['aria_label'] ) {
$aria = sprintf(
' aria-label="%s"',
esc_attr(
sprintf(
__( 'Estimated reading time: %s minutes', 'rx-theme' ),
$data['minutes']
)
)
);
}
$html = '<span class="' . esc_attr( $classes ) . '"' . $aria . '>';
$html .= rx_get_reading_time(
$post_id,
array(
'show_icon' => $args['show_icon'],
'show_word_count' => $args['show_word_count'],
)
);
$html .= '</span>';
return apply_filters( 'rx_reading_time_badge_html', $html, $data, $post_id, $args );
}
}
/**
* ---------------------------------------------------------
* Echo Badge
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_the_reading_time_badge' ) ) {
function rx_the_reading_time_badge( $post_id = 0, $args = array() ) {
echo wp_kses_post( rx_reading_time_badge( $post_id, $args ) );
}
}
/**
* ---------------------------------------------------------
* Reading Time Full Stats
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_reading_time_stats' ) ) {
function rx_reading_time_stats( $post_id = 0 ) {
$data = rx_get_reading_time_data( $post_id );
if ( empty( $data ) ) {
return '';
}
$html = '<div class="rx-reading-time-stats">';
$html .= '<span class="rx-reading-time-stats__item rx-reading-time-stats__minutes">';
$html .= esc_html( number_format_i18n( $data['minutes'] ) . ' ' . __( 'min read', 'rx-theme' ) );
$html .= '</span>';
$html .= '<span class="rx-reading-time-stats__item rx-reading-time-stats__words">';
$html .= esc_html( number_format_i18n( $data['word_count'] ) . ' ' . __( 'words', 'rx-theme' ) );
$html .= '</span>';
$html .= '<span class="rx-reading-time-stats__item rx-reading-time-stats__paragraphs">';
$html .= esc_html( number_format_i18n( $data['paragraph_count'] ) . ' ' . __( 'paragraphs', 'rx-theme' ) );
$html .= '</span>';
if ( ! empty( $data['image_count'] ) ) {
$html .= '<span class="rx-reading-time-stats__item rx-reading-time-stats__images">';
$html .= esc_html( number_format_i18n( $data['image_count'] ) . ' ' . __( 'images', 'rx-theme' ) );
$html .= '</span>';
}
$html .= '</div>';
return apply_filters( 'rx_reading_time_stats_html', $html, $data, $post_id );
}
}
/**
* ---------------------------------------------------------
* Content Auto Display
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_reading_time_auto_display' ) ) {
function rx_reading_time_auto_display( $content ) {
if ( is_admin() || ! is_singular() || ! in_the_loop() || ! is_main_query() ) {
return $content;
}
if ( ! rx_reading_time_get_setting( 'auto_display', false ) ) {
return $content;
}
$post_id = get_the_ID();
if ( ! rx_reading_time_is_supported_post_type( $post_id ) ) {
return $content;
}
$badge = rx_reading_time_badge( $post_id );
if ( empty( $badge ) ) {
return $content;
}
$wrapper = '<div class="rx-reading-time-auto">' . $badge . '</div>';
$position = rx_reading_time_get_setting( 'auto_display_position', 'before' );
if ( 'after' === $position ) {
return $content . $wrapper;
}
return $wrapper . $content;
}
add_filter( 'the_content', 'rx_reading_time_auto_display', 12 );
}
/**
* ---------------------------------------------------------
* Shortcode: [rx_reading_time]
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_reading_time_shortcode' ) ) {
function rx_reading_time_shortcode( $atts ) {
if ( ! rx_reading_time_get_setting( 'enable_shortcode', true ) ) {
return '';
}
$atts = shortcode_atts(
array(
'id' => get_the_ID(),
'type' => 'badge', // badge, text, stats
'show_icon' => 'true',
'show_word_count' => 'false',
),
$atts,
'rx_reading_time'
);
$post_id = absint( $atts['id'] );
$args = array(
'show_icon' => filter_var( $atts['show_icon'], FILTER_VALIDATE_BOOLEAN ),
'show_word_count' => filter_var( $atts['show_word_count'], FILTER_VALIDATE_BOOLEAN ),
);
if ( 'stats' === $atts['type'] ) {
return rx_reading_time_stats( $post_id );
}
if ( 'text' === $atts['type'] ) {
return rx_get_reading_time( $post_id, $args );
}
return rx_reading_time_badge( $post_id, $args );
}
add_shortcode( 'rx_reading_time', 'rx_reading_time_shortcode' );
}
/**
* ---------------------------------------------------------
* Shortcode: [rx_word_count]
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_word_count_shortcode' ) ) {
function rx_word_count_shortcode( $atts ) {
$atts = shortcode_atts(
array(
'id' => get_the_ID(),
'label' => 'true',
),
$atts,
'rx_word_count'
);
$post_id = absint( $atts['id'] );
if ( ! $post_id ) {
return '';
}
$data = rx_get_reading_time_data( $post_id );
if ( empty( $data['word_count'] ) ) {
return '';
}
$output = number_format_i18n( $data['word_count'] );
if ( filter_var( $atts['label'], FILTER_VALIDATE_BOOLEAN ) ) {
$output .= ' ' . esc_html__( 'words', 'rx-theme' );
}
return '<span class="rx-word-count">' . esc_html( $output ) . '</span>';
}
add_shortcode( 'rx_word_count', 'rx_word_count_shortcode' );
}
/**
* ---------------------------------------------------------
* REST API Field
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_reading_time_register_rest_field' ) ) {
function rx_reading_time_register_rest_field() {
if ( ! rx_reading_time_get_setting( 'enable_rest_field', true ) ) {
return;
}
$post_types = rx_reading_time_get_setting( 'allowed_post_types', array( 'post' ) );
if ( ! is_array( $post_types ) ) {
$post_types = array( 'post' );
}
foreach ( $post_types as $post_type ) {
register_rest_field(
$post_type,
'rx_reading_time',
array(
'get_callback' => function( $object ) {
$post_id = isset( $object['id'] ) ? absint( $object['id'] ) : 0;
return rx_get_reading_time_data( $post_id );
},
'schema' => array(
'description' => __( 'RX reading time data.', 'rx-theme' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
),
)
);
}
}
add_action( 'rest_api_init', 'rx_reading_time_register_rest_field' );
}
/**
* ---------------------------------------------------------
* Admin Column
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_reading_time_add_admin_column' ) ) {
function rx_reading_time_add_admin_column( $columns ) {
if ( ! rx_reading_time_get_setting( 'enable_admin_column', true ) ) {
return $columns;
}
$new_columns = array();
foreach ( $columns as $key => $label ) {
$new_columns[ $key ] = $label;
if ( 'title' === $key ) {
$new_columns['rx_reading_time'] = __( 'Reading Time', 'rx-theme' );
}
}
return $new_columns;
}
}
if ( ! function_exists( 'rx_reading_time_render_admin_column' ) ) {
function rx_reading_time_render_admin_column( $column, $post_id ) {
if ( 'rx_reading_time' !== $column ) {
return;
}
$data = rx_get_reading_time_data( $post_id );
if ( empty( $data['minutes'] ) ) {
echo esc_html__( '—', 'rx-theme' );
return;
}
echo esc_html(
sprintf(
_n( '%s min', '%s mins', $data['minutes'], 'rx-theme' ),
number_format_i18n( $data['minutes'] )
)
);
if ( ! empty( $data['word_count'] ) ) {
echo '<br><small>';
echo esc_html( number_format_i18n( $data['word_count'] ) . ' ' . __( 'words', 'rx-theme' ) );
echo '</small>';
}
}
}
if ( ! function_exists( 'rx_reading_time_admin_columns_init' ) ) {
function rx_reading_time_admin_columns_init() {
$post_types = rx_reading_time_get_setting( 'allowed_post_types', array( 'post' ) );
if ( ! is_array( $post_types ) ) {
$post_types = array( 'post' );
}
foreach ( $post_types as $post_type ) {
add_filter( "manage_{$post_type}_posts_columns", 'rx_reading_time_add_admin_column' );
add_action( "manage_{$post_type}_posts_custom_column", 'rx_reading_time_render_admin_column', 10, 2 );
}
}
add_action( 'admin_init', 'rx_reading_time_admin_columns_init' );
}
/**
* ---------------------------------------------------------
* Schema / Meta Helper
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_reading_time_schema_meta' ) ) {
function rx_reading_time_schema_meta( $post_id = 0 ) {
if ( ! rx_reading_time_get_setting( 'enable_schema_meta', true ) ) {
return '';
}
$post_id = $post_id ? absint( $post_id ) : get_the_ID();
if ( ! $post_id ) {
return '';
}
$data = rx_get_reading_time_data( $post_id );
if ( empty( $data['minutes'] ) ) {
return '';
}
$iso_duration = 'PT' . absint( $data['minutes'] ) . 'M';
return '<meta itemprop="timeRequired" content="' . esc_attr( $iso_duration ) . '">';
}
}
/**
* ---------------------------------------------------------
* Echo Schema Meta in Single Post Head
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_reading_time_wp_head_meta' ) ) {
function rx_reading_time_wp_head_meta() {
if ( ! is_singular() ) {
return;
}
$post_id = get_the_ID();
if ( ! $post_id || ! rx_reading_time_is_supported_post_type( $post_id ) ) {
return;
}
$data = rx_get_reading_time_data( $post_id );
if ( empty( $data['minutes'] ) ) {
return;
}
echo "\n" . '<meta name="rx-reading-time" content="' . esc_attr( $data['minutes'] ) . ' minutes">' . "\n";
echo '<meta name="rx-word-count" content="' . esc_attr( $data['word_count'] ) . '">' . "\n";
}
add_action( 'wp_head', 'rx_reading_time_wp_head_meta', 5 );
}
/**
* ---------------------------------------------------------
* Body Class
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_reading_time_body_class' ) ) {
function rx_reading_time_body_class( $classes ) {
if ( ! is_singular() ) {
return $classes;
}
$post_id = get_the_ID();
if ( ! $post_id ) {
return $classes;
}
$data = rx_get_reading_time_data( $post_id );
if ( empty( $data['minutes'] ) ) {
return $classes;
}
$minutes = absint( $data['minutes'] );
if ( $minutes <= 3 ) {
$classes[] = 'rx-short-read';
} elseif ( $minutes <= 8 ) {
$classes[] = 'rx-medium-read';
} else {
$classes[] = 'rx-long-read';
}
$classes[] = 'rx-reading-time-' . $minutes . '-min';
return $classes;
}
add_filter( 'body_class', 'rx_reading_time_body_class' );
}
/**
* ---------------------------------------------------------
* CSS Output
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_reading_time_inline_css' ) ) {
function rx_reading_time_inline_css() {
?>
<style id="rx-reading-time-css">
.rx-reading-time,
.rx-reading-time-badge,
.rx-reading-time-auto,
.rx-reading-time-stats {
box-sizing: border-box;
}
.rx-reading-time-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px;
line-height: 1.4;
font-weight: 500;
color: var(--rx-reading-time-color, #555);
background: var(--rx-reading-time-bg, rgba(0,0,0,0.04));
border: 1px solid var(--rx-reading-time-border, rgba(0,0,0,0.08));
border-radius: 999px;
padding: 5px 10px;
text-decoration: none;
}
.rx-reading-time__icon {
display: inline-flex;
line-height: 1;
}
.rx-reading-time__words {
opacity: .78;
font-size: .92em;
}
.rx-reading-time-auto {
margin: 0 0 18px;
}
.rx-reading-time-stats {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 16px 0;
}
.rx-reading-time-stats__item {
display: inline-flex;
align-items: center;
border-radius: 8px;
padding: 6px 10px;
background: rgba(0,0,0,0.04);
font-size: 13px;
}
@media (prefers-color-scheme: dark) {
.rx-reading-time-badge {
color: var(--rx-reading-time-dark-color, #ddd);
background: var(--rx-reading-time-dark-bg, rgba(255,255,255,0.08));
border-color: var(--rx-reading-time-dark-border, rgba(255,255,255,0.12));
}
.rx-reading-time-stats__item {
background: rgba(255,255,255,0.08);
}
}
</style>
<?php
}
add_action( 'wp_head', 'rx_reading_time_inline_css', 20 );
}
/**
* ---------------------------------------------------------
* Template Helper: Display in Post Meta
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_post_meta_reading_time' ) ) {
function rx_post_meta_reading_time( $post_id = 0 ) {
$post_id = $post_id ? absint( $post_id ) : get_the_ID();
if ( ! $post_id ) {
return '';
}
return '<span class="rx-post-meta__reading-time">' . rx_reading_time_badge( $post_id ) . '</span>';
}
}
/**
* ---------------------------------------------------------
* Get Raw Minutes Only
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_get_reading_time_minutes' ) ) {
function rx_get_reading_time_minutes( $post_id = 0 ) {
$data = rx_get_reading_time_data( $post_id );
return ! empty( $data['minutes'] ) ? absint( $data['minutes'] ) : 0;
}
}
/**
* ---------------------------------------------------------
* Get Raw Word Count Only
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_get_word_count' ) ) {
function rx_get_word_count( $post_id = 0 ) {
$data = rx_get_reading_time_data( $post_id );
return ! empty( $data['word_count'] ) ? absint( $data['word_count'] ) : 0;
}
}
/**
* ---------------------------------------------------------
* Useful JSON-LD Helper
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_reading_time_json_ld_piece' ) ) {
function rx_reading_time_json_ld_piece( $post_id = 0 ) {
$post_id = $post_id ? absint( $post_id ) : get_the_ID();
if ( ! $post_id ) {
return array();
}
$data = rx_get_reading_time_data( $post_id );
if ( empty( $data['minutes'] ) ) {
return array();
}
return array(
'timeRequired' => 'PT' . absint( $data['minutes'] ) . 'M',
'wordCount' => absint( $data['word_count'] ),
);
}
}
/**
* ---------------------------------------------------------
* Developer Debug Helper
* ---------------------------------------------------------
*/
if ( ! function_exists( 'rx_debug_reading_time' ) ) {
function rx_debug_reading_time( $post_id = 0 ) {
if ( ! current_user_can( 'manage_options' ) ) {
return '';
}
$data = rx_get_reading_time_data( $post_id, true );
return '<pre class="rx-reading-time-debug">' . esc_html( print_r( $data, true ) ) . '</pre>';
}
}
Use it inside your theme template like this:
<?php echo rx_reading_time_badge(); ?>
Or:
<?php rx_the_reading_time_badge(); ?>
Or in post content/editor:
[rx_reading_time]
For full stats:
[rx_reading_time type="stats"]
For only word count:
[rx_word_count]
Best place to show it in single.php or template-parts/content-single.php:
<div class="rx-single-meta">
<?php rx_the_reading_time_badge(); ?>
</div>
This file is safe because every function uses the rx_ prefix, checks ABSPATH, sanitizes output, supports cache, clears cache on post update, and does not break your theme if one feature is unused.