Efficient SSG in Next.js with WPGraphQL

Francis Agulto Avatar

·

In this article, I will discuss best practices around Static Site Generation in Next.js with dynamic routes and static paths.

Static Site Generation

Before I dive into optimization details, let’s go over quickly for context what Static Site Generation (or SSG for short) does and how it works with getStaticPaths in dynamic routes.

Next.js allows you to statically generate your site, pages and fetch data at build time with the function getStaticProps. The main reason developers choose this method is speed and performance as the static files and data are cached and served on a CDN and available right at request.

Static Paths and Dynamic Routes

When you have a site that is statically generated but you have a selection of posts on a home page and want users to be able to click that post which will route them to the details page of that individual post, you will need a route parameter for the route for that individual details page of the post. Now, Next.js does not know how many individual details pages we have and the routes associated with those pages since it depends on external data, in this case WordPress is our external data source.

We can explicitly tell Next.js what pages and routes we need to create at build time based on our WordPress data. To do this, we use the function called getStaticPaths. This runs at build time and inside it we return all the possible values of our route parameters. Then once we do, Next.js will know to generate a route and a page for each of those parameters.

How they work together

The Next.js syntax [param] allows for a page file to have the dynamic route capability based on parameters. Within this file, you can have the two functions I discussed. The getStaticPaths function which will build the paths and pages for each individual details page. The getStaticProps function fetches the data related to those individual details pages and adds the data unique to those pages statically. At a high level, this is how these two functions work together in a dynamic route page.

Next.js & WPGraphQL

When you use Next.js and WPGraphQL for Headless WordPress, an issue that you will run into is pre-rendering all your paths and pages in the function called getStaticPaths.

Building ALL pages every time a build is run leads to the WordPress server being hammered and sometimes becoming unresponsive. Another thing to consider when you do this is the long build times you will have if your site has a lot of pages.

Here are some symptoms of an unresponsive WP server examples in WPGraphQL:

SyntaxError: Unexpected token < in JSON at position 0

This code block below is a headless WordPress starter that my teammate Jeff made using Next.js. This is my getStaticPaths function at the bottom of my dynamic route file page [slug].js :

export async function getStaticPaths() {
  const GET_POSTS = gql`
    query AllPostsQuery {
      posts(first: 10000) {
        nodes {
          id
          title
          slug
          uri
        }
      }
    }
  `;
  const response = await client.query({
    query: GET_POSTS,
  });

  const posts = response?.data?.posts?.nodes;
  const paths = posts.map(({ slug }) => {
    return {
      params: {
        slug: slug,
      },
    };
  });

  return {
    paths,
    fallback: false,
  };
}

Code language: JavaScript (javascript)

This is a similar pattern we’ve seen in a few popular WordPress starters such as Colby Fayock’s and WebDevStudios. While this pattern feels intuitive, it can actually be problematic.

The first thing to notice at the top of this function is my GraphQL query and what it is fetching. It is fetching 10000 nodes from WPGraphQL. By default, WPGraphQL prevents more than 100 per request. If I continue to use this query, it either will only return 100 items or I will need to make bespoke modifiers in WPGraphQL to support this use case and Jason Bahl who created and maintains WPGraphQL highly advises against this.

I have a variable of paths and I am mapping through posts to grab the slug that I set it to. In the return object of the variable I have params giving us the slug of the post. Below that variable, I have a return object with the paths property getting all the paths and if that path does not exist in my prebuilt static paths is a 404 page which is fallback: false in Next.js.

When the 10000 nodes are returned, they are passed as paths and Next.js will build every single page and each page has a GraphQL query or more and is sent to the WordPress server, which then overwhelms the server. This is not optimal as I as I stated since this will not only overwhelm your server and make for a bad user experience on your site, but you will also accrue costs if your site gets larger for tools that charge for build times since your build times will continue to increase.

This is what it looks like when I run npm run build to create an optimized production build and build directory of my site within terminal:

Notice the /posts/[postSlug].js folder and file. Due to the way I have my getStaticPaths function set up, you can see that it is pre-building every single path and the time it takes to build them. Now, imagine if this was a site with hundreds or thousands of pages like ESPN. This would not be optimal. It could take hours to build every page.

An alternative to consider to fix this issue in your Dynamic Route file within your getStaticProps function in the return statement would be something like this:


export async function getStaticPaths() {
  const paths = [];
  return {
    paths,
    fallback: "blocking",
  };
}

Code language: JavaScript (javascript)

This is the same return statement shown previously. The difference is with setting the paths as an empty array and adding fallback: "blocking" ; this tells Next.js to not pre-build pages at build time. This will instead be Server Rendered upon each visit and Statically Generated upon subsequent visits. Doing this alleviates the issue of unnecessary GraphQL queries sent to the WordPress server and really long build times.

nodeByUri Query

One thing to note is the change of your query when you are going to server render your pages. The initial issue was that the query was asking for 10,000 posts and sent the post through the context of each path being pre-built. What we need now is a way to get the url out of the context and then query the page based on that using nodeByUri.

Here is a logic example:

 export const SEED_QUERY = gql`
query GetNodeByUri($uri: String!) {
    node: nodeByUri(uri: $uri) {
      ...NodeByUri
    }
  }


 if ( context.resolvedUrl ) {
    params = context?.params ?? null;
    resolvedUrl = context?.resolvedUrl ?? null;
    
  } else if ( context?.params?.WordPressNode ) {
    params = context?.params ?? null;
    isStatic = true;
    resolvedUrl = context?.params?.WordPressNode ? context?.params?.WordPressNode.join('/') : null;
  }

Code language: PHP (php)

This code example is getting the url of the page the user is visiting, then using that in the nodeByUri query. This allows users to do fallback: blocking, paths: [] but still have the context needed to grab the data and build the page. This video gives a walk through as well for reference if you need an overview of the query.

This is what my production build looks like now with this syntax change when I run npm run build :

In this image, the /posts/[slug].js folder and file is not pre-building the paths. It is allowing paths and pages to be generated on the fly by Server Rendering. No unnecessary path and page prebuilds.

If you have really important pages, you could put them in paths like this:

export async function getStaticPaths() {
    return {
        paths: [
          '/some-really-important-page',
        ],
        fallback: 'blocking'
    }
}
Code language: JavaScript (javascript)

This tells Next.js to build only the paths specified in the array. The rest are Server Rendered.

ISR Option

If you have content editors who want pages to be available close to the time that the content is published in WordPress and not after each new build step is complete, Incremental Static Regeneration or ISR for short is the best option. Even for cases that have very important pages you want to make sure are always static.

The code within your getStaticProps function in your Dynamic Route file to invoke ISR would look something like this:

export async function getStaticProps() {
   return {
    props: {
      posts,
    },
    // Next.js will attempt to re-generate the page:
    // - When a request comes in
    // - At most once every 10 seconds
    revalidate: 10, // In seconds
  }
}
Code language: JavaScript (javascript)

This is saying that every 10 seconds, Next.js will will revalidate the data on and this page upon user request. The caveat here is that the initial user who request this page will get the stale data but every user and request for this page after that initial request will get the fresh data within the timed interval you set. (You can set whatever time you want to revalidate). If you want a deeper dive into ISR, please reference the Next.js docs and our very own Jeff Everhart’s blog post.

ISR Considerations

A scenario to consider when you use ISR is a busy site with lots of visits. Staying with my time stamp example in my code block, I set it to revalidate every 10 seconds. Imagine that I have a very large, busy site and I invoke ISR on 5,000 pages. If I get traffic to all those pages and set to revalidate it every 10 seconds, it will rebuild all of the paths and pages every 10 seconds and you are back to square one with the original issue of overwhelming your WordPress server.

Now, this is just something I want to point out for consideration. For the most part, ISR is still the best option in our opinion. You can set your time stamp to an increased time interval as well as figure out how often each type of data really does change and configure it that way to optimize this approach.

On-Demand ISR Option

Next.js has a feature called On-Demand ISR which is similar to ISR except the difference with this feature is that instead of a time stamp interval and a visit from a user revalidating your stale data, you can update and revalidate the data and content of a page “on-demand” or manually; configuring WordPress to send a webhook to an API route in Next.js when an update to WordPress backend is made.

How to throttle Next.js’s concurrency

Another option is to throttle Next.js’s concurrency during the build and export phase in relation to how many threads it is using. Lowering the number of CPU’s to reduce concurrent builds will alleviate resources on the server requests when Next.js builds your site. The object in the next.config.js file at the root of the project for this option is as follows:

module.exports = uniformNextConfig({
  experimental: {
    // This is experimental but can
    // be enabled to allow parallel threads
    // with nextjs automatic static generation
    workerThreads: false,
    cpus: 1
  },
});
Code language: JavaScript (javascript)

This is an experimental feature in Next.js. In the config file above, the cpus are set to the value of your limits on your WordPress concurrent connections. This example shows 1. I do recommend you not setting it to the max since you want to leave some for WordPress editors.

The trade-off to this approach is it will slow down the build step, while will reducing the number of pages it tries to build simultaneously. This can help when you WordPress exceeding limitations under the number of requests.

Conclusion & Future Solutions

After seeing some Headless WordPress setups on Next.js and discussing issues within this topic with the community and WPGraphQL, we believe it is optimal to not pre-render every static path and page within getStaticPaths and a Dynamic Route file in Next.js to reduce running into server and GraphQL issues.

Adopting Headless WordPress and using Next.js can be daunting, especially if you have are not familiar with the ecosystem, its issues, and best practices to solve those issues.

Currently, there is no solution on WP that accurately listens to events and communicates with Next.js. Don’t worry though! Myself, the Headless WordPress team, and Jason Bahl here at WP Engine are working actively to continue to solve these issues in the very near future so stay tuned!!!!

Hopefully, this blog post on best practice tips of this focused topic was helpful and gave you a better understanding on optimizing Next.js, WPGraphQL, and getStaticPaths! If you want to see this blog post come to life in a video code tutorial, check out Colby, Jason and myself as we refactor according to these best practices here!

As always, hit us up on discord if you have any questions, thoughts, or just want to Jamstoke out with us!