Skip to content

Commit c8d962d

Browse files
committed
feat(plugin): v1.1.0 - add revalidation log, retry logic, debug
mode
1 parent 7a95b8c commit c8d962d

File tree

1 file changed

+220
-48
lines changed

1 file changed

+220
-48
lines changed

wordpress/next-revalidate/next-revalidate.php

Lines changed: 220 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
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
@@ -15,7 +15,8 @@
1515

1616
class 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

Comments
 (0)