22/**
33 * Plugin Name: Next.js Revalidation
44 * Description: Revalidates Next.js site when WordPress content changes
5- * Version: 1.0.3
5+ * Version: 1.0.5
66 * Author: 9d8
77 * Author URI: https://9d8.dev
88 */
1515class Next_Revalidation {
1616 private $ options ;
1717 private $ option_name = 'next_revalidation_settings ' ;
18+ private $ last_revalidation = 0 ;
19+ private $ revalidation_cooldown = 2 ; // seconds between revalidations
20+ private $ history_option_name = 'next_revalidation_history ' ;
21+ private $ max_history_items = 50 ;
1822
1923 public function __construct () {
2024 // Initialize plugin
@@ -31,9 +35,12 @@ public function __construct() {
3135 // Post save/update hooks - any post status change
3236 add_action ('transition_post_status ' , array ($ this , 'on_post_status_change ' ), 10 , 3 );
3337 add_action ('save_post ' , array ($ this , 'on_content_change ' ), 10 , 3 );
34- add_action ('wp_trash_post ' , array ($ this , 'on_post_trash ' ), 10 );
35- add_action ('untrash_post ' , array ($ this , 'on_post_untrash ' ), 10 );
36- add_action ('before_delete_post ' , array ($ this , 'on_post_delete ' ), 10 );
38+
39+ // Trash and delete hooks with higher priority to ensure they run
40+ add_action ('trashed_post ' , array ($ this , 'on_post_trash ' ), 5 );
41+ add_action ('untrashed_post ' , array ($ this , 'on_post_untrash ' ), 5 );
42+ add_action ('delete_post ' , array ($ this , 'on_post_delete ' ), 5 );
43+ add_action ('after_delete_post ' , array ($ this , 'on_post_delete ' ), 5 );
3744
3845 // Term changes
3946 add_action ('created_term ' , array ($ this , 'on_term_change ' ), 10 , 3 );
@@ -49,6 +56,11 @@ public function __construct() {
4956 add_action ('add_attachment ' , array ($ this , 'on_media_change ' ), 10 );
5057 add_action ('edit_attachment ' , array ($ this , 'on_media_change ' ), 10 );
5158 add_action ('delete_attachment ' , array ($ this , 'on_media_change ' ), 10 );
59+
60+ // Menu changes
61+ add_action ('wp_update_nav_menu ' , array ($ this , 'on_menu_change ' ), 10 );
62+ add_action ('wp_create_nav_menu ' , array ($ this , 'on_menu_change ' ), 10 );
63+ add_action ('wp_delete_nav_menu ' , array ($ this , 'on_menu_change ' ), 10 );
5264 }
5365
5466 public function init () {
@@ -92,6 +104,14 @@ public function register_settings() {
92104 'next-revalidation-settings ' ,
93105 'next_revalidation_section '
94106 );
107+
108+ add_settings_field (
109+ 'revalidation_cooldown ' ,
110+ 'Revalidation Cooldown ' ,
111+ array ($ this , 'revalidation_cooldown_callback ' ),
112+ 'next-revalidation-settings ' ,
113+ 'next_revalidation_section '
114+ );
95115 }
96116
97117 public function sanitize_settings ($ input ) {
@@ -111,6 +131,11 @@ public function sanitize_settings($input) {
111131 $ new_input ['enable_notifications ' ] = (bool )$ input ['enable_notifications ' ];
112132 }
113133
134+ if (isset ($ input ['revalidation_cooldown ' ])) {
135+ $ cooldown = intval ($ input ['revalidation_cooldown ' ]);
136+ $ new_input ['revalidation_cooldown ' ] = max (0 , min (60 , $ cooldown )); // Between 0 and 60 seconds
137+ }
138+
114139 return $ new_input ;
115140 }
116141
@@ -135,14 +160,40 @@ public function enable_notifications_callback() {
135160 echo '<input type="checkbox" id="enable_notifications" name=" ' . $ this ->option_name . '[enable_notifications]" ' . checked ($ value , true , false ) . ' /> ' ;
136161 echo '<label for="enable_notifications">Show admin notifications for revalidation events</label> ' ;
137162 }
163+
164+ public function revalidation_cooldown_callback () {
165+ $ value = isset ($ this ->options ['revalidation_cooldown ' ]) ? intval ($ this ->options ['revalidation_cooldown ' ]) : 2 ;
166+ echo '<input type="number" min="0" max="60" id="revalidation_cooldown" name=" ' . $ this ->option_name . '[revalidation_cooldown]" value=" ' . $ value . '" class="small-text" /> ' ;
167+ echo '<p class="description">Minimum seconds between revalidation requests (0-60). Use higher values for busy sites.</p> ' ;
168+ }
138169
139170 public function add_admin_menu () {
140- add_options_page (
141- 'Next.js Revalidation ' ,
171+ add_menu_page (
142172 'Next.js Revalidation ' ,
173+ 'Next.js ' ,
143174 'manage_options ' ,
144175 'next-revalidation-settings ' ,
145- array ($ this , 'admin_page_content ' )
176+ array ($ this , 'admin_page_content ' ),
177+ 'dashicons-update ' , // WordPress dashicon
178+ 100 // Position in menu
179+ );
180+
181+ // Add submenu pages
182+ add_submenu_page (
183+ 'next-revalidation-settings ' ,
184+ 'Settings ' ,
185+ 'Settings ' ,
186+ 'manage_options ' ,
187+ 'next-revalidation-settings '
188+ );
189+
190+ add_submenu_page (
191+ 'next-revalidation-settings ' ,
192+ 'Revalidation History ' ,
193+ 'History ' ,
194+ 'manage_options ' ,
195+ 'next-revalidation-history ' ,
196+ array ($ this , 'history_page_content ' )
146197 );
147198 }
148199
@@ -194,6 +245,82 @@ public function admin_page_content() {
194245 <?php
195246 }
196247
248+ public function history_page_content () {
249+ // Get revalidation history
250+ $ history = get_option ($ this ->history_option_name , array ());
251+
252+ ?>
253+ <div class="wrap">
254+ <h1><?php echo esc_html (get_admin_page_title ()); ?> </h1>
255+
256+ <div class="tablenav top">
257+ <div class="alignleft actions">
258+ <form method="post" action="">
259+ <?php wp_nonce_field ('clear_revalidation_history ' , 'clear_history_nonce ' ); ?>
260+ <input type="hidden" name="action" value="clear_history">
261+ <input type="submit" class="button" value="Clear History" onclick="return confirm('Are you sure you want to clear the revalidation history?');">
262+ </form>
263+ </div>
264+ <br class="clear">
265+ </div>
266+
267+ <table class="wp-list-table widefat fixed striped">
268+ <thead>
269+ <tr>
270+ <th>Time</th>
271+ <th>Content Type</th>
272+ <th>Content ID</th>
273+ <th>Status</th>
274+ <th>Response</th>
275+ </tr>
276+ </thead>
277+ <tbody>
278+ <?php if (empty ($ history )): ?>
279+ <tr>
280+ <td colspan="5">No revalidation history found.</td>
281+ </tr>
282+ <?php else : ?>
283+ <?php foreach ($ history as $ entry ): ?>
284+ <tr>
285+ <td><?php echo esc_html (date ('Y-m-d H:i:s ' , $ entry ['time ' ])); ?> </td>
286+ <td><?php echo esc_html ($ entry ['content_type ' ]); ?> </td>
287+ <td><?php echo isset ($ entry ['content_id ' ]) ? esc_html ($ entry ['content_id ' ]) : 'N/A ' ; ?> </td>
288+ <td>
289+ <?php if ($ entry ['success ' ]): ?>
290+ <span style="color: green;">Success</span>
291+ <?php else : ?>
292+ <span style="color: red;">Failed</span>
293+ <?php endif ; ?>
294+ </td>
295+ <td>
296+ <?php
297+ if (isset ($ entry ['response ' ])) {
298+ echo esc_html (substr ($ entry ['response ' ], 0 , 50 ));
299+ if (strlen ($ entry ['response ' ]) > 50 ) {
300+ echo '... ' ;
301+ }
302+ } else {
303+ echo 'N/A ' ;
304+ }
305+ ?>
306+ </td>
307+ </tr>
308+ <?php endforeach ; ?>
309+ <?php endif ; ?>
310+ </tbody>
311+ </table>
312+ </div>
313+ <?php
314+
315+ // Handle clear history action
316+ if (isset ($ _POST ['action ' ]) && $ _POST ['action ' ] === 'clear_history ' ) {
317+ if (check_admin_referer ('clear_revalidation_history ' , 'clear_history_nonce ' )) {
318+ delete_option ($ this ->history_option_name );
319+ echo '<script>window.location.reload();</script> ' ;
320+ }
321+ }
322+ }
323+
197324 // AJAX action for manual revalidation
198325 public function register_ajax_actions () {
199326 add_action ('wp_ajax_manual_revalidation ' , array ($ this , 'handle_manual_revalidation ' ));
@@ -225,6 +352,7 @@ public function on_post_status_change($new_status, $old_status, $post) {
225352
226353 // If the status is changing, we should revalidate
227354 if ($ new_status !== $ old_status ) {
355+ error_log ("Next.js Revalidation: Post status changed from {$ old_status } to {$ new_status } for post {$ post ->ID }" );
228356 $ this ->send_revalidation_request ($ post ->post_type , $ post ->ID );
229357 }
230358 }
@@ -240,22 +368,28 @@ public function on_content_change($post_id, $post = null, $update = null) {
240368 $ post = get_post ($ post_id );
241369 }
242370
243- // Revalidate regardless of post status
371+ error_log ( " Next.js Revalidation: Content changed for post { $ post_id }" );
244372 $ this ->send_revalidation_request ($ post ->post_type , $ post_id );
245373 }
246374
247375 public function on_post_trash ($ post_id ) {
248376 $ post_type = get_post_type ($ post_id );
377+ error_log ("Next.js Revalidation: Post trashed {$ post_id } of type {$ post_type }" );
249378 $ this ->send_revalidation_request ($ post_type , $ post_id );
250379 }
251380
252381 public function on_post_untrash ($ post_id ) {
253382 $ post_type = get_post_type ($ post_id );
383+ error_log ("Next.js Revalidation: Post untrashed {$ post_id } of type {$ post_type }" );
254384 $ this ->send_revalidation_request ($ post_type , $ post_id );
255385 }
256386
257387 public function on_post_delete ($ post_id ) {
258388 $ post_type = get_post_type ($ post_id );
389+ if (!$ post_type ) {
390+ $ post_type = 'unknown ' ; // Fallback if post type can't be determined
391+ }
392+ error_log ("Next.js Revalidation: Post deleted {$ post_id } of type {$ post_type }" );
259393 $ this ->send_revalidation_request ($ post_type , $ post_id );
260394 }
261395
@@ -268,25 +402,67 @@ public function on_term_change($term_id, $tt_id, $taxonomy) {
268402 $ content_type = 'tag ' ;
269403 }
270404
405+ error_log ("Next.js Revalidation: Term changed {$ term_id } of type {$ content_type }" );
271406 $ this ->send_revalidation_request ($ content_type , $ term_id );
272407 }
273408
274409 public function on_user_change ($ user_id ) {
410+ error_log ("Next.js Revalidation: User changed {$ user_id }" );
275411 $ this ->send_revalidation_request ('author ' , $ user_id );
276412 }
277413
278414 public function on_media_change ($ attachment_id ) {
415+ error_log ("Next.js Revalidation: Media changed {$ attachment_id }" );
279416 $ this ->send_revalidation_request ('media ' , $ attachment_id );
280417 }
418+
419+ public function on_menu_change ($ menu_id ) {
420+ error_log ("Next.js Revalidation: Menu changed {$ menu_id }" );
421+ $ this ->send_revalidation_request ('menu ' , $ menu_id );
422+ }
423+
424+ private function log_revalidation ($ content_type , $ content_id , $ success , $ response = '' ) {
425+ $ history = get_option ($ this ->history_option_name , array ());
426+
427+ // Add new entry at the beginning
428+ array_unshift ($ history , array (
429+ 'time ' => time (),
430+ 'content_type ' => $ content_type ,
431+ 'content_id ' => $ content_id ,
432+ 'success ' => $ success ,
433+ 'response ' => $ response
434+ ));
435+
436+ // Keep only the last X entries
437+ if (count ($ history ) > $ this ->max_history_items ) {
438+ $ history = array_slice ($ history , 0 , $ this ->max_history_items );
439+ }
440+
441+ update_option ($ this ->history_option_name , $ history );
442+ }
281443
282444 private function send_revalidation_request ($ content_type , $ content_id = null ) {
445+ // Get cooldown from settings if available
446+ $ cooldown = isset ($ this ->options ['revalidation_cooldown ' ]) ? intval ($ this ->options ['revalidation_cooldown ' ]) : $ this ->revalidation_cooldown ;
447+
448+ // Implement throttling to prevent too many requests
449+ $ current_time = time ();
450+ if ($ current_time - $ this ->last_revalidation < $ cooldown ) {
451+ error_log ("Next.js Revalidation: Throttled request for {$ content_type } {$ content_id }" );
452+ $ this ->log_revalidation ($ content_type , $ content_id , false , 'Throttled: Too many requests ' );
453+ return false ;
454+ }
455+ $ this ->last_revalidation = $ current_time ;
456+
283457 // Check if we have the required settings
284458 if (empty ($ this ->options ['next_url ' ]) || empty ($ this ->options ['webhook_secret ' ])) {
285459 if (!empty ($ this ->options ['enable_notifications ' ])) {
286460 add_action ('admin_notices ' , function () {
287461 echo '<div class="notice notice-error is-dismissible"><p>Next.js revalidation failed: Missing URL or webhook secret. Please configure the plugin settings.</p></div> ' ;
288462 });
289463 }
464+ error_log ("Next.js Revalidation: Missing URL or webhook secret " );
465+ $ this ->log_revalidation ($ content_type , $ content_id , false , 'Missing URL or webhook secret ' );
290466 return false ;
291467 }
292468
@@ -302,6 +478,8 @@ private function send_revalidation_request($content_type, $content_id = null) {
302478 $ payload ['contentId ' ] = $ content_id ;
303479 }
304480
481+ error_log ("Next.js Revalidation: Sending request to {$ endpoint } for {$ content_type } {$ content_id }" );
482+
305483 // Send revalidation request
306484 $ response = wp_remote_post ($ endpoint , array (
307485 'method ' => 'POST ' ,
@@ -318,33 +496,38 @@ private function send_revalidation_request($content_type, $content_id = null) {
318496
319497 // Check for success
320498 if (is_wp_error ($ response )) {
321- error_log ('Next.js revalidation error: ' . $ response ->get_error_message ());
499+ $ error_message = $ response ->get_error_message ();
500+ error_log ('Next.js revalidation error: ' . $ error_message );
322501 if (!empty ($ this ->options ['enable_notifications ' ])) {
323502 add_action ('admin_notices ' , function () use ($ response ) {
324503 echo '<div class="notice notice-error is-dismissible"><p>Next.js revalidation failed: ' . esc_html ($ response ->get_error_message ()) . '</p></div> ' ;
325504 });
326505 }
506+ $ this ->log_revalidation ($ content_type , $ content_id , false , $ error_message );
327507 return false ;
328508 }
329509
330510 $ status_code = wp_remote_retrieve_response_code ($ response );
511+ $ body = wp_remote_retrieve_body ($ response );
331512 $ success = $ status_code >= 200 && $ status_code < 300 ;
332513
333514 if ($ success ) {
515+ error_log ("Next.js Revalidation: Success for {$ content_type } {$ content_id }" );
334516 if (!empty ($ this ->options ['enable_notifications ' ])) {
335517 add_action ('admin_notices ' , function () use ($ content_type , $ content_id ) {
336518 echo '<div class="notice notice-success is-dismissible"><p>Next.js site revalidated successfully due to ' . esc_html ($ content_type ) . ' update ' . ($ content_id ? ' (ID: ' . esc_html ($ content_id ) . ') ' : '' ) . '</p></div> ' ;
337519 });
338520 }
521+ $ this ->log_revalidation ($ content_type , $ content_id , true , $ body );
339522 return true ;
340523 } else {
341- $ body = wp_remote_retrieve_body ($ response );
342- error_log ('Next.js revalidation failed: ' . $ body );
524+ error_log ("Next.js revalidation failed with status {$ status_code }: {$ body }" );
343525 if (!empty ($ this ->options ['enable_notifications ' ])) {
344526 add_action ('admin_notices ' , function () use ($ status_code , $ body ) {
345527 echo '<div class="notice notice-error is-dismissible"><p>Next.js revalidation failed with status ' . esc_html ($ status_code ) . ': ' . esc_html ($ body ) . '</p></div> ' ;
346528 });
347529 }
530+ $ this ->log_revalidation ($ content_type , $ content_id , false , "Status {$ status_code }: {$ body }" );
348531 return false ;
349532 }
350533 }
0 commit comments