WP Read Tools
A WordPress plugin that enhances your posts with reading time estimation and text-to-speech capabilities via a parametrized customizable shortcode
by Adalberto H. Vega · github.com/ahvega/wp-read-tools · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/ahvega/wp-read-tools/archive/refs/heads/main.zipA WordPress plugin that adds reading time estimation and text-to-speech capabilities to posts. Uses the browser's native Web Speech API with intelligent Latin American Spanish voice selection. No external APIs required.
Features
- Reading Time Estimation — Word count-based calculation at configurable WPM, locale-aware formatting
- Text-to-Speech — Native Web Speech API with pause/resume/stop controls
- Smart Voice Selection — Prioritizes es-US Neural voices, falls back through Latin American Spanish variants
- Page Builder Support — Reads content from Avada/Fusion Builder and Elementor shortcodes by preserving shortcode inner text
- Conditional Asset Loading — Scripts and styles are skipped on archives, home and search pages with no shortcode
- Security — Nonce verification, input sanitization, rate limiting, published-posts-only access
- i18n Ready — Translation-ready with Spanish (es_ES) included
Requirements
| Requirement | Version |
|---|---|
| WordPress | 5.0+ |
| PHP | 7.2+ (8.0+ recommended) |
| Browser | Chrome 33+, Firefox 49+, Safari 7+, Edge 14+ |
Installation
git clone https://github.com/ahvega/wp-read-tools.git
Upload wp-read-tools/ to /wp-content/plugins/ and activate via WordPress admin.
Usage
Shortcode
[readtime] # Reading time only
[readtime read-aloud="yes"] # With text-to-speech
[readtime read-aloud="yes" wpm="200"] # Custom reading speed
Parameters
| Parameter | Default | Description |
|---|---|---|
read-aloud |
"no" |
Enable TTS ("yes" / "no") |
wpm |
180 |
Words per minute for reading time |
class |
"readtime" |
CSS class for container |
link_text |
"Listen" |
TTS button text |
icon_class |
"fas fa-headphones" |
Font Awesome icon class |
content_id |
"" |
Deprecated no-op. Accepted for backward compatibility but ignored; its only consumer never executed and was removed in 1.2.0 |
Theme Integration
// Auto-add to all single posts
add_filter('the_content', function($content) {
if (is_single() && !is_admin()) {
return do_shortcode('[readtime read-aloud="yes"]') . $content;
}
return $content;
});
Voice Selection Strategy
For Spanish content, the TTS engine selects voices in this priority order:
- es-US Neural/Natural — Bilingual, handles English terms in Spanish text
- Any es-US voice
- Latin American Neural — es-MX, es-CR, es-CO, es-GT, es-HN, es-PA, etc.
- Any Latin American voice
- Any es-* voice — Including es-ES as last resort
For non-Spanish content, Neural/Natural voices matching the page language are preferred.
Page Builder Support
The plugin extracts content from page builder shortcodes (Avada/Fusion Builder, Elementor) by stripping shortcode tags while preserving the text content within them. This ensures accurate word counts and proper TTS content regardless of the page builder used.
For best results, include content in WordPress's native post editor field.
Available Filters
// Adjust reading speed per post type
add_filter('wp_read_tools_wpm', function($wpm, $post_id) {
return get_post_type($post_id) === 'product' ? 150 : $wpm;
}, 10, 2);
// Filter content before speech synthesis
add_filter('wp_read_tools_speech_content', function($content, $post_id) {
return $content;
}, 10, 2);
// Disable Font Awesome (if theme already loads it)
add_filter('wp_read_tools_load_fontawesome', '__return_false');
// Force load assets on specific pages
add_filter('wp_read_tools_force_load_assets', '__return_true');
// Disable rate limiting
add_filter('wp_read_tools_enable_rate_limiting', '__return_false');
Architecture
wp-read-tools/
├── wp-read-tools.php # Entry point, constants, init
├── includes/
│ ├── class-wp-read-tools-shortcode.php # [readtime] shortcode & reading time calc
│ ├── class-wp-read-tools-ajax.php # AJAX content retrieval for TTS
│ └── class-wp-read-tools-enqueue.php # Conditional asset loading
├── assets/
│ ├── js/read-aloud.js # Speech synthesis & UI controls
│ └── css/read-tools.css # Plugin styles
└── languages/ # i18n (.pot, .po, .mo)
Debugging
// Enable debug logging in wp-config.php
define('WP_READ_TOOLS_DEBUG', true);
// Check available voices in browser console
speechSynthesis.getVoices().filter(v => v.lang.startsWith('es'));
Changelog
1.2.0 — Correctness & performance
- Fixed: Font Awesome never loaded on a stock install — the plugin's icons rendered as nothing unless the theme shipped Font Awesome. The conflict check included
dashicons, which core registers unconditionally, so it always matched. FA6 class names now recognised - Fixed: Reading times inflated ~10–15% in Spanish, French, Portuguese and German —
str_word_count()is byte-oriented and split every accented word ("canción" = 2 words). Digits were never counted - Fixed: Posts opening with a one-letter word were corrupted — "A mi me gusta" → "Ami me gusta" (A, Y, O, E in Spanish; "I" in English). Drop-caps now matched by name, fixing every occurrence rather than only the first
- Fixed: Citation markers
[1],[15]were deleted from spoken text and word counts - Fixed:
'apostrophes read aloud character by character; leaked into speech - Fixed: latin1-imported content could silently yield empty speech and "0.0 min read"
- Fixed: Two read-aloud links on one page — pausing one and clicking the other resumed the first and left the second stuck on "Pause" permanently
- Fixed: Chrome could queue and speak an article two or three times (voice-loading handler fired repeatedly, never detached)
- Fixed: A database write on every uncached page view, from every anonymous visitor, during shortcode render
- Fixed: Links added after page load (infinite scroll, AJAX archives) had no click handler
- Removed: 412 lines of unreachable page-builder extraction code
- Changed: Very short posts read "0.5 min read" instead of "0.0 min read"
1.1.1 — Security release
The wp_read_tools_get_content AJAX endpoint is registered for logged-out users and is therefore reachable by anonymous remote callers. Update recommended for all sites.
- Security: Password-protected posts could be read without the password. The endpoint gated only on
post_status === 'publish', but password-protected posts keep that status — protection lives inpost_password. Access control now combines a post-type allowlist,is_post_publicly_viewable()andpost_password_required() - Security: Non-public post types stored as
publish(reusable blocks, field groups, private CPTs) were readable by enumerating post IDs - Security: The cache lookup ran before the access check, serving cached bodies without authorization
- Security: Rate limiting ran before nonce verification, letting nonce-less requests allocate transients
- Security:
X-Forwarded-For/CF-Connecting-IPwere trusted unconditionally — rotating the header bypassed the rate limit, and setting it to a third party's address locked that person out. Now gated behind the newwp_read_tools_trusted_proxiesfilter and parsed right-to-left - Fixed: Rate limiting silently disabled itself on containerised/load-balanced installs where the peer address is private
- Fixed: The limiter refreshed its window on every request, so a steady low-rate caller could stay blocked indefinitely. Now a fixed window
- Added:
wp_read_tools_max_content_lengthcap (default 500 KB), truncated on character boundaries
Upgrade note: the post-type allowlist defaults to all publicly-registered types. Sites reading aloud a type registered publicly_queryable => false must opt in via wp_read_tools_allowed_post_types.
1.1.0
- Fixed: Reading time showing 0.0 for Avada/Fusion Builder posts — shortcode tags are now stripped while preserving inner content instead of using
strip_shortcodes()which removed content within registered shortcodes - Fixed: TTS reading theme configuration data instead of article text — replaced aggressive database meta-field extraction with standard
post_contentretrieval - Fixed: Speech synthesis errors on previously working articles caused by oversized/malformed content from meta-field concatenation
- Improved: Voice selection now prioritizes es-US Neural/Natural voices with Latin American Spanish fallback chain
- Improved: Speech rate and pitch set to natural defaults (1.0)
1.0.0
- Initial release
- Reading time estimation via
[readtime]shortcode - Text-to-speech with Web Speech API
- Avada and Elementor content detection
- Conditional asset loading
- i18n support with Spanish translation
License
GPL v2 or later — see LICENSE.
Author
Adalberto H. Vega — ahvega