{"id":255570,"date":"2025-11-01T05:12:49","date_gmt":"2025-11-01T05:12:49","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/init-reading-position\/"},"modified":"2026-08-02T06:17:23","modified_gmt":"2026-08-02T06:17:23","slug":"init-reading-position","status":"publish","type":"plugin","link":"https:\/\/ko.wordpress.org\/plugins\/init-reading-position\/","author":14479633,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"1.8","stable_tag":"1.8","tested":"7.0.2","requires":"5.5","requires_php":"7.4","requires_plugins":null,"header_name":"Init Reading Position","header_author":"Init HTML","header_description":"Remembers where readers left off in a post and automatically scrolls back to that spot when they return. Lightweight, localStorage-based.","assets_banners_color":"dfcfd8","last_updated":"2026-08-02 06:17:23","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/inithtml.com\/plugin\/init-reading-position\/","header_author_uri":"https:\/\/inithtml.com\/","rating":5,"author_block_rating":0,"active_installs":80,"downloads":1176,"num_ratings":1,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.0.1":{"tag":"1.0.1","author":"brokensmile.2103","date":"2025-11-01 05:12:36"},"1.1":{"tag":"1.1","author":"brokensmile.2103","date":"2025-11-05 06:24:32"},"1.2":{"tag":"1.2","author":"brokensmile.2103","date":"2025-11-12 06:47:42"},"1.3":{"tag":"1.3","author":"brokensmile.2103","date":"2026-04-21 14:43:31"},"1.3.1":{"tag":"1.3.1","author":"brokensmile.2103","date":"2026-04-21 15:07:22"},"1.4":{"tag":"1.4","author":"brokensmile.2103","date":"2026-04-21 15:44:05"},"1.4.1":{"tag":"1.4.1","author":"brokensmile.2103","date":"2026-04-21 16:13:37"},"1.4.2":{"tag":"1.4.2","author":"brokensmile.2103","date":"2026-04-21 16:44:29"},"1.5":{"tag":"1.5","author":"brokensmile.2103","date":"2026-04-23 07:13:43"},"1.5.1":{"tag":"1.5.1","author":"brokensmile.2103","date":"2026-04-23 07:29:08"},"1.6":{"tag":"1.6","author":"brokensmile.2103","date":"2026-04-23 09:18:11"},"1.6.1":{"tag":"1.6.1","author":"brokensmile.2103","date":"2026-05-17 15:59:00"},"1.7":{"tag":"1.7","author":"brokensmile.2103","date":"2026-08-01 13:18:47"},"1.8":{"tag":"1.8","author":"brokensmile.2103","date":"2026-08-02 06:17:23"}},"upgrade_notice":[],"ratings":{"1":0,"2":0,"3":0,"4":0,"5":1},"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3387907,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3387907,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3387907,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3387907,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{},"all_blocks":[],"tagged_versions":["1.0.1","1.1","1.2","1.3","1.3.1","1.4","1.4.1","1.4.2","1.5","1.5.1","1.6","1.6.1","1.7","1.8"],"block_files":[],"assets_screenshots":{"screenshot-1.png":{"filename":"screenshot-1.png","revision":3390164,"resolution":"1","location":"assets","locale":"","width":748,"height":293}},"screenshots":{"1":"Simple settings page \u2014 choose post types and optionally enter a CSS selector (e.g. <code>.entry-content<\/code>) to limit where reading progress is tracked."}},"plugin_section":[],"plugin_tags":[9342,260907,225949,250074,790],"plugin_category":[],"plugin_contributors":[242666],"plugin_business_model":[],"class_list":["post-255570","plugin","type-plugin","status-publish","hentry","plugin_tags-reading","plugin_tags-reading-position","plugin_tags-reading-progress","plugin_tags-resume-reading","plugin_tags-scroll","plugin_contributors-brokensmile2103-1","plugin_committers-brokensmile2103-1"],"banners":{"banner":"https:\/\/ps.w.org\/init-reading-position\/assets\/banner-772x250.png?rev=3387907","banner_2x":"https:\/\/ps.w.org\/init-reading-position\/assets\/banner-1544x500.png?rev=3387907","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":false,"icon":"https:\/\/ps.w.org\/init-reading-position\/assets\/icon-128x128.png?rev=3387907","icon_2x":"https:\/\/ps.w.org\/init-reading-position\/assets\/icon-256x256.png?rev=3387907","generated":false},"screenshots":[{"src":"https:\/\/ps.w.org\/init-reading-position\/assets\/screenshot-1.png?rev=3390164","caption":"Simple settings page \u2014 choose post types and optionally enter a CSS selector (e.g. <code>.entry-content<\/code>) to limit where reading progress is tracked."}],"raw_content":"<!--section=description-->\n<p>Init Reading Position enhances the reading experience by remembering how far a visitor has read on a post.<br \/>\nWhen they return, it automatically scrolls back to where they left off.<\/p>\n\n<p>Perfect for:<\/p>\n\n<ul>\n<li>Long-form articles<\/li>\n<li>Tutorials or guides<\/li>\n<li>Web novels or manga<\/li>\n<li>Any content where readers often stop and come back later<\/li>\n<\/ul>\n\n<p>This plugin is part of the <a href=\"https:\/\/en.inithtml.com\/init-plugin-suite-minimalist-powerful-and-free-wordpress-plugins\/\">Init Plugin Suite<\/a> \u2014 a collection of minimalist, fast, and developer-focused tools for WordPress.<\/p>\n\n<p>GitHub repository: <a href=\"https:\/\/github.com\/brokensmile2103\/init-reading-position\">https:\/\/github.com\/brokensmile2103\/init-reading-position<\/a><\/p>\n\n<p><strong>Features<\/strong><\/p>\n\n<ul>\n<li>Saves scroll position using localStorage (guests) or a dedicated database table (logged-in users)<\/li>\n<li>Smart device-based sync: remembers position separately for PC, Mobile, and Tablet<\/li>\n<li>Automatically scrolls back on page load<\/li>\n<li>Lightweight, no jQuery, no bloat<\/li>\n<li>Easy to extend via filters<\/li>\n<li>Optional settings page to control which post types are enabled<\/li>\n<\/ul>\n\n<h3>License<\/h3>\n\n<p>This plugin is licensed under the GPLv2 or later.<br \/>\nYou are free to use, modify, and distribute it under the same license.<\/p>\n\n<!--section=installation-->\n<ol>\n<li>Upload the plugin folder to <code>\/wp-content\/plugins\/<\/code> or install via Plugins \u2192 Add New.<\/li>\n<li>Activate the plugin.<\/li>\n<li>Go to <strong>Settings \u2192 Reading Position<\/strong> and choose which post types should support this feature.<\/li>\n<\/ol>\n\n<!--section=faq-->\n<dl>\n<dt id=\"does%20it%20sync%20across%20devices%3F\"><h3>Does it sync across devices?<\/h3><\/dt>\n<dd><p>Yes. For logged-in users, scroll position is stored in a dedicated database table, saved separately for each device type (PC, Mobile, Tablet).<\/p><\/dd>\n<dt id=\"will%20it%20work%20with%20custom%20post%20types%3F\"><h3>Will it work with custom post types?<\/h3><\/dt>\n<dd><p>Yes. You can enable it for any public post type in the plugin settings page.<\/p><\/dd>\n<dt id=\"will%20it%20slow%20down%20my%20site%3F\"><h3>Will it slow down my site?<\/h3><\/dt>\n<dd><p>No. It only runs a small JS script on enabled single pages and stores data efficiently in a dedicated table with indexed queries.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>1.8 \u2013 August 2, 2026<\/h4>\n\n<ul>\n<li>Fixed: The frontend script could fail to enqueue on every page of the site whenever the option controlling enabled post types had never been saved (fresh installs, or sites where the admin never opened Settings) \u2014 a mismatched default value caused the enabled post-type list to resolve to empty instead of the intended <code>post<\/code> default<\/li>\n<li>Fixed: Reading position saves could silently fail intermittently (\"works, then doesn't\") \u2014 the script previously marked a position as synced as soon as a save request was sent, without waiting for server confirmation; combined with server-side rate limiting on sites with a persistent object cache (Redis\/Memcached), some saves were silently dropped while the script believed they had succeeded, resulting in stale or incorrect resume positions<\/li>\n<li>Added: The final save sent when a tab is hidden\/closed now bypasses server-side rate limiting and always writes straight to the database \u2014 this is the last chance to persist unsaved progress, so it's no longer subject to being silently throttled or deferred away<\/li>\n<li>Improved: Save requests to the REST API now return an explicit <code>throttled<\/code> status instead of a false \"success\" when rate-limited, so the script can correctly retry instead of assuming the position was saved<\/li>\n<li>Added: On sites with a persistent object cache, heartbeat saves (the periodic safety-net sync sent while a reader keeps scrolling forward, as opposed to a real re-read reversal or the final save) are now written to cache instead of the database. This cuts sustained database write load dramatically on high-concurrency sites \u2014 e.g. a manga\/webtoon theme with tens of thousands of simultaneously reading logged-in users \u2014 while page reloads still resume from the correct position, since reads are cache-first. Progress is always promoted to a real, durable database write at the next meaningful checkpoint (re-read reversal or tab close). Sites without a persistent object cache are unaffected and continue writing straight to the database, same as before<\/li>\n<li>Added: The heartbeat interval (default 30s) is now filterable via <code>init_plugin_suite_reading_position_heartbeat<\/code>, letting high-traffic sites widen the safety-net cadence to further reduce request volume at peak hours without editing plugin files<\/li>\n<li>No breaking changes \u2014 REST API, database schema, localStorage keys, and frontend behavior remain fully compatible<\/li>\n<\/ul>\n\n<h4>1.7 \u2013 August 1, 2026<\/h4>\n\n<ul>\n<li>Added: Behavior-based sync engine replacing the fixed 5-second timer \u2014 the script now only talks to the server on meaningful checkpoints (a real upward reversal while re-reading, an occasional heartbeat while continuously reading forward, or right before the tab is hidden\/closed), cutting network requests dramatically for the common case of a reader scrolling straight through an article<\/li>\n<li>Improved: The furthest scroll position ever reached is what gets persisted, not the current position \u2014 scrolling back up to re-read a section never regresses the saved\/resume point, for both logged-in users and guests (localStorage)<\/li>\n<li>Improved: Scroll and resize listeners are now registered as passive \u2014 the browser no longer waits on the handler before scrolling, removing a source of jank on scroll-heavy pages<\/li>\n<li>Improved: Full-page scroll height is now cached alongside scoped-element bounds instead of being read on every debounced scroll tick \u2014 avoids repeated layout-forcing reads; refreshed on resize and once after full page load<\/li>\n<li>Added: Best-effort final save on tab hide\/close (<code>pagehide<\/code> \/ <code>visibilitychange<\/code>) using <code>navigator.sendBeacon<\/code> where available \u2014 the last known position is no longer lost when a reader closes the tab between two sync checkpoints<\/li>\n<li>Improved: Reading position lookups now skip the legacy user_meta fallback entirely once the user_meta \u2192 database migration is confirmed complete \u2014 removes 1-2 unnecessary lookups per uncached request on the vast majority of sites<\/li>\n<li>Improved: Removed a redundant <code>user_id<\/code> database index \u2014 it duplicated the leftmost prefix of the existing <code>user_post_device<\/code> unique key, so dropping it removes unnecessary index-maintenance overhead on every write with no read-performance impact. Existing sites get this cleaned up automatically and safely, exactly once, on upgrade<\/li>\n<li>No breaking changes \u2014 REST API, database schema (aside from the redundant index removal above), localStorage keys, and frontend behavior remain fully compatible<\/li>\n<\/ul>\n\n<h4>1.6 \u2013 April 23, 2026<\/h4>\n\n<ul>\n<li>Improved: Reading position retrieval is now handled via a single bulk query instead of multiple per-device lookups \u2014 reduces database queries and improves performance for logged-in users<\/li>\n<li>Added: Bulk cache layer for multi-device reading positions with a deterministic cache key (<code>pc_mobile_tablet<\/code>) \u2014 ensures consistent cache hits and simplifies invalidation logic<\/li>\n<li>Improved: Cache invalidation now clears both single-device and bulk cache entries \u2014 prevents stale data when users continue reading across sessions<\/li>\n<li>Improved: Data flow refactored to return all device positions (<code>pc<\/code>, <code>mobile<\/code>, <code>tablet<\/code>) in one payload \u2014 frontend now selects the correct device slot dynamically<\/li>\n<li>Fixed: Inconsistent cache key generation (previous md5-based keys) that could lead to stale or non-invalidated cache entries<\/li>\n<li>Fixed: Potential PHP notice when <code>savedPositions<\/code> was undefined for guest users \u2014 now always initialized with default values<\/li>\n<li>Improved: Internal code cleanup and minor optimizations for better maintainability and consistency<\/li>\n<li>No breaking changes \u2014 REST API, database schema, localStorage keys, and frontend behavior remain fully compatible<\/li>\n<\/ul>\n\n<h4>1.5 \u2013 April 23, 2026<\/h4>\n\n<ul>\n<li>Improved: Device detection is now handled entirely by JavaScript \u2014 PHP no longer performs UA sniffing on the server side, eliminating mismatches between detected device on page load vs. save<\/li>\n<li>Improved: Saved scroll position lookup now tries all three device slots (pc, mobile, tablet) in order, so the correct position is always restored regardless of which device the user was last on<\/li>\n<li>Improved: Scope element bounds (<code>absTop<\/code>, <code>absBottom<\/code>) are now cached after DOMContentLoaded and refreshed on window resize \u2014 <code>getBoundingClientRect()<\/code> is no longer called on every scroll event<\/li>\n<li>Improved: REST API rate limiting switched from transient-based to <code>wp_cache<\/code>-based \u2014 eliminates transient writes to <code>wp_options<\/code> on high-traffic sites; works best with an object cache (Redis\/Memcached), degrades gracefully without one<\/li>\n<li>Fixed: <code>autoClearOnEnd<\/code> with no CSS selector configured would never trigger the end-of-content clear; now correctly falls back to page-bottom detection in that case<\/li>\n<li>No breaking changes \u2014 REST API, localStorage keys, and all existing settings remain fully compatible<\/li>\n<\/ul>\n\n<h4>1.4 \u2013 April 21, 2026<\/h4>\n\n<ul>\n<li>Refactored: Migration now runs via a self-scheduling WP-Cron event instead of admin_init<\/li>\n<li>Added: Automatic re-scheduling every 30 seconds until migration completes<\/li>\n<li>Improved: Removed dependency on admin requests for background processing<\/li>\n<li>Improved: Added locking to prevent overlapping migration runs<\/li>\n<li>No breaking changes<\/li>\n<\/ul>\n\n<h4>1.3 \u2013 April 21, 2026<\/h4>\n\n<ul>\n<li>Added: Switched from user_meta storage to a dedicated custom database table for reading positions<\/li>\n<li>Added: Automatic migration from existing user_meta data to the new database structure (runs safely in the background)<\/li>\n<li>Added: Optimized database schema with unique keys (user_id, post_id, device) for efficient upsert operations<\/li>\n<li>Improved: Faster read\/write performance and better scalability for large sites<\/li>\n<li>Improved: Reduced API request frequency by adding a throttle layer on top of existing debounce<\/li>\n<li>Improved: Device detection now uses normalized lowercase values (<code>pc<\/code>, <code>mobile<\/code>, <code>tablet<\/code>) for full backend consistency<\/li>\n<li>Improved: Added fail-safe handling for REST requests to prevent JavaScript errors on network issues<\/li>\n<li>Performance: Significantly reduced server load for logged-in users and improved overall responsiveness<\/li>\n<li>Backward-compatible: Existing user_meta data is migrated seamlessly, with fallback support during transition<\/li>\n<li>No breaking changes \u2014 REST API, localStorage keys, and frontend behavior remain fully compatible<\/li>\n<\/ul>\n\n<h4>1.2 \u2013 November 12, 2025<\/h4>\n\n<ul>\n<li>Added: Support for multiple CSS selectors separated by commas (e.g. <code>.entry-content, .post-content, #main<\/code>)<\/li>\n<li>Added: Option \"Auto-clear saved position at content end\" (enabled by default)<\/li>\n<li>Improved: Scroll tracking now activates if the reader is inside <em>any<\/em> of the configured selector areas<\/li>\n<li>Improved: Percent calculation prioritizes the selector in scope, falls back to whole page when outside all selectors<\/li>\n<li>Behavior: When auto-clear is enabled, progress is cleared at the end of the content area; when disabled, it falls back to clearing at page end<\/li>\n<li>Zero breaking changes \u2014 existing settings, localStorage keys, and user_meta structure remain compatible<\/li>\n<\/ul>\n\n<h4>1.1 \u2013 November 5, 2025<\/h4>\n\n<ul>\n<li>Added: CSS Selector option \u2014 plugin now only tracks progress inside the selected content area (e.g. <code>.entry-content<\/code>)<\/li>\n<li>Added: i18n ready strings for new settings field (English + Vietnamese translations included)<\/li>\n<li>Improved: Scroll position tracking logic \u2014 no longer saves progress when user scrolls outside the selected content area (e.g. comments section)<\/li>\n<li>Improved: Cleanup behavior \u2014 scroll progress is deleted only when reaching end of the page, never based on selector range<\/li>\n<li>No breaking changes \u2014 keeps old localStorage keys and user meta structure<\/li>\n<li>Updated: Admin settings page UI\/UX (placeholder instead of default selector)<\/li>\n<\/ul>\n\n<h4>1.0 \u2013 May 20, 2025<\/h4>\n\n<ul>\n<li>Initial release<\/li>\n<li>Saves scroll position using localStorage (guests) and user meta (logged-in users)<\/li>\n<li>Auto-scrolls back to last position on load<\/li>\n<li>Per-device storage (PC, Mobile, Tablet)<\/li>\n<li>Settings page to choose post types<\/li>\n<li>Filter <code>init_plugin_suite_reading_position_delay<\/code> to adjust debounce time<\/li>\n<\/ul>","raw_excerpt":"Remembers reading position and auto-scrolls when returning. Works for guests (localStorage) and logged-in users (custom DB table, per device).","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/255570","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=255570"}],"author":[{"embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/brokensmile2103-1"}],"wp:attachment":[{"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=255570"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=255570"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=255570"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=255570"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=255570"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=255570"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}