Tags: Headless, integration, Next.js, Shifter, Tutorial

How to create a static Headless WordPress site using Next.js

Next.js is an application framework using React, developed and released as an Open Source Software (OSS) mainly by Vercel, Inc, a hosting company.

There are many examples, such as Hulu, NIKE, Netflix, and many others, and it is one of the most frequently chosen frameworks for publishing websites using React.

In this article, I’ll show you how to build a front-end for your WordPress site using this Next.js.

Final Image

It is difficult to show all the source code. So we’ve provided the source code of the sample website on GitHub.

https://github.com/getshifter/headless-example-nextjs-typescript

If you build using the data from above, you will have a website that looks like this.

Building a Static Site using Next.js

There are two ways to build a website using Next.js.

  • Static Generation: Generate static HTML at build time
  • Server-side Rendering: Generate HTML on Node.js

Today, we will create a static site using Static Generation, which can be published by Netlify, AWS Amplify and others.

Both have their advantages and disadvantages. More advanced configurations such as Incremental Static Regeneration and serverless mode are also possible, but let’s start with the simplest method.

Next.js Application setup

To set up your application in Next.js, use the create-next-app command.

$ npx create-next-app
# or
$ yarn create next-app

If you want to use the sample application introduced in the introduction, you can also specify the repository name as follows

$ npx create-next-app YOUR_APP_NAME --example "https://github.com/getshifter/headless-example-nextjs-typescript/tree/main"

[Appendix] Using TypeScript

By using TypeScript, you can avoid accidents such as the accidental use of non-existent values. There are also development advantages, such as the ability to easily examine the values of objects and arguments on the IDE.

Usually, you can prepare to use TypeSciprt by using the following command.

$ touch tsconfig.json
$ npm install --save-dev typescript @types/react @types/node

However, in addition to this procedure, it takes a little bit of work to rewrite the existing files into TypeScript, so you can also use the Starter I have published.

$ npx create-next-app YOUR_APP_NAME --example "https://github.com/wpkyoto/nextjs-starter-typescript/tree/main"

Displaying the list of WordPress posts on the top page

Next.js creates templates for pages to be published under the pages directory. For example, pages/index.js (or pages/index.[jsx|ts|tsx]) allows you to describe the content to be displayed on your site’s top page.

If you want to display the list of WordPress posts on the top page, you need to write the code to get and display the WP API data in this file.

Utilizing swr to retrieve the data

There are many libraries to get data from external APIs, such as Axios, superagent, fetch polyfil, etc. But here we will use a library called swr, which is released by the same company and team as Next.js.

$ npm install --save swr isomorphic-unfetch

Implement pages/index.js

The following code is an implementation for a simple article listing.

import useSWR from 'swr'
import Head from 'next/head'

export const Home = () => {
  const {data: posts} = useSWR(
      'https://YOUR_SITE_URL/wp-json/wp/v2/posts'
  )
  return (
    <div>
      <Head>
        <title>Create Next App</title>
      </Head>
      {posts.map(post => {
          return (
              <article key={post.id}>
                  <h2 dangerouslySetInnerHTML={{__html: post.title.rendered}} />
                  <div dangerouslySetInnerHTML={{__html: post.excerpt.rendered}} />
              </article>
          )
      })}
    </div>
  )
}

export default Home

We use a React Hook called useSWR provided by swr to obtain data from the WP API. After that, we use the acquired data to define the HTML for each article.

[For production] What if I need categories, thumbnails, author information, etc.? Add an _embed query.

By default, when you get the article information from the WP API, the article’s category tags and thumbnail and author information are excluded from the response.

If you want to take advantage of these data, be sure to add an _embed query to your API request.

Make the article list to be displayed static

If it is a general React application, the list of articles is completed in the previous step. However, if you are creating a static site in Next.js, you can make it even faster by adding one more step.

When statifying a site in Next.js, you can use the getStaticProps function to get the article data when the site is built.

import useSWR from 'swr'
import Head from 'next/head'
import unfetch from 'isomorphic-unfetch'

const fetcher = url => unfetch(url).then(r => r.json())
const url =  'https://YOUR_SITE_URL/wp-json/wp/v2/posts'

export const Home = ({posts: initialPosts}) => {
  const {data: posts} = useSWR(
     url,
      fetcher,
      initialPosts
  )
  return (...)
}

export const getStaticProps = async () => {
     const posts = await fetcher(url)
     return {
       props: { posts }
     }
}

export default Home

As seen above, some changes were added.

By calling the WP API in getStaticProps, we get the list of articles at build time. After that, we put the obtained value in the third argument of useSWR. By doing so, the list of articles obtained at build time will be displayed until the frontend completes the process of retrieving the list of articles again.

In this way, in addition to the behaviors of a normal React application, you can use Next.js to pre-acquire data at build time.

Set the link to the article details

Finally, let’s set up a link to the detailed page of the displayed article.

import useSWR from 'swr'
import Head from 'next/head'
import Link from 'next/link'

export const Home = () => {
      ...
      {posts.map(post => {
          return (
              <article key={post.id}>
                <Link href={`/${post.slug}`}>
                  <h2 dangerouslySetInnerHTML={{__html: post.title.rendered}} />
                </Link>
                  <div dangerouslySetInnerHTML={{__html: post.excerpt.rendered}} />
            ...

We implemented the h2 tag with a Link tag. Now when you click on the article title, you will be taken to example.com/{THE_POST_SLUG}. If you have set the permalink to post_id, change post.slug to post.id.

Create a separate article page

We have created links to individual article pages, but the main part of the article details page is not ready yet. From here, I’ll show you how to create an article detail page as a static site in Next.js.

To process pages that are generated dynamically in a different place than Next.js, such as WordPress posts, use the Dynamic Routing function. Create pages/[slug].js if you have permalinks set, set it to pages/[id].js. You can now use XXX in [XXX] as an argument to getStaticProps.

import useSWR from 'swr'
import Head from 'next/head'
import unfetch from 'isomorphic-unfetch'

const fetcher = url => unfetch(url).then(r => r.json())
const url =  'https://YOUR_SITE_URL/wp-json/wp/v2/posts'

export const Home = ({post: initialPost, slug}) => {
    const {data: post} = useSWR(
        [url, `?slug=${slug}`].join('/'),
        fetcher,
        initialPost
    )
  return (
    <div>
      <Head>
        <title>{post.title.rendered}</title>
      </Head>
      <article key={post.id}>
          <h2 dangerouslySetInnerHTML={{__html: post.title.rendered}} />
          <div dangerouslySetInnerHTML={{__html: post.content.rendered}} />
      </article>
    </div>
  )
}

export default Home


export const getStaticPaths = async () => {
    const posts = await fetcher(url)
    return {
        paths: posts.map(post => ({
            params: {
                id: post.id,
                slug: decodeURI(post.slug)
            }
        })),
        fallback: false
    }
}

export const getStaticProps = async ({params}) => {
    if (!params) {
        return {
            props: {
                post: null,
            }
        }
    }
    const slug = typeof params.slug === 'string' ? params.slug : params.slug[0]
    const posts = await fetcher([url, `?slug=${slug}`].join('/'))
    return {
        props: {
            slug,
            post: Array.isArray(posts) ? posts[0] : posts
        }
    }
}

We use slug in getStatisProps to retrieve the article from the WP API.

While using the values retrieved by React, the part where the data is re-fetched and the part where the data is displayed is almost the same as the article list.

There is a new function called getStaticPaths. This function is for getting the list of paths that need to be generated statically in Next.js.

Since the page with the path specified by this function will be generated as static HTML, we get the data of the article we want to generate from the WP API and convert it to the required form and pass it on.

[For production] getStaticPaths allows you to make recursive calls to the WP API

In our sample, which we have described as a simple example, this implementation only generates 10 articles’ worth of data. This depends on the default value of the number of articles returned by the WP API.

In practice, we need to process hundreds or more articles. Therefore, we need to call the WP API recursively to get the data of all the articles, as shown in the following sample.

// via: https://github.com/getshifter/headless-example-nextjs-typescript/blob/3e285bb2c0378c0df035a1e0e98a67cfb42b668b/pages/%5Bslug%5D.tsx#L22-L49

export const listAllPosts = async (APIURLBuilder: WPAPIURLBuilder, posts: WPPost[] = []): Promise<WPPost[]> => {
    const perPage = 20
    try {
        const url = APIURLBuilder.perPage(20).getURL()
        const response = await fetch(url)
        if (
            response instanceof Error ||
            (
                response.data &&
                response.data.status &&
                response.data.status > 399
            )
            ) {
            throw response
        }
        const mergedPosts = uniqWPPosts([...posts,...response])
        if (canUseServerSideFeatures() || response.length < perPage) {
            return mergedPosts
        }
        APIURLBuilder.nextPage()
        return listAllPosts(APIURLBuilder, mergedPosts)
    } catch (e) {
        if (e.code && e.code === 'rest_invalid_param') {
            return posts
        }
        throw e   
    }
}

Testing in the local environment

Once you’re ready, it’s time to see if it actually works locally.

To run the Next.js app locally, use the npm run dev or next dev command.

Testing the site to be static

The actual testing to make the site static is done with a combination of the next build command and the next export command.

...
"scripts": {
    "build:static": "next build && next export", 
    "dev": "next dev",
....

Let’s run the npx serve out command to access the displayed URL for the display of the page after it has been statified.

[For development] Turn on ISG mode during development

When implementing a frontend in Next.js, one of the most common bottlenecks is a page that uses getStatcsPaths. This page may be implemented to get all or close to all of the data, such as WordPress posts, in this page.


export const getStaticPaths = async () => {
    const posts = await getAllWPPost()
    return {
        paths: posts.map(post => ({
            params: {
                id: post.id,
                slug: decodeURI(post.slug)
            }
        })),
        fallback: false
    }
}

However, in this case, every time you visit a page that uses this implementation, getStaticPaths will be executed, leading to a delay in page display and load on the API server.

This is where the Next.js feature called ISG (Incremental Static Generation) comes in. This feature is not available for building static sites, but you can use it only for local development.

const isProd = process.env.NODE_ENV === 'production'

export const getStaticPaths = async () => {
    const posts = await isProd ? getAllWPPost(): getWPPost({ limit: 10 })
    return {
        paths: posts.map(post => ({
            params: {
                id: post.id,
                slug: decodeURI(post.slug)
            }
        })),
        fallback: isProd ? false: 'blocking'
    }
}

This sample code uses the NODE_ENV environment variable. This is the value that will be set to development for next dev and production for next build, so you can use it to determine if you are in development mode or not.

In this case, we use the HTML generated by the next build and then the next export, so we make it behave as a static site at the time of the next build.

And in the non-production state, getStaticPaths suppresses the number of articles retrieved by getStaticPaths. This is because by setting the return value fallback to blocking or true, the page will be automatically generated by ISG mode, even if you visit a page that has not been generated before.

By implementing it in this way, you can focus on front-end development without making unnecessary API calls or waiting for their processing.

Deploying a Site to Netlify

If you have successfully tested and built your site locally, the final step is to deploy it to Netlify and publish it.

After committing these changes in Git, you can push the changes to your Git repository service, such as GitHub or GitLab.

$ git add .
$ git commit -m "feat: ready for publish"
$ git push origin main 
or
$ git push origin master

If you want to build with Netlify or any other hosting service, configure it as follows

  • Build command: npm run export or yarn export
  • Publish directory: out

Conclusion

Compared to Gatsby, Next.js requires you to write a certain amount of React and JavaScript code. For example, you need to write the data acquisition process yourself.

However, I think the benefits are the ability to use and operate it with a high degree of flexibility, such as making it behave as a dynamic application only under certain conditions.

In addition to this example, we would like to introduce various ways to use this application, such as Serverless Nextjs Plugin using AWS, dynamic processing using Vercel, and E-Commerce, in addition to this example, in future blog posts.