Picture of Chris Eagle

Chris EagleWriting about web development and other things.

Headless WordPress & Next.js - On Demand Incremental Static Regeneration

ยท5 min read

Next.js v12.1 has been released with a highly requested feature to manually trigger static regeneration and purge the cache instead of relying on visitors to regenerate after the invalidation time has been exceeded. This allows an event driven flow from the CMS to rebuild static pages on demand. Let's explore how we can leverage this with WordPress as our Headless CMS.

WordPress has a hooks system built in using what it calls actions and filters. These hooks allow you to hook in at specific points in the WordPress life cycle or at user generated events. We can use these hooks to our advantage and trigger an invalidation to our Next.js app when a post as been saved or updated.

Next.js v12.1 - On-demand Incremental Static Regeneration

Let's take a look at how we can configure the /api routes in Next.js to trigger this static regeneration. The v12.1 release notes provide a good example of how we can leverage this functionality. Any files created within /pages/api in Next.js can be called to handle a plethora of tasks that can be run in Node.

Let's create /pages/api/revalidate.js in our Next.js app.

// pages/api/revalidate.js

export default async function handler(request, response) {
  if (request.query.secret !== process.env.REVALIDATE_TOKEN) {
    return response.status(401).json({ message: 'Invalid token' })
  }
  try {
    await response.unstable_revalidate(request.query.path)
    return response.json({ revalidated: true })
  } catch (err) {
    return response.status(500).send('Error revalidating')
  }
}

As we can see in the above snippet, the handler function provides us with a request and a response params that we can utilise to shape the response we return. As we run down through the snippet, we see a conditional block that checks to see if a REVALIDATE_TOKEN environment variable is present in the request, and if not it returns a 401 unauthorised response. The next try/catch block calls the new unstable_revalidate method to trigger the regeneration and catches any errors that occur.

Configure the revalidation token

Environment variables are super simple to add in Next.js and read without any additional packages required. Next.js runs through a specific priority order when reading from .env files, in this instance we will simplify things and pop the variables in .env.local.

Create a .env.local file in the root of our Next app and add the following to it.

// .env.local
REVALIDATE_TOKEN=SuperSecretSquirrel

The token can then be read as per the example using process.env.REVALIDATE_TOKEN in the api route. Once saved, we can call this api route by sending a request to //yournextapp.com/api/revalidate. You can try it out by visiting the URL in your browser to double check it's working with and without the query params.

Configure the WordPress hook

Let's head over to WordPress and create the hook that will run when we save or update a post. We'll be creating the functionality in a must-use plugin, must-use plugins is a good pattern follow as they are automatically enabled and always-on, with no need to enable via admin and users cannot disable them by accident.

In a headless setup this a good pattern - as you likely won't have a local theme functions.php to add this code into and if being leveraged correctly, it will be paramount to delivering up to date to content to your users and will need to always be available.

// wp-content/mu-plugins/next_revalidate.php

/**
 * On save_post schedule a one-time event
 * Priority @ 99 to run after other events on same hook
 */
add_action('save_post', function( int $post_ID, WP_Post $post, bool $update ) {

  // whitelist of post types to check against
  $type_whitelist = array(
    'post',
    'page',
    'custom-type'
  );

  // check if post is one we want to invalidate and check status
  if( false === in_array( $post->post_type, $type_whitelist ) || 'publish' !== $post->post_status ) {
    return;
  }

  // schedule one time event
  wp_schedule_single_event( time(), 'next_revalidate', array( 'post_ID' => $post_ID ) );

}, 99, 3);

/**
 * One-time event to request our api endpoint in Next.js
 */
add_action('next_revalidate', function( int $post_ID ) {
  $secret = 'SuperSecretSquirrel';
  $parts = wp_parse_url( get_permalink( $post_ID ) );
  $path = urlencode( $parts['path'] );
  wp_remote_get( 
    "https://nextapp.com/api/revalidate?secret={$secret}&path={$path}",
  );
}, 10, 1);

Scheduling a one-time event for running the revalidation allows the request to happen behind the scenes on the next page request and should mean that if the request to our Next.js app responds slower than expected, editors won't experience additional lag when saving posts.

Now you can go ahead and test out the revalidation. The easiest way to test that this is working is to console log out the request.query object console.log(request.query) in the handler function, if working correctly you should see an object with both the secret and the path populated.

Additionally you can configure the invalidation time in getStaticProps() to 3600, and return a timestamp to the page. If you render out the timestamp on the route you've invalidated, you should expect to see the timestamp update after each regeneration request.

You can find sample code for this post over on Github here: github.com/chriseagle.