Date

Easy static HTML exports of your Next.js + GraphQL site

You’re here because you’d like to learn how to create static HTML exports for your Next.js site which uses GraphQL as a data source to create dynamic pages from page components. And that site may even use WPGraphQL to pull content from WordPress.

Fantastic, this article will describe the simple process of doing exactly that.

Generally speaking, all else being equal, serving a static HTML file as a webpage is generally going to be the fastest way to get that page in your users hands. There are exceptions to every rule, but this is a pretty safe bet. This speed is great for SEO, UX, conversion rates, blah blah blah, but building a site that delivers that fast is also just fun!

Bare with me for a few moments as we setup the solution with a bit of boilerplate.

As you will see if you peruse the interactive Next.js docs, they have a simple method of enabling static HTML exports. Create a file in your project at the root directory called next.config.js, and add the following code:

module.exports = {
  exportPathMap: function () {
    return {
      '/': { page: '/' }
    }
  }
}

Then add the following to package.json:

{
  "scripts": {
    "build": "next build",
    "export": "next export"
  }
}

And then run:

npm run build
npm run export

Now checkout the out directory. You’ll see static HTML file for your index route. You can now run the following to see your site in action:

npm install -g serve
cd out
serve -p 8080

The Next.js docs then extend this example with the following code snippet:

module.exports = {
  exportPathMap: function () {
    return {
      '/': { page: '/' },
      '/about': { page: '/about' },
      '/p/hello-nextjs': {page: '/post', query: { title: 'Hello Next.js' } },
      '/p/learn-nextjs': { page: '/post', query: { title: 'Learn Next.js is awesome' } },
      '/p/deploy-nextjs': { page: '/post', query: { title: 'Deploy apps with Zeit' } },
      '/p/exporting-pages': { page: '/post', query: { title: 'Learn to Export HTML Pages' } }
    }
  }
}

What we are doing here on the lines starting with '/p…' is passing query data to the component at '/post'.

Ok, now the problem becomes clear: we need a data source to populate the query (or params, title, date, etc.) that our components rely on. But the next export doesn’t “hook into” the GraphQL data source you have likely already setup in your _app.js.

So does this mean you need to rewire everything to hook up GraphQL, write fetching logic, looping, etc?

No, it means I had to. You can just install the NPM package I created and do some basic configuration. 🙂

Enter next-graphql-static-export:

A minimal package which provides the functionality required to perform a static HTML export of dynamic pages from Next.js when using GraphQL as a data source.
https://www.npmjs.com/package/next-graphql-static-export

Ok, end of boilerplate setup. Let’s get down creating your Next.js+GraphQL+WPGraphQL+static-HTML-exported site.

By the way, you do not need to include WPGraphQL in your project to make use of this method. Though if your site has a WordPress backend, you probably should.

There are no assumptions in the code for the NPM package which rely on WPGraphQL, other than possibly the schema.

Here is the relevant bit of the WPGraphQL schema in a query result:

{
  "data": {
    "posts": {
      "pageInfo": {
        "endCursor": "PQOXYXljb25uZWN0oT6uOjEwMzg5Mg==",
        "hasNextPage": true
      },
      "nodes": [
        {
          "id": "cG3zdDozMDO3MTA=",
          "uri": "why-you-should-use-wpgraphql",
          "title": "Why You Should Use WPGraphQL"
        }
      ]
    }
  }
}

Notice the nesting of pageInfo, endCursor and hasNext page, along with nodes. It’s worth taking a quick look at the results you get from queries on your project if you are not using WPGraphQL. If your GraphQL server does not return data in this format, no problem, you can still easily use this package. You’ll just need to write a custom query result parsing function. Details below.

We’ll assume for the moment that your GraphQL server does have the same schema as WPGraphQL and move on.

First install ‘next-graphql-static-export’:

npm i next-graphql-static-export

Now create a new config-exports.js file in the root directory of your project, where we will configure the parameters that we pass to the processContent made available via the next-graphql-static-export library. The following is an example of sensible defaults:

// Include our queries. See https://docs.wpgraphql.com/getting-started/posts for basic queries
const postsQuery = require("./queries/posts-query");
const pagesQuery = require("./queries/pages-query");
const productsQuery = require("./queries/products-query");

// Your graphql endpoint
const endpoint = `https://www.website.com/graphql`;

// The custom information we pass to properly fetch data for each 'post type' or content type, the result of which is the object that Next.js needs to produce an accurate static HTML export of our site
const typeParams = [
  {
    pageComponent: "page",
    contentType: "pages",
    query: pagesQuery,
    urlBase: "pages",
    perPage: 100,
    endpoint
  },
  {
    pageComponent: "product",
    contentType: "products",
    query: productsQuery,
    urlBase: "products",
    perPage: 100,
    endpoint
  },
  {
    pageComponent: "article",
    contentType: "posts",
    query: postsQuery,
    urlBase: "articles",
    perPage: 100,
    endpoint
  }
];

module.exports = {
  typeParams
};

Open up your package.json again, and add change the line pertaining to the dev script to the following:

"scripts": {
    "dev": "EXPORT=false node server.js",
    ...
}

This EXPORT env var will allow us to continue to use npm run dev to develop our site, yet avoid doing a full static HTML export every time we change a line of code (see next step for where we use this env var).

Open up your next.config.js again, and make it look similar to the following:

const processContent = require("next-graphql-static-export");
const { typeParams } = require("./config-export");

module.exports = {
  exportPathMap: async () => {
    if (EXPORT === "false") return {}; // This is where we use the env var we just added to the dev script.

    const [pages, products] = await processContent(typeParams);

    // Create the static pages with Next
    return {
      "/": { page: "/" }
      // ...pages,
      // ...products
      // ...posts
    };
  }
};

Now go ahead and run the following again:

npm run build
npm run export
cd out
serve -p 8080

Open up localhost:8080 and you should see your beautiful site with all dynamic page fully rendered and served as static HTML!

Passing your own query result parsing function

If your schema differs from the one shown above, then you will need to pass your own query result parsing function to your typeParams in your config-export.js file. Here is an example function which exactly reproduces the existing functionality:

const parseQueryResults = (queryResponse, contentType) => {
  const {
    [contentType]: {
      nodes,
      pageInfo: { hasNextPage, endCursor } 
    }
  } = queryResponse;
  return { nodes, hasNextPage, endCursor };
};

Note that the function takes in the queryResponse, and the current contentType, both of which are used to destructure the query result and return the following required values: nodes, hasNextPage, endCursor.

You can use any amount of logic in this function, but those values are required.

To use this function in your implementation, simply add the function as a parameter to the relevant content types. So our new config-exports.js would look like this:

// Include our queries. See https://docs.wpgraphql.com/getting-started/posts for basic queries
const postsQuery = require("./queries/posts-query");
const pagesQuery = require("./queries/pages-query");
const productsQuery = require("./queries/products-query");

// Your graphql endpoint
const endpoint = `https://www.website.com/graphql`;

// Custom query result parsing function
const parseQueryResults = (queryResponse, contentType) => {
  const {
    [contentType]: {
      nodes,
      pageInfo: { hasNextPage, endCursor } 
    }
  } = queryResponse;
  return { nodes, hasNextPage, endCursor };
};

// The custom information we pass to properly fetch data for each 'post type' or content type, the result of which is the object that Next.js needs to produce an accurate static HTML export of our site
const typeParams = [
  {
    pageComponent: "page",
    contentType: "pages",
    query: pagesQuery,
    urlBase: "pages",
    perPage: 100,
    endpoint,
    parseQueryResults // Passing our function as a parameter
  },
  {
    pageComponent: "product",
    contentType: "products",
    query: productsQuery,
    urlBase: "products",
    perPage: 100,
    endpoint,
    parseQueryResults
  },
  {
    pageComponent: "article",
    contentType: "posts",
    query: postsQuery,
    urlBase: "articles",
    perPage: 100,
    endpoint,
    parseQueryResults
  }
];

module.exports = {
  typeParams
};

And that’s all there is to adapting this module to a different schema than the one natively expected. Feel free to reach out with any questions.