Skip to content

Commit b5c47f0

Browse files
ccordachrisherold
andauthored
refactor: use stellate plugin instead of custom code for graphql cache management (#264)
* add stellate plugin * add stellate helper * stellate updates with vercel revalidation * key updates * update composer * update docs * pr feedback --------- Co-authored-by: Chris Herold <cmherold@gmail.com>
1 parent 73e9316 commit b5c47f0

File tree

10 files changed

+206
-284
lines changed

10 files changed

+206
-284
lines changed

.vscode/settings.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,8 @@
99
"source.fixAll.eslint": "explicit",
1010
"source.fixAll.stylelint": "explicit"
1111
},
12-
"eslint.workingDirectories": ["website"]
12+
"eslint.workingDirectories": ["website"],
13+
"[php]": {
14+
"editor.defaultFormatter": "bmewburn.vscode-intelephense-client"
15+
}
1316
}

docs/performance-backend.md

+6-16
Original file line numberDiff line numberDiff line change
@@ -31,30 +31,20 @@ To enable in production, change the same env variable via Vercel's ENV settings.
3131

3232
To bypass the CDN for authenticated requests, set a bypass header for `x-preview-token`.
3333

34-
## Purging on publish with webhooks
34+
## Purging on publish with Stellate and Vercel On-Demand Revalidation
3535

36-
Inside of `helpers/webhookds.php` we've written a small script that processes these WP events:
36+
We're using the Stellate Wordpress plugin to purge the Stellate cache on publish. It's a simple plugin that hooks into the `save_post` action in WordPress and purges the Stellate cache on a per-post basis. We also add custom hooks to purge the redirect cache using the Redirection Wordpress plugin and to purge our ACF Theme Settings when updated.
3737

38-
- Post create/update/delete
39-
- Page create/update/delete
40-
- Menu changes
41-
- Updates to our default theme options.
42-
- Updated to redirects for either Redirection or Yoast Premium.
38+
The stellate plugin provides a callback function with any post ids and types that we're invalidated and we use this function to get the paths of any pages that were cleared from the Stellate cache and send a request to the `/api/revalidate` endpoint to also call Vercel's On-Demand Revalidation on the affected paths.
4339

44-
There are configuration variables at the top of `functions.php`, prefixed with `$headless_webhooks_` where you can customize for your instance. One common customization is to add any custom post types you might have to the array of `$headless_webhooks_post_types`
40+
In order to call the `/api/revalidate` endpoint, we need to set the `HEADLESS_REVALIDATE_SECRET` environment variable in Vercel and in the `wp-config.php` file in our WordPress install. This should be a secure, random string that will be used to authenticate the request.
4541

46-
When these events happen, a webhook will ping an API route in Next that sends a purge request to the Graph CDN. By default it's a simple `purgeAll` call, which is good enough on most sites that infrequently publish. For larger sites you might want to selectively purge content with some custom logic.
42+
If everything is working correctly, then after you make a change in WordPress, you should see a WPStellateIntegration purging request inside of the stellate admin.
4743

48-
To activate purging on publish, you'll need to get an API key and save it to a `GRAPHCDN_PURGE_API_TOKEN` and also set `GRAPHCDN_PURGE_API_URL` in Vercel ENV variables.
44+
Then on the Vercel logs, you should see a 200 request on calls to /api/revalidate/
4945

5046
## Debugging Graph CDN cache
5147

5248
Inside of the Graph CDN interface is a helpful "API playground" where you can inspect and test cache statuses.
5349

5450
You can try for example running a query, confirming successful `HIT` cache status, then update content in WordPress. When you rerun the query from the playground the status should change to `MISS`
55-
56-
## Future
57-
58-
Something we're keeping our eye on is the ability to invalidate the vercel edge cache via the same webhook we use to invalidate GraphCDN. This will allow us to lengthen the amount of time Vercel uses before revalidating.
59-
60-
[https://nextjs.org/blog/next-9-5#stable-incremental-static-regeneration](https://nextjs.org/blog/next-9-5#stable-incremental-static-regeneration)

website/.env.sample

+3
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ NEXT_PUBLIC_WORDPRESS_API_URL=https://bubsnext.graphcdn.app
2727

2828
# Enable bundle analyzer
2929
# ANALYZE=true
30+
31+
# Vercel secret for revalidate API, also need to set this as HEADLESS_REVALIDATE_SECRET in wp-config.php
32+
HEADLESS_REVALIDATE_SECRET=bubs-next-vercel-revalidate-secret-key

website/src/pages/api/graphcdn.js

-72
This file was deleted.

website/src/pages/api/revalidate.js

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export default async function handler(req, res) {
2+
const { secret, paths } = req.body;
3+
4+
// Check for secret to confirm this is a valid request
5+
if (secret !== process.env.HEADLESS_REVALIDATE_SECRET) {
6+
return void res.status(401).json({ message: 'Invalid token' });
7+
}
8+
9+
if (!paths || paths.length === 0) {
10+
return void res.json({ revalidated: false });
11+
}
12+
13+
try {
14+
await Promise.all(paths.map((path) => res.revalidate(path)));
15+
return void res.json({ revalidated: true });
16+
} catch (err) {
17+
// If there was an error, Next.js will continue
18+
// to show the last successfully generated page
19+
console.error(err);
20+
return void res.status(500).send('Error revalidating');
21+
}
22+
}

wordpress/composer.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@
2828
"wpackagist-plugin/filter-page-by-template": "3.1",
2929
"wpackagist-plugin/google-apps-login": "3.4.6",
3030
"wpackagist-plugin/post-type-archive-links": "1.3.1",
31-
"wpackagist-plugin/post-type-switcher": "3.2.1",
31+
"wpackagist-plugin/post-type-switcher": "3.3.1",
3232
"wpackagist-plugin/redirection": "5.3.10",
3333
"wpackagist-plugin/safe-svg": "2.1.1",
3434
"wpackagist-plugin/simple-history": "4.4.0",
3535
"wpackagist-plugin/so-clean-up-wp-seo": "4.0.1",
36+
"wpackagist-plugin/stellate": "0.1.8",
3637
"wpackagist-plugin/term-management-tools": "2.0.1",
3738
"wpackagist-plugin/wordpress-seo": "20.13",
3839
"wpackagist-plugin/wp-graphql": "1.14.10",

wordpress/docker-compose.yml

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ services:
2929
WORDPRESS_DEBUG: 1
3030
WORDPRESS_CONFIG_EXTRA: |
3131
define('WP_ENV', 'development');
32+
define('HEADLESS_REVALIDATE_SECRET', 'bubs-next-vercel-revalidate-secret-key');
3233
define('HEADLESS_AUTH_SECRET', 'bubs-next-wp-auth-secret-key');
3334
define('HEADLESS_API_SECRET', 'bubs-next-headless-secret-key');
3435
define('WP_LOCAL_DEV', true);

wordpress/wp-content/themes/headless/functions.php

+13-10
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,21 @@
1212
$dashboard_cleanup = false; // Optionally will hide all but our custom widget
1313
$docs_link = ''; // set to a path if you have a site/document for editor instructions
1414

15-
// webhook config
16-
$headless_webhooks_password_protected = true;
17-
$headless_webhooks_graphcdn_purge_api = 'http://host.docker.internal:3000/api/graphcdn/'; // Docker to host computer endpoint
18-
$headless_webhooks_acf_options = ['acf-options-theme-settings'];
19-
$headless_webhooks_post_types = ['page', 'post'];
20-
$headless_webhooks_redirects_redirection = true;
21-
$headless_webhooks_redirects_yoast = false;
15+
// stellate config
16+
$stellate_production_enabled = true;
17+
$stellate_staging_enabled = false;
18+
$stellate_staging_service_name = "";
19+
$stellate_staging_token = "";
20+
$stellate_development_enabled = false;
21+
$stellate_development_service_name = "";
22+
$stellate_development_token = "";
23+
$stellate_purge_redirection = true;
24+
$stellate_purge_acf_options = true;
2225

2326
// Determine the hosting environment we're in
2427
if (defined('WP_ENV') && WP_ENV == 'development') {
2528
define('WP_HOST', 'localhost');
26-
$headless_domain = $local_domain || 'http://localhost:3000';
29+
$headless_domain = 'http://localhost:3000';
2730
} else {
2831
$headless_domain = rtrim(get_theme_mod('headless_preview_url'), '/');
2932

@@ -67,7 +70,7 @@ function bubs_theme_options($wp_customize) {
6770
}
6871

6972
include_once 'setup/helpers/role-super-editor.php';
70-
include_once 'setup/helpers/webhooks.php';
73+
include_once 'setup/helpers/stellate.php';
7174
include_once 'setup/helpers/wpgraphql.php';
7275
include_once 'setup/helpers/wysiwyg.php';
7376

@@ -81,4 +84,4 @@ function bubs_theme_options($wp_customize) {
8184
// REMOVAL OF THESE = POTIENTAL LOSS OF DATA
8285

8386
add_theme_support('post-thumbnails');
84-
add_theme_support('menus');
87+
add_theme_support('menus');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<?php
2+
// Disable a plugin based on environment. We don't want to purge cache during local/staging development
3+
// unless needed for specific testing, in which case we can enable it using global variables in functions.php
4+
// Also allows for listening to graphcdn_purge event to perform additional invalidation
5+
6+
// add_action('admin_init', 'disable_stellate_plugin');
7+
8+
9+
function disable_stellate_plugin()
10+
{
11+
$plugin_slug = 'stellate/wp-stellate.php';
12+
if (!defined('WP_STELLATE_ENABLED')) {
13+
if (defined('WP_HOST') && WP_HOST == "production") {
14+
global $stellate_production_enabled;
15+
if ($stellate_production_enabled) {
16+
define('WP_STELLATE_ENABLED', true);
17+
}
18+
}
19+
20+
if (defined('WP_HOST') && WP_HOST == "staging") {
21+
global $stellate_staging_enabled;
22+
if ($stellate_staging_enabled) {
23+
define('WP_STELLATE_ENABLED', true);
24+
}
25+
}
26+
27+
if (defined('WP_HOST') && WP_HOST == "development") {
28+
global $stellate_development_enabled;
29+
if ($stellate_development_enabled) {
30+
define('WP_STELLATE_ENABLED', true);
31+
}
32+
}
33+
}
34+
35+
36+
if (defined('WP_STELLATE_ENABLED') && WP_STELLATE_ENABLED) {
37+
// leave enabled
38+
} else {
39+
// disable
40+
deactivate_plugins($plugin_slug);
41+
}
42+
}
43+
44+
global $stellate_purge_redirection;
45+
global $stellate_purge_acf_options;
46+
47+
if ($stellate_purge_redirection) {
48+
function purge_redirection()
49+
{
50+
stellate_add_purge_entity('purged_types', 'RedirectionRedirects');
51+
}
52+
add_action('redirection_redirect_updated', 'purge_redirection');
53+
add_action('redirection_redirect_deleted', 'purge_redirection');
54+
}
55+
56+
if ($stellate_purge_acf_options) {
57+
function purge_acf_options()
58+
{
59+
stellate_add_purge_entity('purged_types', 'AcfOptionsThemeSettings');
60+
}
61+
add_action('acf/options_page/save', 'purge_acf_options');
62+
}
63+
64+
add_action('redirection_redirect_updated', 'purge_redirection');
65+
add_action('redirection_redirect_deleted', 'purge_redirection');
66+
add_action('acf/options_page/save', 'purge_acf_options');
67+
68+
69+
function vercel_revalidate($urls)
70+
{
71+
global $headless_domain;
72+
73+
if (defined('WP_ENV') && WP_ENV == 'development') {
74+
$api_domain = 'http://host.docker.internal:3000';
75+
} else {
76+
$api_domain = $headless_domain;
77+
}
78+
79+
$paths = [];
80+
81+
if (!defined('HEADLESS_REVALIDATE_SECRET')) {
82+
error_log('HEADLESS_REVALIDATE_SECRET not defined');
83+
return;
84+
}
85+
86+
foreach ($urls as $url) {
87+
if (strpos($url, '://')) {
88+
$parsed = parse_url($url);
89+
$url = $parsed['path'];
90+
}
91+
if ($url !== '/') {
92+
$url = rtrim($url, '/'); // Remove trailing slash
93+
}
94+
array_push($paths, $url);
95+
}
96+
97+
$post_url = $api_domain . '/api/revalidate';
98+
99+
$body = array(
100+
'paths' => $paths,
101+
'secret' => HEADLESS_REVALIDATE_SECRET
102+
);
103+
104+
$args = array(
105+
'body' => json_encode($body),
106+
'headers' => array('Content-Type' => 'application/json')
107+
);
108+
109+
wp_remote_post($post_url, $args);
110+
}
111+
112+
function stellate_purge_callback($purge_data)
113+
{
114+
$paths = [];
115+
116+
// Uncomment to add logs to debug.log
117+
// error_log('attempting handle_stellate_purge');
118+
// error_log(json_encode($purge_data));
119+
120+
if ($purge_data['has_purged_all']) {
121+
$query = new WP_Query(array('posts_per_page' => -1, 'fields' => 'ids'));
122+
foreach ($query as $id) {
123+
array_push($paths, get_permalink($id));
124+
}
125+
vercel_revalidate($paths);
126+
return;
127+
}
128+
129+
// Process purged types
130+
foreach ($purge_data['purged_types'] as $type) {
131+
$path = get_post_type_archive_link($type);
132+
if ($path) {
133+
array_push($paths, $path);
134+
}
135+
}
136+
137+
if (isset($purge_data['purged_types'])) {
138+
unset($purge_data['purged_types']);
139+
}
140+
141+
foreach ($purge_data as $type => $ids) {
142+
if (is_array($ids) && !empty($ids)) {
143+
foreach ($ids as $id) {
144+
$path = get_permalink($id);
145+
if ($path) {
146+
array_push($paths, $path);
147+
}
148+
}
149+
}
150+
}
151+
152+
vercel_revalidate($paths);
153+
}
154+
155+
add_action('stellate_purge', 'stellate_purge_callback', 10, 2);

0 commit comments

Comments
 (0)