Skip to content

Commit 1681aa8

Browse files
committed
add wordpress setup
1 parent da5bec8 commit 1681aa8

File tree

6 files changed

+425
-0
lines changed

6 files changed

+425
-0
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Build WordPress Image
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'wordpress/**'
8+
workflow_dispatch:
9+
10+
env:
11+
REGISTRY: ghcr.io
12+
IMAGE_NAME: ${{ github.repository_owner }}/next-wp-wordpress
13+
14+
jobs:
15+
build:
16+
runs-on: ubuntu-latest
17+
permissions:
18+
contents: read
19+
packages: write
20+
21+
steps:
22+
- name: Checkout repository
23+
uses: actions/checkout@v4
24+
25+
- name: Log in to GitHub Container Registry
26+
uses: docker/login-action@v3
27+
with:
28+
registry: ${{ env.REGISTRY }}
29+
username: ${{ github.actor }}
30+
password: ${{ secrets.GITHUB_TOKEN }}
31+
32+
- name: Extract metadata for Docker
33+
id: meta
34+
uses: docker/metadata-action@v5
35+
with:
36+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
37+
tags: |
38+
type=raw,value=latest
39+
type=sha,prefix=
40+
41+
- name: Build and push Docker image
42+
uses: docker/build-push-action@v5
43+
with:
44+
context: ./wordpress
45+
push: true
46+
tags: ${{ steps.meta.outputs.tags }}
47+
labels: ${{ steps.meta.outputs.labels }}

wordpress/Dockerfile

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
FROM wordpress:latest
2+
3+
# Labels for GitHub Container Registry
4+
LABEL org.opencontainers.image.source="https://github.com/9d8dev/next-wp"
5+
LABEL org.opencontainers.image.description="WordPress with next-revalidate plugin for headless Next.js"
6+
LABEL org.opencontainers.image.licenses="MIT"
7+
8+
# Install WP-CLI
9+
RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \
10+
&& chmod +x wp-cli.phar \
11+
&& mv wp-cli.phar /usr/local/bin/wp
12+
13+
# Copy the revalidation plugin
14+
COPY next-revalidate /var/www/html/wp-content/plugins/next-revalidate
15+
16+
# Copy the setup scripts
17+
COPY setup.sh /usr/local/bin/setup-wordpress.sh
18+
COPY entrypoint.sh /usr/local/bin/custom-entrypoint.sh
19+
RUN chmod +x /usr/local/bin/setup-wordpress.sh /usr/local/bin/custom-entrypoint.sh
20+
21+
ENTRYPOINT ["/usr/local/bin/custom-entrypoint.sh"]
22+
CMD ["apache2-foreground"]

wordpress/entrypoint.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/bash
2+
3+
# Run the setup script in the background after a delay
4+
(sleep 30 && /usr/local/bin/setup-wordpress.sh) &
5+
6+
# Run the original WordPress entrypoint
7+
exec docker-entrypoint.sh "$@"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<?php
2+
// Silence is golden.
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
<?php
2+
/**
3+
* Plugin Name: Next.js Revalidation
4+
* Plugin URI: https://github.com/9d8dev/next-wp
5+
* Description: Automatically revalidate Next.js cache when WordPress content changes
6+
* Version: 1.0.0
7+
* Author: 9d8
8+
* Author URI: https://9d8.dev
9+
* License: MIT
10+
*/
11+
12+
if (!defined('ABSPATH')) {
13+
exit;
14+
}
15+
16+
class NextRevalidate {
17+
private $option_name = 'next_revalidate_settings';
18+
private $last_revalidation = 'next_revalidate_last';
19+
20+
public function __construct() {
21+
add_action('admin_menu', [$this, 'add_admin_menu']);
22+
add_action('admin_init', [$this, 'register_settings']);
23+
24+
// Hook into content changes
25+
add_action('save_post', [$this, 'on_post_change'], 10, 3);
26+
add_action('delete_post', [$this, 'on_post_delete']);
27+
add_action('transition_post_status', [$this, 'on_status_change'], 10, 3);
28+
29+
// Hook into taxonomy changes
30+
add_action('created_term', [$this, 'on_term_change'], 10, 3);
31+
add_action('edited_term', [$this, 'on_term_change'], 10, 3);
32+
add_action('delete_term', [$this, 'on_term_change'], 10, 3);
33+
}
34+
35+
public function add_admin_menu() {
36+
add_options_page(
37+
'Next.js Revalidation',
38+
'Next.js Revalidation',
39+
'manage_options',
40+
'next-revalidate',
41+
[$this, 'settings_page']
42+
);
43+
}
44+
45+
public function register_settings() {
46+
register_setting($this->option_name, $this->option_name, [
47+
'sanitize_callback' => [$this, 'sanitize_settings']
48+
]);
49+
50+
add_settings_section(
51+
'next_revalidate_main',
52+
'Configuration',
53+
null,
54+
'next-revalidate'
55+
);
56+
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+
);
80+
}
81+
82+
public function sanitize_settings($input) {
83+
$sanitized = [];
84+
$sanitized['nextjs_url'] = esc_url_raw(rtrim($input['nextjs_url'] ?? '', '/'));
85+
$sanitized['webhook_secret'] = sanitize_text_field($input['webhook_secret'] ?? '');
86+
$sanitized['cooldown'] = absint($input['cooldown'] ?? 2);
87+
return $sanitized;
88+
}
89+
90+
public function field_nextjs_url() {
91+
$options = get_option($this->option_name);
92+
$value = $options['nextjs_url'] ?? '';
93+
echo '<input type="url" name="' . $this->option_name . '[nextjs_url]" value="' . esc_attr($value) . '" class="regular-text" placeholder="https://your-nextjs-site.com" />';
94+
echo '<p class="description">The URL of your Next.js application (without trailing slash)</p>';
95+
}
96+
97+
public function field_webhook_secret() {
98+
$options = get_option($this->option_name);
99+
$value = $options['webhook_secret'] ?? '';
100+
echo '<input type="text" name="' . $this->option_name . '[webhook_secret]" value="' . esc_attr($value) . '" class="regular-text" />';
101+
echo '<p class="description">Must match WORDPRESS_WEBHOOK_SECRET in your Next.js environment</p>';
102+
}
103+
104+
public function field_cooldown() {
105+
$options = get_option($this->option_name);
106+
$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>';
109+
}
110+
111+
public function settings_page() {
112+
if (!current_user_can('manage_options')) {
113+
return;
114+
}
115+
116+
$last = get_option($this->last_revalidation);
117+
?>
118+
<div class="wrap">
119+
<h1>Next.js Revalidation Settings</h1>
120+
121+
<?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>
126+
</div>
127+
<?php endif; ?>
128+
129+
<form method="post" action="options.php">
130+
<?php
131+
settings_fields($this->option_name);
132+
do_settings_sections('next-revalidate');
133+
submit_button();
134+
?>
135+
</form>
136+
137+
<hr>
138+
<h2>Test Revalidation</h2>
139+
<p>
140+
<button type="button" class="button" onclick="testRevalidation()">Send Test Request</button>
141+
<span id="test-result"></span>
142+
</p>
143+
144+
<script>
145+
function testRevalidation() {
146+
const result = document.getElementById('test-result');
147+
result.textContent = 'Sending...';
148+
149+
fetch(ajaxurl, {
150+
method: 'POST',
151+
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
152+
body: 'action=next_revalidate_test'
153+
})
154+
.then(r => r.json())
155+
.then(data => {
156+
result.textContent = data.success ? 'Success!' : 'Failed: ' + data.data;
157+
})
158+
.catch(e => {
159+
result.textContent = 'Error: ' + e.message;
160+
});
161+
}
162+
</script>
163+
</div>
164+
<?php
165+
}
166+
167+
public function on_post_change($post_id, $post, $update) {
168+
if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) {
169+
return;
170+
}
171+
172+
if ($post->post_status !== 'publish') {
173+
return;
174+
}
175+
176+
$this->trigger_revalidation('post', [
177+
'id' => $post_id,
178+
'slug' => $post->post_name,
179+
'type' => $post->post_type,
180+
'action' => $update ? 'update' : 'create'
181+
]);
182+
}
183+
184+
public function on_post_delete($post_id) {
185+
$post = get_post($post_id);
186+
if (!$post || $post->post_status !== 'publish') {
187+
return;
188+
}
189+
190+
$this->trigger_revalidation('post', [
191+
'id' => $post_id,
192+
'slug' => $post->post_name,
193+
'type' => $post->post_type,
194+
'action' => 'delete'
195+
]);
196+
}
197+
198+
public function on_status_change($new_status, $old_status, $post) {
199+
if ($new_status === $old_status) {
200+
return;
201+
}
202+
203+
if ($old_status === 'publish' || $new_status === 'publish') {
204+
$this->trigger_revalidation('post', [
205+
'id' => $post->ID,
206+
'slug' => $post->post_name,
207+
'type' => $post->post_type,
208+
'action' => 'status_change',
209+
'old_status' => $old_status,
210+
'new_status' => $new_status
211+
]);
212+
}
213+
}
214+
215+
public function on_term_change($term_id, $tt_id, $taxonomy) {
216+
$this->trigger_revalidation('term', [
217+
'id' => $term_id,
218+
'taxonomy' => $taxonomy,
219+
'action' => current_action()
220+
]);
221+
}
222+
223+
private function trigger_revalidation($type, $data) {
224+
$options = get_option($this->option_name);
225+
226+
if (empty($options['nextjs_url'])) {
227+
return;
228+
}
229+
230+
// Check cooldown
231+
$cooldown = $options['cooldown'] ?? 2;
232+
$last = get_option($this->last_revalidation);
233+
if ($last && (time() - $last['time']) < $cooldown) {
234+
return;
235+
}
236+
237+
$url = $options['nextjs_url'] . '/api/revalidate';
238+
$secret = $options['webhook_secret'] ?? '';
239+
240+
$payload = [
241+
'type' => $type,
242+
'data' => $data,
243+
'timestamp' => time()
244+
];
245+
246+
$response = wp_remote_post($url, [
247+
'timeout' => 10,
248+
'headers' => [
249+
'Content-Type' => 'application/json',
250+
'x-webhook-secret' => $secret
251+
],
252+
'body' => json_encode($payload)
253+
]);
254+
255+
$success = !is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200;
256+
257+
update_option($this->last_revalidation, [
258+
'time' => time(),
259+
'type' => $type,
260+
'success' => $success
261+
]);
262+
}
263+
}
264+
265+
// Initialize plugin
266+
add_action('init', function() {
267+
new NextRevalidate();
268+
});
269+
270+
// AJAX handler for test button
271+
add_action('wp_ajax_next_revalidate_test', function() {
272+
if (!current_user_can('manage_options')) {
273+
wp_send_json_error('Unauthorized');
274+
}
275+
276+
$options = get_option('next_revalidate_settings');
277+
278+
if (empty($options['nextjs_url'])) {
279+
wp_send_json_error('Next.js URL not configured');
280+
}
281+
282+
$url = $options['nextjs_url'] . '/api/revalidate';
283+
$secret = $options['webhook_secret'] ?? '';
284+
285+
$response = wp_remote_post($url, [
286+
'timeout' => 10,
287+
'headers' => [
288+
'Content-Type' => 'application/json',
289+
'x-webhook-secret' => $secret
290+
],
291+
'body' => json_encode([
292+
'type' => 'test',
293+
'data' => ['message' => 'Test from WordPress'],
294+
'timestamp' => time()
295+
])
296+
]);
297+
298+
if (is_wp_error($response)) {
299+
wp_send_json_error($response->get_error_message());
300+
}
301+
302+
$code = wp_remote_retrieve_response_code($response);
303+
if ($code !== 200) {
304+
wp_send_json_error('HTTP ' . $code);
305+
}
306+
307+
wp_send_json_success('Revalidation triggered successfully');
308+
});

0 commit comments

Comments
 (0)