reading-time.php

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.

Leave a Reply

Your email address will not be published. Required fields are marked *