33 * Plugin Name: Next.js Revalidation
44 * Plugin URI: https://github.com/9d8dev/next-wp
55 * Description: Automatically revalidate Next.js cache when WordPress content changes
6- * Version: 1.0 .0
6+ * Version: 1.1 .0
77 * Author: 9d8
88 * Author URI: https://9d8.dev
99 * License: MIT
1515
1616class NextRevalidate {
1717 private $ option_name = 'next_revalidate_settings ' ;
18- private $ last_revalidation = 'next_revalidate_last ' ;
18+ private $ log_option = 'next_revalidate_log ' ;
19+ private $ last_option = 'next_revalidate_last ' ;
1920
2021 public function __construct () {
2122 add_action ('admin_menu ' , [$ this , 'add_admin_menu ' ]);
@@ -54,36 +55,20 @@ public function register_settings() {
5455 'next-revalidate '
5556 );
5657
57- add_settings_field (
58- 'nextjs_url ' ,
59- 'Next.js Site URL ' ,
60- [$ this , 'field_nextjs_url ' ],
61- 'next-revalidate ' ,
62- 'next_revalidate_main '
63- );
64-
65- add_settings_field (
66- 'webhook_secret ' ,
67- 'Webhook Secret ' ,
68- [$ this , 'field_webhook_secret ' ],
69- 'next-revalidate ' ,
70- 'next_revalidate_main '
71- );
72-
73- add_settings_field (
74- 'cooldown ' ,
75- 'Cooldown (seconds) ' ,
76- [$ this , 'field_cooldown ' ],
77- 'next-revalidate ' ,
78- 'next_revalidate_main '
79- );
58+ add_settings_field ('nextjs_url ' , 'Next.js Site URL ' , [$ this , 'field_nextjs_url ' ], 'next-revalidate ' , 'next_revalidate_main ' );
59+ add_settings_field ('webhook_secret ' , 'Webhook Secret ' , [$ this , 'field_webhook_secret ' ], 'next-revalidate ' , 'next_revalidate_main ' );
60+ add_settings_field ('cooldown ' , 'Cooldown (seconds) ' , [$ this , 'field_cooldown ' ], 'next-revalidate ' , 'next_revalidate_main ' );
61+ add_settings_field ('max_retries ' , 'Max Retries ' , [$ this , 'field_max_retries ' ], 'next-revalidate ' , 'next_revalidate_main ' );
62+ add_settings_field ('debug_mode ' , 'Debug Mode ' , [$ this , 'field_debug_mode ' ], 'next-revalidate ' , 'next_revalidate_main ' );
8063 }
8164
8265 public function sanitize_settings ($ input ) {
8366 $ sanitized = [];
8467 $ sanitized ['nextjs_url ' ] = esc_url_raw (rtrim ($ input ['nextjs_url ' ] ?? '' , '/ ' ));
8568 $ sanitized ['webhook_secret ' ] = sanitize_text_field ($ input ['webhook_secret ' ] ?? '' );
8669 $ sanitized ['cooldown ' ] = absint ($ input ['cooldown ' ] ?? 2 );
70+ $ sanitized ['max_retries ' ] = min (10 , max (0 , absint ($ input ['max_retries ' ] ?? 3 )));
71+ $ sanitized ['debug_mode ' ] = !empty ($ input ['debug_mode ' ]);
8772 return $ sanitized ;
8873 }
8974
@@ -104,25 +89,49 @@ public function field_webhook_secret() {
10489 public function field_cooldown () {
10590 $ options = get_option ($ this ->option_name );
10691 $ value = $ options ['cooldown ' ] ?? 2 ;
107- echo '<input type="number" name=" ' . $ this ->option_name . '[cooldown]" value=" ' . esc_attr ($ value ) . '" min="0" max="60" class="small-text" /> ' ;
108- echo '<p class="description">Minimum seconds between revalidation requests (prevents spam)</p> ' ;
92+ echo '<input type="number" name=" ' . $ this ->option_name . '[cooldown]" value=" ' . esc_attr ($ value ) . '" min="0" max="60" class="small-text" /> seconds ' ;
93+ echo '<p class="description">Minimum time between revalidation requests (prevents spam)</p> ' ;
94+ }
95+
96+ public function field_max_retries () {
97+ $ options = get_option ($ this ->option_name );
98+ $ value = $ options ['max_retries ' ] ?? 3 ;
99+ echo '<input type="number" name=" ' . $ this ->option_name . '[max_retries]" value=" ' . esc_attr ($ value ) . '" min="0" max="10" class="small-text" /> ' ;
100+ echo '<p class="description">Number of retry attempts for failed requests (with exponential backoff)</p> ' ;
101+ }
102+
103+ public function field_debug_mode () {
104+ $ options = get_option ($ this ->option_name );
105+ $ checked = !empty ($ options ['debug_mode ' ]) ? 'checked ' : '' ;
106+ echo '<label><input type="checkbox" name=" ' . $ this ->option_name . '[debug_mode]" value="1" ' . $ checked . ' /> Enable debug logging</label> ' ;
107+ echo '<p class="description">Logs detailed request/response info to PHP error log</p> ' ;
109108 }
110109
111110 public function settings_page () {
112111 if (!current_user_can ('manage_options ' )) {
113112 return ;
114113 }
115114
116- $ last = get_option ($ this ->last_revalidation );
115+ $ last = get_option ($ this ->last_option );
116+ $ log = get_option ($ this ->log_option , []);
117117 ?>
118118 <div class="wrap">
119119 <h1>Next.js Revalidation Settings</h1>
120120
121121 <?php if ($ last ): ?>
122- <div class="notice notice-info">
123- <p>Last revalidation: <?php echo esc_html (date ('Y-m-d H:i:s ' , $ last ['time ' ])); ?>
124- - Type: <?php echo esc_html ($ last ['type ' ]); ?>
125- - Status: <?php echo $ last ['success ' ] ? 'Success ' : 'Failed ' ; ?> </p>
122+ <div class="notice notice-<?php echo $ last ['success ' ] ? 'success ' : 'error ' ; ?> ">
123+ <p>
124+ <strong>Last revalidation:</strong>
125+ <?php echo esc_html (date ('Y-m-d H:i:s ' , $ last ['time ' ])); ?> —
126+ Type: <?php echo esc_html ($ last ['type ' ]); ?> —
127+ Status: <?php echo $ last ['success ' ] ? '✓ Success ' : '✗ Failed ' ; ?>
128+ <?php if (!empty ($ last ['error ' ])): ?>
129+ — Error: <?php echo esc_html ($ last ['error ' ]); ?>
130+ <?php endif ; ?>
131+ <?php if (!empty ($ last ['http_code ' ])): ?>
132+ — HTTP <?php echo esc_html ($ last ['http_code ' ]); ?>
133+ <?php endif ; ?>
134+ </p>
126135 </div>
127136 <?php endif ; ?>
128137
@@ -135,16 +144,62 @@ public function settings_page() {
135144 </form>
136145
137146 <hr>
138- <h2>Test Revalidation</h2>
147+ <h2>Test Connection</h2>
148+ <p>
149+ <button type="button" class="button button-secondary" onclick="testRevalidation()">Send Test Request</button>
150+ <span id="test-result" style="margin-left: 10px;"></span>
151+ </p>
152+
153+ <hr>
154+ <h2>Recent Activity</h2>
155+ <?php if (!empty ($ log )): ?>
139156 <p>
140- <button type="button" class="button" onclick="testRevalidation ()">Send Test Request </button>
141- <span id="test -result"></span>
157+ <button type="button" class="button button-secondary " onclick="clearLog ()">Clear Log </button>
158+ <span id="clear -result" style="margin-left: 10px; "></span>
142159 </p>
160+ <table class="widefat striped" style="margin-top: 10px;">
161+ <thead>
162+ <tr>
163+ <th>Time</th>
164+ <th>Type</th>
165+ <th>Action</th>
166+ <th>Status</th>
167+ <th>HTTP</th>
168+ <th>Retries</th>
169+ <th>Details</th>
170+ </tr>
171+ </thead>
172+ <tbody>
173+ <?php foreach (array_slice ($ log , 0 , 50 ) as $ entry ): ?>
174+ <tr>
175+ <td><?php echo esc_html (date ('Y-m-d H:i:s ' , $ entry ['time ' ])); ?> </td>
176+ <td><?php echo esc_html ($ entry ['type ' ]); ?> </td>
177+ <td><?php echo esc_html ($ entry ['data ' ]['action ' ] ?? '- ' ); ?> </td>
178+ <td><?php echo $ entry ['success ' ] ? '<span style="color:green;">✓</span> ' : '<span style="color:red;">✗</span> ' ; ?> </td>
179+ <td><?php echo esc_html ($ entry ['http_code ' ] ?? '- ' ); ?> </td>
180+ <td><?php echo esc_html ($ entry ['retries ' ] ?? 0 ); ?> </td>
181+ <td>
182+ <?php if (!empty ($ entry ['error ' ])): ?>
183+ <span style="color:red;"><?php echo esc_html ($ entry ['error ' ]); ?> </span>
184+ <?php elseif (!empty ($ entry ['data ' ]['slug ' ])): ?>
185+ <?php echo esc_html ($ entry ['data ' ]['slug ' ]); ?>
186+ <?php else : ?>
187+ -
188+ <?php endif ; ?>
189+ </td>
190+ </tr>
191+ <?php endforeach ; ?>
192+ </tbody>
193+ </table>
194+ <?php else : ?>
195+ <p>No revalidation requests yet.</p>
196+ <?php endif ; ?>
143197
144198 <script>
145199 function testRevalidation() {
146200 const result = document.getElementById('test-result');
147201 result.textContent = 'Sending...';
202+ result.style.color = '';
148203
149204 fetch(ajaxurl, {
150205 method: 'POST',
@@ -153,10 +208,38 @@ function testRevalidation() {
153208 })
154209 .then(r => r.json())
155210 .then(data => {
156- result.textContent = data.success ? 'Success!' : 'Failed: ' + data.data;
211+ result.textContent = data.success ? '✓ ' + data.data : '✗ ' + data.data;
212+ result.style.color = data.success ? 'green' : 'red';
213+ })
214+ .catch(e => {
215+ result.textContent = '✗ Error: ' + e.message;
216+ result.style.color = 'red';
217+ });
218+ }
219+
220+ function clearLog() {
221+ if (!confirm('Clear all log entries?')) return;
222+
223+ const result = document.getElementById('clear-result');
224+ result.textContent = 'Clearing...';
225+
226+ fetch(ajaxurl, {
227+ method: 'POST',
228+ headers: {'Content-Type': 'application/x-www-form-urlencoded'},
229+ body: 'action=next_revalidate_clear_log'
230+ })
231+ .then(r => r.json())
232+ .then(data => {
233+ if (data.success) {
234+ location.reload();
235+ } else {
236+ result.textContent = '✗ ' + data.data;
237+ result.style.color = 'red';
238+ }
157239 })
158240 .catch(e => {
159- result.textContent = 'Error: ' + e.message;
241+ result.textContent = '✗ Error: ' + e.message;
242+ result.style.color = 'red';
160243 });
161244 }
162245 </script>
@@ -213,8 +296,10 @@ public function on_status_change($new_status, $old_status, $post) {
213296 }
214297
215298 public function on_term_change ($ term_id , $ tt_id , $ taxonomy ) {
299+ $ term = get_term ($ term_id , $ taxonomy );
216300 $ this ->trigger_revalidation ('term ' , [
217301 'id ' => $ term_id ,
302+ 'slug ' => $ term ? $ term ->slug : '' ,
218303 'taxonomy ' => $ taxonomy ,
219304 'action ' => current_action ()
220305 ]);
@@ -229,11 +314,52 @@ private function trigger_revalidation($type, $data) {
229314
230315 // Check cooldown
231316 $ cooldown = $ options ['cooldown ' ] ?? 2 ;
232- $ last = get_option ($ this ->last_revalidation );
317+ $ last = get_option ($ this ->last_option );
233318 if ($ last && (time () - $ last ['time ' ]) < $ cooldown ) {
319+ $ this ->debug_log ('Skipped: cooldown active ' );
234320 return ;
235321 }
236322
323+ $ max_retries = $ options ['max_retries ' ] ?? 3 ;
324+ $ result = $ this ->send_with_retry ($ type , $ data , $ max_retries );
325+
326+ // Update last status
327+ update_option ($ this ->last_option , [
328+ 'time ' => time (),
329+ 'type ' => $ type ,
330+ 'success ' => $ result ['success ' ],
331+ 'http_code ' => $ result ['http_code ' ],
332+ 'error ' => $ result ['error ' ]
333+ ]);
334+
335+ // Add to log
336+ $ this ->add_log_entry ([
337+ 'time ' => time (),
338+ 'type ' => $ type ,
339+ 'data ' => $ data ,
340+ 'success ' => $ result ['success ' ],
341+ 'http_code ' => $ result ['http_code ' ],
342+ 'error ' => $ result ['error ' ],
343+ 'retries ' => $ result ['retries ' ]
344+ ]);
345+ }
346+
347+ private function send_with_retry ($ type , $ data , $ max_retries , $ attempt = 0 ) {
348+ $ result = $ this ->send_request ($ type , $ data );
349+
350+ if (!$ result ['success ' ] && $ attempt < $ max_retries ) {
351+ $ delay = pow (2 , $ attempt ); // Exponential backoff: 1, 2, 4, 8...
352+ $ this ->debug_log ("Retry {$ attempt }/ {$ max_retries } after {$ delay }s delay " );
353+ sleep ($ delay );
354+ return $ this ->send_with_retry ($ type , $ data , $ max_retries , $ attempt + 1 );
355+ }
356+
357+ $ result ['retries ' ] = $ attempt ;
358+ return $ result ;
359+ }
360+
361+ private function send_request ($ type , $ data ) {
362+ $ options = get_option ($ this ->option_name );
237363 $ url = $ options ['nextjs_url ' ] . '/api/revalidate ' ;
238364 $ secret = $ options ['webhook_secret ' ] ?? '' ;
239365
@@ -243,22 +369,58 @@ private function trigger_revalidation($type, $data) {
243369 'timestamp ' => time ()
244370 ];
245371
372+ $ this ->debug_log ("Sending request to {$ url }" , $ payload );
373+
246374 $ response = wp_remote_post ($ url , [
247- 'timeout ' => 10 ,
375+ 'timeout ' => 15 ,
248376 'headers ' => [
249377 'Content-Type ' => 'application/json ' ,
250378 'x-webhook-secret ' => $ secret
251379 ],
252380 'body ' => json_encode ($ payload )
253381 ]);
254382
255- $ success = !is_wp_error ($ response ) && wp_remote_retrieve_response_code ($ response ) === 200 ;
383+ if (is_wp_error ($ response )) {
384+ $ error = $ response ->get_error_message ();
385+ $ this ->debug_log ("Request failed: {$ error }" );
386+ return [
387+ 'success ' => false ,
388+ 'http_code ' => null ,
389+ 'error ' => $ error
390+ ];
391+ }
256392
257- update_option ($ this ->last_revalidation , [
258- 'time ' => time (),
259- 'type ' => $ type ,
260- 'success ' => $ success
261- ]);
393+ $ http_code = wp_remote_retrieve_response_code ($ response );
394+ $ body = wp_remote_retrieve_body ($ response );
395+ $ success = $ http_code === 200 ;
396+
397+ $ this ->debug_log ("Response: HTTP {$ http_code }" , $ body );
398+
399+ return [
400+ 'success ' => $ success ,
401+ 'http_code ' => $ http_code ,
402+ 'error ' => $ success ? null : "HTTP {$ http_code }"
403+ ];
404+ }
405+
406+ private function add_log_entry ($ entry ) {
407+ $ log = get_option ($ this ->log_option , []);
408+ array_unshift ($ log , $ entry );
409+ $ log = array_slice ($ log , 0 , 50 ); // Keep last 50
410+ update_option ($ this ->log_option , $ log );
411+ }
412+
413+ private function debug_log ($ message , $ data = null ) {
414+ $ options = get_option ($ this ->option_name );
415+ if (empty ($ options ['debug_mode ' ])) {
416+ return ;
417+ }
418+
419+ $ log_message = '[Next.js Revalidation] ' . $ message ;
420+ if ($ data !== null ) {
421+ $ log_message .= ' - ' . (is_string ($ data ) ? $ data : json_encode ($ data ));
422+ }
423+ error_log ($ log_message );
262424 }
263425}
264426
@@ -283,7 +445,7 @@ private function trigger_revalidation($type, $data) {
283445 $ secret = $ options ['webhook_secret ' ] ?? '' ;
284446
285447 $ response = wp_remote_post ($ url , [
286- 'timeout ' => 10 ,
448+ 'timeout ' => 15 ,
287449 'headers ' => [
288450 'Content-Type ' => 'application/json ' ,
289451 'x-webhook-secret ' => $ secret
@@ -301,8 +463,18 @@ private function trigger_revalidation($type, $data) {
301463
302464 $ code = wp_remote_retrieve_response_code ($ response );
303465 if ($ code !== 200 ) {
304- wp_send_json_error ('HTTP ' . $ code );
466+ wp_send_json_error ('HTTP ' . $ code . ' - ' . wp_remote_retrieve_body ($ response ));
467+ }
468+
469+ wp_send_json_success ('Connection successful! ' );
470+ });
471+
472+ // AJAX handler for clearing log
473+ add_action ('wp_ajax_next_revalidate_clear_log ' , function () {
474+ if (!current_user_can ('manage_options ' )) {
475+ wp_send_json_error ('Unauthorized ' );
305476 }
306477
307- wp_send_json_success ('Revalidation triggered successfully ' );
478+ delete_option ('next_revalidate_log ' );
479+ wp_send_json_success ('Log cleared ' );
308480});
0 commit comments