{"id":316658,"date":"2026-06-14T04:39:18","date_gmt":"2026-06-14T04:39:18","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/effortless-simple-reviews-editor\/"},"modified":"2026-06-14T10:42:59","modified_gmt":"2026-06-14T10:42:59","slug":"effortless-simple-reviews-editor","status":"publish","type":"plugin","link":"https:\/\/wordpress.org\/plugins\/effortless-simple-reviews-editor\/","author":23148025,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"2.2.4","stable_tag":"2.2.4","tested":"7.0","requires":"6.2","requires_php":"8.0","requires_plugins":null,"header_name":"EffortLess Simple Reviews Editor","header_author":"domclic","header_description":"Front-end review submission, admin approval, and responsive card grid with infinite scroll.","assets_banners_color":"9e9e9e","last_updated":"2026-06-14 10:42:59","external_support_url":"","external_repository_url":"","donate_link":"https:\/\/id7.dev\/donate\/","header_plugin_uri":"","header_author_uri":"","rating":0,"author_block_rating":0,"active_installs":0,"downloads":37,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"2.2.3":{"tag":"2.2.3","author":"domclic","date":"2026-06-14 04:38:50"},"2.2.4":{"tag":"2.2.4","author":"domclic","date":"2026-06-14 10:42:59"}},"upgrade_notice":[],"ratings":[],"assets_icons":{"icon-128x128.jpg":{"filename":"icon-128x128.jpg","revision":3571777,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.jpg":{"filename":"icon-256x256.jpg","revision":3571777,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256}},"assets_banners":{"banner-1544x500.jpg":{"filename":"banner-1544x500.jpg","revision":3571777,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.jpg":{"filename":"banner-772x250.jpg","revision":3571777,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{},"all_blocks":[],"tagged_versions":["2.2.3","2.2.4"],"block_files":[],"assets_screenshots":[],"screenshots":{"1":"<strong>Reviews list<\/strong> \u2014 WordPress admin list with rating, reviewer name, and date columns.","2":"<strong>Review edit screen<\/strong> \u2014 meta box with star picker, name, text, and date fields.","3":"<strong>Review Links page<\/strong> \u2014 generate, copy, and manage one-time-use review links.","4":"<strong>Front-end reviews grid<\/strong> \u2014 responsive card grid with infinite scroll.","5":"<strong>Front-end submission form<\/strong> \u2014 star rating picker, name, and review text fields."}},"plugin_section":[],"plugin_tags":[5908,8203,1519,2293,1518],"plugin_category":[53],"plugin_contributors":[241557],"plugin_business_model":[],"class_list":["post-316658","plugin","type-plugin","status-publish","hentry","plugin_tags-infinite-scroll","plugin_tags-ratings","plugin_tags-reviews","plugin_tags-star-rating","plugin_tags-testimonials","plugin_category-ratings-and-reviews","plugin_contributors-domclic","plugin_committers-domclic"],"banners":{"banner":"https:\/\/ps.w.org\/effortless-simple-reviews-editor\/assets\/banner-772x250.jpg?rev=3571777","banner_2x":"https:\/\/ps.w.org\/effortless-simple-reviews-editor\/assets\/banner-1544x500.jpg?rev=3571777","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":false,"icon":"https:\/\/ps.w.org\/effortless-simple-reviews-editor\/assets\/icon-128x128.jpg?rev=3571777","icon_2x":"https:\/\/ps.w.org\/effortless-simple-reviews-editor\/assets\/icon-256x256.jpg?rev=3571777","generated":false},"screenshots":[],"raw_content":"<!--section=description-->\n<p>EffortLess Simple Reviews Editor lets visitors submit reviews (pending admin approval) and displays published reviews in a responsive card grid with infinite scroll.<\/p>\n\n<p><strong>Features:<\/strong><\/p>\n\n<ul>\n<li>Front-end review submission form with star rating, name, title, and review text fields<\/li>\n<li>Admin approval workflow \u2014 all submissions are set to \"Pending\"<\/li>\n<li>Responsive card grid with configurable columns<\/li>\n<li>Infinite scroll (IntersectionObserver) for seamless loading<\/li>\n<li>Spam protection: honeypot field + rate limiting (1 submission per 60 s)<\/li>\n<li>Accessible star rating with keyboard navigation<\/li>\n<li>One-time-use review links \u2014 send unique links to clients<\/li>\n<li>Programmatic link generation via <code>elsre_create_review_link()<\/code><\/li>\n<li>No external dependencies \u2014 pure CSS stars, vanilla JavaScript<\/li>\n<li>i18n ready \u2014 <code>.pot<\/code> translation template included<\/li>\n<\/ul>\n\n<h3>Shortcodes<\/h3>\n\n<h4>[elsre_reviews] \u2014 Display Reviews Grid<\/h4>\n\n<p>Place this shortcode on any page or post to display published reviews.<\/p>\n\n<p>Attributes:<\/p>\n\n<ul>\n<li><code>per_page<\/code> (default: <code>6<\/code>) \u2014 Reviews per load batch<\/li>\n<li><code>columns<\/code> (default: <code>3<\/code>) \u2014 Grid columns on desktop (1\u20136)<\/li>\n<li><code>ids<\/code> (default: empty) \u2014 Comma-separated review IDs; disables pagination<\/li>\n<li><code>order<\/code> (default: <code>desc<\/code>) \u2014 Sort order: <code>desc<\/code> for newest first, <code>asc<\/code> for oldest first<\/li>\n<li><code>star_size<\/code> (default: <code>18px<\/code>) \u2014 Star icon size (any CSS unit)<\/li>\n<li><code>text_size<\/code> (default: <code>15px<\/code>) \u2014 Review text font size<\/li>\n<li><code>name_size<\/code> (default: <code>14px<\/code>) \u2014 Reviewer name font size<\/li>\n<\/ul>\n\n<p>Examples:<\/p>\n\n<pre><code>[elsre_reviews]\n[elsre_reviews per_page=\"9\" columns=\"3\"]\n[elsre_reviews ids=\"12,45,78\" columns=\"2\"]\n[elsre_reviews star_size=\"28px\" text_size=\"18px\" name_size=\"16px\"]\n<\/code><\/pre>\n\n<h4>[elsre_form] \u2014 Review Submission Form<\/h4>\n\n<p>Place this shortcode on any page or post to display the submission form.<\/p>\n\n<p>Attributes:<\/p>\n\n<ul>\n<li><code>require_token<\/code> (default: <code>no<\/code>) \u2014 Set to <code>yes<\/code> to require a one-time-use link token<\/li>\n<li><code>star_size<\/code> (default: <code>32px<\/code>) \u2014 Star icon size in the rating picker<\/li>\n<\/ul>\n\n<p>Examples:<\/p>\n\n<pre><code>[elsre_form]\n[elsre_form require_token=\"yes\"]\n[elsre_form star_size=\"48px\"]&lt;h3&gt;One-Time Review Links&lt;\/h3&gt;\n<\/code><\/pre>\n\n<p>1. Create a page with <code>[elsre_form require_token=\"yes\"]<\/code>.\n2. Go to <strong>Reviews \u2192 Review Links<\/strong> in the admin.\n3. Click <strong>Generate Link<\/strong> (optionally add a label for your reference).\n4. Copy the link and send it to your client.<\/p>\n\n<p>Each link works only once. After the client submits their review, the link is marked as used.<\/p>\n\n<h3>Developer Integration<\/h3>\n\n<p>Generate review links programmatically from your own plugin or theme:<\/p>\n\n<pre><code>$link = elsre_create_review_link( 'client@example.com' );\n$link = elsre_create_review_link( 'client@example.com', 'John Doe', 42 );\n<\/code><\/pre>\n\n<ul>\n<li>First parameter: client email (unique identifier \u2014 reuses existing unused token if found)<\/li>\n<li>Second parameter: optional label for admin reference<\/li>\n<li>Third parameter: optional page ID containing <code>[elsre_form]<\/code> (auto-detected if omitted)<\/li>\n<li>Returns the full URL with token, or empty string if no form page found<\/li>\n<\/ul>\n\n<p>Check availability before calling: <code>function_exists( 'elsre_create_review_link' )<\/code><\/p>\n\n<h3>Admin Pages<\/h3>\n\n<h4>Reviews List (Dashboard \u2192 Reviews)<\/h4>\n\n<p>All submitted reviews are listed here as a standard WordPress post list.\nExtra columns show the key review data at a glance:<\/p>\n\n<pre><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Title        \u2502 Rating \u2502 Reviewer     \u2502 Date        \u2502 Status     \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Great hotel! \u2502 \u2605\u2605\u2605\u2605\u2605  \u2502 Jane Doe     \u2502 2026-03-01  \u2502 Pending    \u2502\n\u2502 Good service \u2502 \u2605\u2605\u2605\u2605\u2606  \u2502 John Smith   \u2502 2026-02-28  \u2502 Published  \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n<\/code><\/pre>\n\n<p>Publish a review to make it appear on the front end. Leave it as Pending to\nkeep it hidden until you have reviewed it.<\/p>\n\n<h4>Review Edit Screen<\/h4>\n\n<p>Each review has a <strong>Review Details<\/strong> meta box below the title field:<\/p>\n\n<pre><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 REVIEW DETAILS                          \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Reviewer Name\u2502 Jane Doe                 \u2502\n\u2502 Review Title \u2502 Perfect weekend getaway! \u2502\n\u2502 Rating       \u2502 \u2605 \u2605 \u2605 \u2605 \u2605               \u2502\n\u2502 Review Text  \u2502 Fantastic experience...  \u2502\n\u2502 Review Date  \u2502 2026-03-01               \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n<\/code><\/pre>\n\n<p>The star picker is fully keyboard-accessible (arrow keys, Enter, Space).<\/p>\n\n<h4>Review Links Page (Dashboard \u2192 Reviews \u2192 Review Links)<\/h4>\n\n<p>Generate one-time-use links to send to specific clients:<\/p>\n\n<pre><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 GENERATE NEW LINK                                            \u2502\n\u2502  Client Email  [client@example.com          ]               \u2502\n\u2502  Label         [e.g. John's booking         ]  (optional)   \u2502\n\u2502  Form Page     [Leave a Review \u25be            ]               \u2502\n\u2502                [ Generate Link ]                            \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 ALL LINKS                                                            \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Email        \u2502 Label     \u2502 Status \u2502 Created    \u2502 Used   \u2502 Actions   \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 jane@...     \u2502 Jane Doe  \u2502 Active \u2502 2026-03-01 \u2502 \u2014      \u2502 Copy Link \u2502\n\u2502 john@...     \u2502 John Smith\u2502 Used   \u2502 2026-02-28 \u2502 Mar 01 \u2502 Expired   \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n<\/code><\/pre>\n\n<p>After generating, a success banner shows the link ready to copy:<\/p>\n\n<pre><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 \u2713  Link generated! Send this link to your client:           \u2502\n\u2502   https:\/\/example.com\/review\/?elsre_token=abc123  [ Copy ] \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n<\/code><\/pre>\n\n<!--section=installation-->\n<ol>\n<li>Upload the <code>effortless-simple-reviews-editor<\/code> folder to <code>\/wp-content\/plugins\/<\/code>.<\/li>\n<li>Activate the plugin through the <strong>Plugins<\/strong> menu in WordPress.<\/li>\n<li>A new <strong>Reviews<\/strong> menu (star icon) appears in the admin sidebar.<\/li>\n<li>Add <code>[elsre_reviews]<\/code> to a page to display reviews.<\/li>\n<li>Add <code>[elsre_form]<\/code> to a page to allow visitors to submit reviews.<\/li>\n<li>For one-time links: use <code>[elsre_form require_token=\"yes\"]<\/code>, then generate links via <strong>Reviews \u2192 Review Links<\/strong>.<\/li>\n<\/ol>\n\n<!--section=faq-->\n<dl>\n<dt id=\"where%20do%20submitted%20reviews%20go%3F\"><h3>Where do submitted reviews go?<\/h3><\/dt>\n<dd><p>All submissions are saved with \"Pending\" status. Go to <strong>Reviews<\/strong> in the admin to approve them.<\/p><\/dd>\n<dt id=\"can%20i%20display%20specific%20reviews%20only%3F\"><h3>Can I display specific reviews only?<\/h3><\/dt>\n<dd><p>Yes, use the <code>ids<\/code> attribute: <code>[elsre_reviews ids=\"12,45,78\"]<\/code><\/p><\/dd>\n<dt id=\"how%20do%20i%20translate%20the%20plugin%3F\"><h3>How do I translate the plugin?<\/h3><\/dt>\n<dd><p>Copy <code>languages\/elsre.pot<\/code> to <code>languages\/elsre-{locale}.po<\/code> (e.g. <code>elsre-fr_FR.po<\/code>), translate, then compile to <code>.mo<\/code>.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>2.2.4<\/h4>\n\n<ul>\n<li>Add dismissible donate notice on Reviews admin screens; add Donate link header<\/li>\n<\/ul>\n\n<h4>2.2.3<\/h4>\n\n<ul>\n<li>Security: added the <code>elsre_require_token<\/code> filter so sites can enforce invitation-only reviews server-side \u2014 when it returns true, tokenless submissions to <code>admin-ajax.php<\/code> are rejected (the <code>require_token=\"yes\"<\/code> shortcode attribute only hid the form in the browser).<\/li>\n<li>Fix: <code>elsre_create_review_link()<\/code> now keeps a reused token's stored <code>page_id<\/code> in sync with the link it builds, matching the admin Generate Link flow.<\/li>\n<li>Code: token rollback after a failed insert is now guarded so it only runs for token-based submissions; shared review-query and ID-parsing logic extracted to <code>ELSRE_Shortcodes<\/code> helpers; flash-notice transients read via a single <code>consume_transient()<\/code> helper.<\/li>\n<\/ul>\n\n<h4>2.2.2<\/h4>\n\n<ul>\n<li>Code: removed <code>plugins_api<\/code> filter and associated <code>plugin_row_meta<\/code> \"View details\" link \u2014 WordPress.org hosting serves plugin information natively; the local interceptor was flagged as an unauthorized update\/phone-home mechanism.<\/li>\n<\/ul>\n\n<h4>2.2.1<\/h4>\n\n<ul>\n<li>Security: all table-name SQL references migrated from <code>esc_sql()<\/code> + backtick interpolation to the <code>%i<\/code> identifier placeholder (WordPress 6.2+).<\/li>\n<li>Security: <code>count_tokens()<\/code> now wraps <code>$wpdb-&gt;get_var()<\/code> in <code>prepare()<\/code> instead of passing a raw SQL string.<\/li>\n<li>UX: invalid email on the Generate Link form now shows an admin error notice instead of silently ignoring the input.<\/li>\n<li>Fix: <code>uninstall.php<\/code> now also removes <code>elsre_error_*<\/code> transients created by the email validation notice.<\/li>\n<\/ul>\n\n<h4>2.2.0<\/h4>\n\n<ul>\n<li>Fix: token is now restored (un-consumed) when <code>wp_insert_post()<\/code> fails after atomic consume, so the review link remains valid and the client can retry.<\/li>\n<li>Fix: <code>uninstall.php<\/code> now also removes <code>_transient_timeout_<\/code> records for <code>elsre_generated_<\/code>, <code>elsre_deleted_<\/code>, and <code>elsre_form_pages<\/code> transients, preventing orphan rows in <code>wp_options<\/code>.<\/li>\n<li>Performance: <code>get_form_pages()<\/code> result is now cached for one hour via transient (invalidated on <code>save_post<\/code> \/ <code>delete_post<\/code>).<\/li>\n<li>Performance: Review Links admin page now paginates token rows (50 per page) instead of loading all rows at once.<\/li>\n<li>Code: <code>render_admin_columns()<\/code> switch now has an explicit <code>default<\/code> branch.<\/li>\n<li>Code: <code>render_form()<\/code> passes the template variable via <code>extract()<\/code> to match the <code>load_card_template()<\/code> pattern.<\/li>\n<li>Security: removed the descriptive <code>&lt;!-- Honeypot field --&gt;<\/code> HTML comment from the front-end form.<\/li>\n<\/ul>\n\n<h4>2.1.9<\/h4>\n\n<ul>\n<li>Fix: <code>post_title<\/code> auto-generation now only runs for new posts or posts with an empty\/Auto Draft title \u2014 existing reviews with a manually set title are never overwritten.<\/li>\n<\/ul>\n\n<h4>2.1.8<\/h4>\n\n<ul>\n<li>UX: removed the native \"Add title\" input from the review edit screen \u2014 <code>post_title<\/code> is now auto-generated from \"Reviewer Name \u2013 Review Title\" and remains visible in the admin list table.<\/li>\n<\/ul>\n\n<h4>2.1.7<\/h4>\n\n<ul>\n<li>UX: admin \"Review Details\" meta box fields reordered to match the front-end form: Rating \u2192 Reviewer Name \u2192 Review Title \u2192 Review Text \u2192 Review Date.<\/li>\n<li>Fix: <code>maxlength=\"100\"<\/code> added to Reviewer Name input in admin meta box.<\/li>\n<li>Fix: <code>maxlength=\"1000\"<\/code> added to Review Text textarea in admin meta box.<\/li>\n<li>Security: server-side <code>mb_strlen<\/code> \/ <code>mb_substr<\/code> guards added for reviewer name (100 chars) and review text (1000 chars) in <code>save_meta()<\/code>.<\/li>\n<\/ul>\n\n<h4>2.1.6<\/h4>\n\n<ul>\n<li>Code: removed <code>load_plugin_textdomain()<\/code> call \u2014 WordPress 4.6+ loads translations automatically; the explicit call triggered a PluginCheck warning.<\/li>\n<li>Code: <code>$token<\/code> variable in <code>review-form.php<\/code> template renamed to <code>$elsre_token<\/code> to satisfy WordPress global variable prefix requirement.<\/li>\n<\/ul>\n\n<h4>2.1.5<\/h4>\n\n<ul>\n<li>Security: token now consumed <strong>before<\/strong> the review post is inserted \u2014 eliminates race window that allowed token reuse on server crash.<\/li>\n<li>Security: REST API read access restricted to users with <code>edit_posts<\/code> capability via <code>ELSRE_REST_Controller<\/code> \u2014 review content (including pending) is no longer publicly readable via the REST API.<\/li>\n<li>Security: token delete action changed from a GET link to a POST form, preventing accidental deletion by browser prefetch or link scanners.<\/li>\n<li>Security: <code>handle_admin_actions()<\/code> now checks <code>current_user_can()<\/code> before reading any request parameters.<\/li>\n<li>Fix: admin meta box now reads <code>post_content<\/code> first (authoritative) and falls back to <code>_elsre_review_text<\/code> meta, matching the documented architecture and preventing translated content from being overwritten on save.<\/li>\n<li>Fix: <code>load_plugin_textdomain()<\/code> added to <code>elsre_init()<\/code> so translations load correctly on self-hosted installs.<\/li>\n<li>Fix: <code>response.data<\/code> null guard added on JS success path to prevent TypeError when server returns <code>{success:true, data:null}<\/code>.<\/li>\n<li>Fix: success message receives focus after form submission so keyboard and screen-reader users are not left on the hidden form.<\/li>\n<li>Fix: <code>submit_button()<\/code> no longer receives a pre-escaped label \u2014 prevented double-escaping of special characters.<\/li>\n<li>Code: <code>ms_gpt_translatable_post_types<\/code> filter converted to a named function so third parties can call <code>remove_filter()<\/code>.<\/li>\n<li>Code: <code>uninstall.php<\/code> <code>DROP TABLE<\/code> uses <code>%i<\/code> identifier placeholder (WordPress 6.2+) instead of <code>esc_sql()<\/code> workaround.<\/li>\n<li>CSS: <code>prefers-reduced-motion<\/code> media query added \u2014 disables spinner animation and CSS transitions for users who prefer reduced motion.<\/li>\n<\/ul>\n\n<h4>2.1.4<\/h4>\n\n<ul>\n<li>Feature: live character counter on Name, Title, and Review fields \u2014 green while typing, orange within 5% of the limit, red at the limit. Counter is hidden until the user starts typing. Accessible via <code>aria-live<\/code> and <code>aria-describedby<\/code>.<\/li>\n<\/ul>\n\n<h4>2.1.3<\/h4>\n\n<ul>\n<li>Fix: <code>uninstall.php<\/code> \u2014 <code>DROP TABLE<\/code> query now uses <code>esc_sql()<\/code> inline instead of a <code>$table<\/code> variable, resolving PluginCheck <code>DirectDB.UnescapedDBParameter<\/code> warning.<\/li>\n<\/ul>\n\n<h4>2.1.2<\/h4>\n\n<ul>\n<li>Fix: <code>uninstall.php<\/code> \u2014 tokens table <code>DROP<\/code> query now guarded by a <code>preg_match()<\/code> pattern check on the table name, resolving PluginCheck <code>DirectDB.UnescapedDBParameter<\/code> warning.<\/li>\n<\/ul>\n\n<h4>2.1.1<\/h4>\n\n<ul>\n<li>Fix: assets and script data (<code>elsreData<\/code>) now enqueued during <code>wp_enqueue_scripts<\/code> instead of inside the shortcode render \u2014 prevents \"elsreData is not defined\" JS errors caused by caching and script-optimisation plugins (LiteSpeed, WP Rocket, Autoptimize, etc.).<\/li>\n<li>Fix: form hidden on successful submission instead of calling <code>form.reset()<\/code> \u2014 resolves permanently disabled submit button on Safari\/WebKit after token-based submissions.<\/li>\n<li>Fix: XHR handlers now cover all failure paths (onerror, ontimeout, onabort) with a 30-second timeout, so the submit button is always re-enabled even if the server does not respond.<\/li>\n<\/ul>\n\n<h4>2.1.0<\/h4>\n\n<ul>\n<li>Feature: review title field added to the submission form, review cards, and admin meta box.<\/li>\n<li>Change: review text maximum length reduced from 2000 to 1000 characters.<\/li>\n<li>Change: submission form field order is now Rating \u2192 Name \u2192 Title \u2192 Review.<\/li>\n<\/ul>\n\n<h4>2.0.1<\/h4>\n\n<ul>\n<li>Code: PHPCS\/WPCS zero errors, zero warnings. Alignment fixes auto-corrected by phpcbf. Rephrased doc comment to satisfy capitalisation rule. Removed unused <code>$blog_id<\/code> parameter from <code>elsre_uninstall_site()<\/code>. Added <code>PreparedSQL<\/code> ignore annotation for the dynamic-placeholder query in <code>get_form_pages()<\/code>.<\/li>\n<\/ul>\n\n<h4>2.0.0<\/h4>\n\n<ul>\n<li>Security: REST API meta fields now require <code>edit_posts<\/code> capability via <code>auth_callback<\/code> \u2014 previously any authenticated user could read review meta via the REST API.<\/li>\n<li>Security: <code>get_client_ip()<\/code> now defaults to <code>REMOTE_ADDR<\/code> only. Proxy header trust (<code>X-Forwarded-For<\/code> \/ <code>X-Real-IP<\/code>) requires explicit opt-in via the <code>elsre_trust_proxy_headers<\/code> filter to prevent rate-limit bypass on sites not behind a reverse proxy.<\/li>\n<li>Security: <code>inject_post_content_before_save()<\/code> now checks <code>current_user_can()<\/code> in addition to nonce verification before modifying <code>post_content<\/code>.<\/li>\n<li>Fix: double-escaping of translatable submit button label \u2014 <code>esc_attr__()<\/code> replaced with <code>__()<\/code> to prevent corrupted output for translations containing apostrophes (e.g. French).<\/li>\n<li>Fix: <code>uninstall.php<\/code> now loops over all sites on multisite network installs, cleaning up posts, transients, tokens table, and options for each site.<\/li>\n<li>Fix: token reuse now updates the stored <code>page_id<\/code> when the admin selects a different form page, keeping the token record consistent with the generated URL.<\/li>\n<li>Fix: <code>wp_create_nonce()<\/code> is now called only when a shortcode is actually rendered on a page, not on every front-end page load.<\/li>\n<li>Fix: copy-to-clipboard button now only shows \"Copied!\" when the clipboard write actually succeeded.<\/li>\n<li>Code: <code>load_card_template()<\/code> uses <code>extract()<\/code> with an explicit named array instead of relying on PHP variable scope inheritance across <code>include<\/code>.<\/li>\n<li>Code: <code>render_admin_columns()<\/code> now applies <code>esc_attr()<\/code> to inline star color values (WPCS compliance).<\/li>\n<li>New: <code>elsre_form_page_post_types<\/code> filter \u2014 allows custom post types to appear in the form page dropdown (useful for page-builder custom post types).<\/li>\n<li>New: <code>elsre_trust_proxy_headers<\/code> filter \u2014 opt-in to reading <code>X-Forwarded-For<\/code> \/ <code>X-Real-IP<\/code> on sites behind a trusted reverse proxy.<\/li>\n<\/ul>\n\n<h4>1.9.5<\/h4>\n\n<ul>\n<li>Fix: esc_sql() applied to table variable before interpolation in consume_token() query.<\/li>\n<\/ul>\n\n<h4>1.9.4<\/h4>\n\n<ul>\n<li>Fix: JS \u2014 response.data null-guard added before accessing response.data.html in infinite scroll callback.<\/li>\n<li>Fix: JS \u2014 IntersectionObserver callback now checks entries.length before accessing entries[0].<\/li>\n<li>Fix: JS \u2014 hardcoded 'Submit Review' fallback replaced with translatable elsreData.i18n.submit (added to wp_localize_script).<\/li>\n<li>Fix: Accessibility \u2014 individual star spans in review-card.php now have aria-hidden=\"true\"; the parent div already carries the full aria-label.<\/li>\n<li>Fix: readme.txt \u2014 stale version number 1.4.4 updated in plugin ASCII art example.<\/li>\n<\/ul>\n\n<h4>1.9.3<\/h4>\n\n<ul>\n<li>Code: PHPCS\/WPCS \u2014 zero errors, zero warnings. Fixed short ternary operator (replaced ?: with explicit if\/else), added missing docblock short description, fixed indentation in review-card.php template. Added extensions and exclude-pattern to phpcs.xml to scope checks to PHP only.<\/li>\n<\/ul>\n\n<h4>1.9.2<\/h4>\n\n<ul>\n<li>Fix: SQL table names now consistently wrapped in backticks across all queries in class-elsre-tokens.php and uninstall.php.<\/li>\n<li>Fix: strtotime() return value now validated before passing to wp_date() \u2014 prevents current date\/time showing for reviews with missing or malformed date meta.<\/li>\n<li>Fix: missing esc_attr() on CSS class output in review-card.php (WPCS compliance).<\/li>\n<li>Fix: replaced printf(esc_html__(...), '<code>') pattern with echo wp_kses_post(sprintf(__(...))) \u2014 correct approach for substituting HTML into translatable strings.<\/code><\/li>\n<li>Fix: added PHP 8.0 union type hint false|object on the $api parameter of elsre_plugins_api_info().<\/li>\n<\/ul>\n\n<h4>1.9.1<\/h4>\n\n<ul>\n<li>Fix: blank \"Add New Review\" screen in WordPress 6.9+ \u2014 <code>replace_editor<\/code> filter returning <code>true<\/code> caused WP 6.9 to skip the entire edit form, showing only the admin footer. Replaced with CSS-based editor suppression (<code>.post-type-elsre_review #postdivrich<\/code> etc.).<\/li>\n<li>Fix: added <code>use_block_editor_for_post<\/code> filter alongside <code>use_block_editor_for_post_type<\/code> for belt-and-suspenders block editor disabling.<\/li>\n<li>New: plugin details modal \u2014 \"View details\" link on the Plugins page showing description, shortcode docs, installation guide, and changelog.<\/li>\n<\/ul>\n\n<h4>1.9.0<\/h4>\n\n<ul>\n<li>Fix: editor suppression switched from <code>user_can_richedit<\/code> filter to <code>replace_editor<\/code> filter for WordPress 6.x compatibility.<\/li>\n<\/ul>\n\n<h4>1.8.3<\/h4>\n\n<ul>\n<li>Fix: removed <code>postcustom<\/code> meta box (Custom Fields) from review edit screen to keep the admin UI clean.<\/li>\n<\/ul>\n\n<h4>1.8.2<\/h4>\n\n<ul>\n<li>Fix: translation reverting to source \u2014 meta fallback in inject_post_content_before_save now only fires when post_content is genuinely empty, never when translated content is already present.<\/li>\n<li>Fix: TinyMCE no longer appears on review edit screen (user_can_richedit filter).<\/li>\n<\/ul>\n\n<h4>1.8.1<\/h4>\n\n<ul>\n<li>Fix: restored plain-textarea UI for review text (no TinyMCE, no image uploads). Native content editor hidden via remove_meta_box() while editor stays in CPT supports.<\/li>\n<li>Fix: removed sync_content_from_meta \u2014 its wp_update_post() fired a second save_post which triggered the translator twice, causing random translation results.<\/li>\n<li>inject_post_content_before_save is the single reliable sync point.<\/li>\n<\/ul>\n\n<h4>1.8.0<\/h4>\n\n<ul>\n<li>Fix: <code>editor<\/code> added to CPT supports \u2014 translation plugins check post_type_supports before including post_content in jobs. Without it, post_content was silently skipped. Review text now translates correctly.<\/li>\n<li>Block editor disabled for review posts; classic editor used. The content area holds the review text directly, like a standard post.<\/li>\n<li>Removed \"Review Text\" textarea from the structured meta box \u2014 it is now the native content field. Reviewer name, rating, and date remain in the meta box.<\/li>\n<li>Removed inject_post_content_before_save and sync_content_from_meta hooks \u2014 no longer needed with native editor support.<\/li>\n<li>Backward compat: card template still falls back to _elsre_review_text meta for reviews created before 1.8.0.<\/li>\n<\/ul>\n\n<h4>1.7.1<\/h4>\n\n<ul>\n<li>Security: token consumption is now atomic (UPDATE WHERE is_used = 0) \u2014 prevents race condition where two simultaneous requests with the same token could both succeed.<\/li>\n<li>Security: REST API requests excluded from post_content restore hooks \u2014 translation plugins updating via REST no longer risk source-language overwrite.<\/li>\n<li>Security: server-side max-length validation added for reviewer name (100 chars) and review text (2000 chars).<\/li>\n<li>Security: REMOTE_ADDR validated as IP before use in rate limiting.<\/li>\n<li>Fix: uninstall.php now cleans up admin-notice transients.<\/li>\n<\/ul>\n\n<h4>1.7.0<\/h4>\n\n<ul>\n<li>Requires PHP 8.0+ and WordPress 6.9+.<\/li>\n<li>PHP 8.0 type hints added to all methods and global functions.<\/li>\n<li><code>match<\/code> expressions replace if\/else chains in token validation, AJAX order parsing, and error messages.<\/li>\n<li><code>wp_date()<\/code> replaces <code>date_i18n()<\/code> throughout.<\/li>\n<li>Removed all <code>phpcs:ignore NonceVerification<\/code> suppressions: nonce verified directly in <code>inject_post_content_before_save()<\/code>; programmatic saves detected via <code>$_SERVER['REQUEST_METHOD']<\/code>; admin notices use per-user transients instead of unverified <code>$_GET<\/code> params.<\/li>\n<li>Template variables renamed with <code>elsre_<\/code> prefix \u2014 removes all <code>phpcs:ignore NonPrefixedVariableFound<\/code> suppressions.<\/li>\n<\/ul>\n\n<h4>1.6.3<\/h4>\n\n<ul>\n<li>Fix: Plugin URI updated from example.com to domclic.com.<\/li>\n<li>Fix: removed deprecated load_plugin_textdomain() call.<\/li>\n<li>Fix: esc_sql() applied to table name in get_all_tokens().<\/li>\n<li>Fix: phpcs ignore on loop variable $i and $submit_label in templates.<\/li>\n<\/ul>\n\n<h4>1.6.2<\/h4>\n\n<ul>\n<li>i18n: text domain changed from <code>elsre<\/code> to <code>effortless-simple-reviews-editor<\/code> across all PHP files to match WordPress.org convention.<\/li>\n<\/ul>\n\n<h4>1.6.1<\/h4>\n\n<ul>\n<li>Fix: infinite re-translation loop with multisite translation plugins \u2014 <code>inject_post_content_before_save<\/code> and <code>sync_content_from_meta<\/code> now skip programmatic saves (no <code>$_POST<\/code>) so translated <code>post_content<\/code> on destination sites is never overwritten with source-language meta.<\/li>\n<\/ul>\n\n<h4>1.6.0<\/h4>\n\n<ul>\n<li>Code: full PHPCS\/WPCS compliance \u2014 zero errors, zero warnings.<\/li>\n<li>Fix: Yoda conditions in <code>validate_token()<\/code> and token list rendering.<\/li>\n<li>Fix: renamed <code>$posts<\/code>\/<code>$post_id<\/code> in <code>uninstall.php<\/code> to avoid overriding WordPress globals.<\/li>\n<li>Fix: suppressed false-positive nonce warnings on read-only redirect params.<\/li>\n<li>Fix: alignment and whitespace issues auto-corrected by phpcbf.<\/li>\n<\/ul>\n\n<h4>1.5.9<\/h4>\n\n<ul>\n<li>Fix: <code>$id_array<\/code> undefined variable notice when <code>[elsre_reviews]<\/code> is used without the <code>ids<\/code> attribute.<\/li>\n<li>Fix: replaced <code>gmdate()<\/code>+<code>strtotime()<\/code> with <code>date_i18n()<\/code> \u2014 dates now respect WordPress timezone and locale.<\/li>\n<li>Fix: rate limiting skips gracefully when client IP cannot be determined instead of hashing an empty string.<\/li>\n<li>Fix: AJAX <code>per_page<\/code> capped at 100; <code>page<\/code> capped at 1000 to prevent DB abuse.<\/li>\n<li>Fix: deprecated <code>bigint(20)<\/code> syntax replaced with <code>BIGINT UNSIGNED<\/code> in token table (DB version 1.2).<\/li>\n<li>Fix: front-end form now has <code>method=\"post\"<\/code> for valid HTML.<\/li>\n<\/ul>\n\n<h4>1.5.8<\/h4>\n\n<ul>\n<li>Compatibility: hooks <code>ms_gpt_translatable_post_types<\/code> filter so EffortLess Multisite Auto Translate picks up the non-public <code>elsre_review<\/code> CPT and syncs reviews across sites.<\/li>\n<\/ul>\n\n<h4>1.5.7<\/h4>\n\n<ul>\n<li>Fix: PHP Notice \"post type elsre_review is not registered\" on REST API capability checks \u2014 <code>register_meta_fields()<\/code> now hooks to <code>init<\/code> at priority 11, after <code>register_post_type()<\/code> at priority 10.<\/li>\n<\/ul>\n\n<h4>1.5.6<\/h4>\n\n<ul>\n<li>REST API support enabled on the <code>elsre_review<\/code> post type.<\/li>\n<li>All custom meta fields registered with <code>show_in_rest: true<\/code> for REST-based translation plugins.<\/li>\n<li>Added <code>custom-fields<\/code> to CPT supports.<\/li>\n<\/ul>\n\n<h4>1.5.5<\/h4>\n\n<ul>\n<li>Fix: multisite translation plugins were receiving empty <code>post_content<\/code> because our sync hook ran after the translation plugin's <code>save_post<\/code> hook.<\/li>\n<li>New <code>wp_insert_post_data<\/code> filter injects review text into <code>post_content<\/code> before the database write, so translation plugins always sync the correct content.<\/li>\n<\/ul>\n\n<h4>1.5.4<\/h4>\n\n<ul>\n<li>Fix: <code>$tokens_handler<\/code> variable scope bug in <code>submit_review()<\/code> \u2014 was potentially undefined at token consumption.<\/li>\n<li>Fix: <code>wp_insert_post()<\/code> zero-return now treated as failure (not just <code>WP_Error<\/code>).<\/li>\n<li>Fix: removed <code>$wpdb-&gt;prepare()<\/code> with no placeholders in <code>get_all_tokens()<\/code>.<\/li>\n<li>Security: CSS size attributes on shortcodes now validated against a unit whitelist.<\/li>\n<\/ul>\n\n<h4>1.5.3<\/h4>\n\n<ul>\n<li>Fix: infinite recursion \u2014 removed <code>wp_update_post()<\/code> from <code>save_meta()<\/code>.<\/li>\n<li>Fix: WordPress was clearing <code>post_content<\/code> on every admin save (CPT without editor). <code>sync_content_from_meta()<\/code> now correctly restores it.<\/li>\n<li>Replaced remove\/re-add hook pattern with a static <code>$syncing<\/code> flag.<\/li>\n<\/ul>\n\n<h4>1.5.2<\/h4>\n\n<ul>\n<li>Fix: quick edit and bulk edit now correctly sync <code>post_content<\/code> from <code>_elsre_review_text<\/code> meta.<\/li>\n<li>Infinite-loop guard on the sync hook.<\/li>\n<\/ul>\n\n<h4>1.5.1<\/h4>\n\n<ul>\n<li>Translation compatibility: review text now stored in <code>post_content<\/code> so multisite translators and translation plugins handle it natively.<\/li>\n<li>Card display reads from <code>post_content<\/code> first, falls back to meta for reviews created before this version.<\/li>\n<li>Admin meta box keeps <code>post_content<\/code> in sync when review text is edited.<\/li>\n<\/ul>\n\n<h4>1.5.0<\/h4>\n\n<ul>\n<li>New <code>order<\/code> attribute on <code>[elsre_reviews]<\/code>: <code>order=\"desc\"<\/code> (newest first, default) or <code>order=\"asc\"<\/code> (oldest first).<\/li>\n<li>Sort order preserved across infinite scroll loads.<\/li>\n<\/ul>\n\n<h4>1.4.4<\/h4>\n\n<ul>\n<li>Security: replaced MD5 with SHA-256 for rate-limit IP hashing.<\/li>\n<li>Security: proxy-aware IP detection \u2014 reads <code>X-Forwarded-For<\/code> \/ <code>X-Real-IP<\/code> headers with validation before falling back to <code>REMOTE_ADDR<\/code>.<\/li>\n<li>Security: moved inline <code>onclick<\/code> confirm on delete links to <code>addEventListener<\/code> in JS.<\/li>\n<li>Security: nonce verified before processing delete action; added <code>token_id<\/code> guard.<\/li>\n<li>Security: validate generated page ID exists before issuing a token.<\/li>\n<li>Code: <code>get_all_tokens()<\/code> SQL query now uses <code>$wpdb-&gt;prepare()<\/code>.<\/li>\n<li>Code: date validation uses <code>DateTime::createFromFormat()<\/code> for strict round-trip check.<\/li>\n<li>Code: <code>ELSRE_Shortcodes::load_card_template()<\/code> is now static \u2014 no extra class instantiation in AJAX handler.<\/li>\n<\/ul>\n\n<h4>1.4.3<\/h4>\n\n<ul>\n<li>Plugin renamed to EffortLess Simple Reviews Editor to match the <code>ELSRE_<\/code> prefix convention.<\/li>\n<\/ul>\n\n<h4>1.4.2<\/h4>\n\n<ul>\n<li>i18n ready: <code>load_plugin_textdomain()<\/code> and <code>.pot<\/code> template file for translations.<\/li>\n<\/ul>\n\n<h4>1.4.1<\/h4>\n\n<ul>\n<li>Review title now displayed in the card (between stars and review text).<\/li>\n<\/ul>\n\n<h4>1.4.0<\/h4>\n\n<ul>\n<li>Customizable sizes: <code>star_size<\/code>, <code>text_size<\/code>, <code>name_size<\/code> attributes on <code>[elsre_reviews]<\/code>.<\/li>\n<li>Customizable star size on <code>[elsre_form]<\/code> via <code>star_size<\/code> attribute.<\/li>\n<li>Sizes powered by CSS custom properties for easy theming.<\/li>\n<\/ul>\n\n<h4>1.3.0<\/h4>\n\n<ul>\n<li>Client email is now the unique identifier for review links.<\/li>\n<li>If an unused link exists for the same email, it is reused (no duplicates).<\/li>\n<li>Email column added to the tokens table and admin Review Links page.<\/li>\n<li><code>elsre_create_review_link()<\/code> first parameter is now <code>$email<\/code>.<\/li>\n<\/ul>\n\n<h4>1.2.0<\/h4>\n\n<ul>\n<li>New <code>elsre_create_review_link()<\/code> PHP function for programmatic link generation.<\/li>\n<li>Use it from your own plugin to automatically include a review link in emails.<\/li>\n<\/ul>\n\n<h4>1.1.0<\/h4>\n\n<ul>\n<li>One-time-use review links: generate unique links for clients via Reviews \u2192 Review Links.<\/li>\n<li>New <code>require_token<\/code> attribute on <code>[elsre_form]<\/code> shortcode.<\/li>\n<li>Admin page to generate, copy, and manage review links.<\/li>\n<li>Token consumed after submission \u2014 link can only be used once.<\/li>\n<\/ul>\n\n<h4>1.0.0<\/h4>\n\n<ul>\n<li>Initial release.<\/li>\n<li>Custom post type for reviews with admin meta boxes.<\/li>\n<li><code>[elsre_reviews]<\/code> shortcode with infinite scroll and responsive grid.<\/li>\n<li><code>[elsre_form]<\/code> shortcode with AJAX submission, honeypot, and rate limiting.<\/li>\n<\/ul>","raw_excerpt":"A simple reviews system with front-end submission, admin approval, and responsive card grid display with infinite scroll.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/316658","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=316658"}],"author":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/domclic"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=316658"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=316658"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=316658"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=316658"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=316658"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=316658"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}