Gutenberg is the default editor for WordPress. The editor lets you craft and style content using discrete blocks for text, images, video, and other site elements through a drag-and-drop interface. This approach enhances WordPress’s flexibility and design capabilities.

This guide explains how to parse Gutenberg content as HTML using the WordPress REST API in a Next.js static site.

Prerequisites

To follow along, you need:

Fetch Gutenberg content using a REST API

To interact with your WordPress site programmatically and retrieve content structured in Gutenberg blocks, you use the WordPress REST API or the WPGraphQL plugin. These tools enable you to fetch your WordPress content in JSON format.

To enable JSON data access via the REST API, adjust your WordPress permalink settings away from “Plain.” This allows API access through a structured URL, as follows:

https://yoursite.com/wp-json/wp/v2

By making API requests to this URL, you can programmatically retrieve various information and perform operations on your WordPress site. For example, you can fetch a list of posts by sending a GET request to:

https://yoursite.com/wp-json/wp/v2/posts

This will return a JSON object containing information about the posts on your WordPress site, including titles, content, author details, and more.

Parse Gutenberg blocks as HTML

When retrieving posts from a WordPress site that uses the Gutenberg editor, the stored content in the database can feature a blend of HTML and JSON metadata to describe various block types, such as quotes and galleries. For instance:

<!-- wp:quote {"className":"inspirational-quote","style":{"typography":{"fontSize":"large"}}} -->
<blockquote class="wp-block-quote inspirational-quote has-large-font-size"><p>“The journey of a thousand miles begins with one step.”</p><cite>Lao Tzu</cite></blockquote>
<!-- /wp:quote -->

<!-- wp:gallery {"ids":[34,35],"columns":2,"linkTo":"none","sizeSlug":"medium","className":"custom-gallery"} -->
<ul class="wp-block-gallery columns-2 is-cropped custom-gallery"><li class="blocks-gallery-item"><figure><img src="http://example.com/wp-content/uploads/2021/09/image1-300x200.jpg" alt="A breathtaking view of the mountains" class="wp-image-34"/></figure></li><li class="blocks-gallery-item"><figure><img src="http://example.com/wp-content/uploads/2021/09/image2-300x200.jpg" alt="Serene lakeside at dawn" class="wp-image-35"/></figure></li></ul>
<!-- /wp:gallery -->

This snippet illustrates two Gutenberg blocks: a Quote and a Gallery. Each is augmented with JSON metadata encapsulated within HTML comments. The metadata defines attributes such as class names, styles, and other configurations relevant to the block’s presentation.

When you fetch these blocks through the WordPress REST API or WPGraphQL, WordPress processes them, transforming the combination of HTML and JSON metadata into fully rendered HTML elements that you can directly incorporate into web pages. The transformed HTML for the above blocks would appear as follows:

<blockquote class="wp-block-quote inspirational-quote has-large-font-size"><p>“The journey of a thousand miles begins with one step.”</p><cite>Lao Tzu</cite></blockquote>

<ul class="wp-block-gallery columns-2 is-cropped custom-gallery">
  <li class="blocks-gallery-item"><figure><img loading="lazy" src="http://example.com/wp-content/uploads/2021/09/image1-300x200.jpg" alt="A breathtaking view of the mountains" class="wp-image-34" sizes="(max-width: 300px) 100vw, 300px" /></figure></li>
  <li class="blocks-gallery-item"><figure><img loading="lazy" src="http://example.com/wp-content/uploads/2021/09/image2-300x200.jpg" alt="Serene lakeside at dawn" class="wp-image-35" sizes="(max-width: 300px) 100vw, 300px" /></figure></li>
</ul>

For developers building decoupled or headless applications using JavaScript frameworks like Next.js, this presents a straightforward method to display content by directly injecting the HTML into the page using the dangerouslySetInnerHTML property to render the markup.

<div dangerouslySetInnerHTML={{ __html: <raw_html_string> }} />

Additionally, you might need to perform further formatting for elements such as links and handle excess newline characters (\n), which this guide explains later.

Parse Gutenberg blocks content into Next.js static site

In this section, let’s fetch WordPress content into a Next.js project and then parse the Gutenberg blocks as HTML.

  1. Start by setting up a function to fetch posts from your WordPress site. Open the src/page.js file in your project and replace its content with the following code snippet:
    const getWpPosts = async () => {
    	const res = await fetch('https://yoursite.com/wp-json/wp/v2/posts');
      	const posts = await res.json();
    	return posts;
    };

    This asynchronous function performs an API request to the WordPress REST API. It fetches all the posts available on your site and returns them as an array.

  2. Next, let’s utilize the fetched posts within a simple Next.js page component by logging the posts to the console and rendering a basic greeting:
    const page = async () => {
      const posts = await getWpPosts();
      console.log(posts);
      
      return (
        <div>
          <h1>Hello World</h1>
        </div>
      );
    };
    
    export default page;

    When you run your project using npm run dev, it displays the “Hello World” message and logs the fetched posts to the Terminal.

    [
      {
        "_links" : {
          "about" : [...],
          "author" : [...],
          "collection" : [...],
          "curies" : [...],
          "predecessor-version" : [...],
          "replies" : [...],
          "self" : [...],
          "version-history" : [...],
          "wp:attachment" : [...],
          "wp:term" : [...]
        },
        "author" : 1,
        "categories" : [...],
        "comment_status" : "open",
        "content" : {
          "protected" : false,
          "rendered" : "\n<p>Fire, a primal force, captivates with its <strong>flickering flames</strong>, evoking both awe and caution. Its <quote>dance</quote> symbolizes destruction and renewal, consuming the old to make way for the new. While it warms our homes and hearts, fire demands respect for its power to devastate.</p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"250\" height=\"148\" src=\"https://img.example.com/wp-content/uploads/2024/02/burningbuilding.jpg\" alt=\"\" class=\"wp-image-14\"/></figure>\n\n\n\n<p>In ancient times, fire was a beacon of light and warmth, essential for survival. Today, it remains a symbol of human ingenuity and danger. From the comforting glow of a hearth to the destructive fury of wildfires, fire’s dual nature reminds us of our fragile relationship with the elements.</p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https://img.example.com/premium-photo/painting-burning-building-illuminated-by-bright-flames-night_168058-249.jpg?w=1380\" alt=\"\"/></figure>\n\n\n\n<p>You can check out other articles on our blog:</p>\n\n\n\n<ul>\n<li><a href=\"https://yoursite.com/?p=6\">Lorem Ipsum: Beginnings</a></li>\n\n\n\n<li><a href=\"https://yoursite.com/?p=9\">Lorem Ipsum: Act 2</a></li>\n\n\n\n<li><a href=\"https://yoursite.com/?p=11\">Lorem Ipsum: Act 3</a></li>\n</ul>\n"
        },
        "date" : "2024-02-27T12:08:30",
        "date_gmt" : "2024-02-27T12:08:30",
        "excerpt" : {
          "protected" : false,
          "rendered" : "<p>Fire, a primal force, captivates with its flickering flames, evoking both awe and caution. Its dance symbolizes destruction and renewal, consuming the old to make way for the new. While it warms our homes and hearts, fire demands respect for its power to devastate. In ancient times, fire was a beacon of light and warmth, […]</p>\n"
        },
        "featured_media" : 0,
        "format" : "standard",
        "guid" : {
          "rendered" : "https://yoursite.com/?p=13"
        },
        "id" : 13,
        "link" : "https://yoursite.com/?p=13",
        "meta" : {
          "footnotes" : ""
        },
        "modified" : "2024-02-29T16:45:36",
        "modified_gmt" : "2024-02-29T16:45:36",
        "ping_status" : "open",
        "slug" : "fire-fire",
        "status" : "publish",
        "sticky" : false,
        "tags" : [],
        "template" : "",
        "title" : {
          "rendered" : "Fire"
        },
        "type" : "post"
       },
      },
      ...
    ]

    The JSON objects representing individual Gutenberg post data include various fields, among which the content and excerpt fields are returned as Gutenberg blocks parsed as HTML strings.

  3. To render this HTML content correctly in Next.js, we employ the dangerouslySetInnerHTML property:
    const page = async () => {
      const posts = await getWpPosts();
    
      return (
        <>
          <h1> Headless Blog </h1>
    
          <div>
            {posts.map((post) => (
              <Link href={'/blog/' + post.id} key={post.id}>
                <h2>
                  {post.title.rendered} <span>-></span>
                </h2>
                <div dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} />
              </Link>
            ))}
          </div>
        </>
      );
    };
    
    export default page;

    In this updated component, we map over the fetched posts array to generate a list of post excerpts. Each excerpt is wrapped in a Link component for navigation, displaying the post title and a snippet of its content.

    The dangerouslySetInnerHTML property is used to parse and render the HTML content contained within the excerpt.rendered field.

  4. Next, create a file blog/[id]/page.js within the app directory. You use folders to define routes. So, by creating a blog folder, you define the blog route. You combine this with dynamic routing to generate routes for each post.
  5. Each post has an ID. You use this ID to generate its unique route, /blog/{post_id}in your application. Add the following code:
    import Link from 'next/link';
    
    export async function generateStaticParams() {
        const res = await fetch('https://yoursite.com/wp-json/wp/v2/posts');
        const posts = await res.json();
        return posts.map((post) => {
            return {
                params: {
                    id: post.id.toString(),
                },
            };
        });
    }
    
    export async function getPost(id) {
        const response = await fetch('https://yoursite.com/wp-json/wp/v2/posts/' + id);
        const post = await response.json();
        return post;
    }

    The generateStaticParams() function statically generates routes at build-time based on the corresponding ID returned on each post. The getPost() function fetches Gutenberg data from the REST API for the post with a passed ID.

    An earlier section showed sample parsed Gutenberg data returned from the REST API for a post. For now, we are only concerned with the content.rendered field:

    [
      {
        ...
        "content": {
          "rendered" : "\n<p>Fire, a primal force, captivates with its <strong>flickering flames</strong>, evoking both awe and caution. Its <quote>dance</quote> symbolizes destruction and renewal, consuming the old to make way for the new. While it warms our homes and hearts, fire demands respect for its power to devastate.</p>\n\n\n\n<figure> class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"250\" height=\"148\" src=\"https://img.example.com/wp-content/uploads/2024/02/burningbuilding.jpg\" alt=\"\" class=\"wp-image-14\"/></figure>\n\n\n\n<p>In ancient times, fire was a beacon of light and warmth, essential for survival. Today, it remains a symbol of human ingenuity and danger. From the comforting glow of a hearth to the destructive fury of wildfires, fire’s dual nature reminds us of our fragile relationship with the elements.</p>\n\n\n\n<figure> class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https://img.example.com/premium-photo/painting-burning-building-illuminated-by-bright-flames-night_168058-249.jpg?w=1380\" alt=\"\"/></figure>\n\n\n\n<p>You can check out other articles on our blog:</p>\n\n\n\n<ul>\n<li><a> href=\"https://yoursite.com/?p=6\">Lorem Ipsum: Beginnings</a></li>\n\n\n\n<li><a> href=\"https://yoursite.com/?p=9\">Lorem Ipsum: Act 2</a></li>\n\n\n\n<li><a> href=\"https://yoursite.com/?p=11\">Lorem Ipsum: Act 3</a></li>\n</ul>\n"
        },
        ...
      }
    ]

    This field contains the post’s raw HTML. It can be rendered directly using the dangerouslySetInnerHTML property like this, <div dangerouslySetInnerHTML={{ __html: <raw_html_string> }} />.

  6. Next, you can process the data by parsing internal links and resizing images. Install the html-react-parser package to simplify the process of parsing tags:
    npm install html-react-parser --save
  7. Add the following code to the blog/[id]/page.js file:
    import parse, { domToReact } from "html-react-parser";
    
    /*
     * We use a regular expression (pattern) to match the specific URL you want to replace.
     * The (\d+) part captures the numeric ID after ?p=.
     * Then, we use the replacement string 'data-internal-link="true" href="/blog/$1"',
     * where $1 is a placeholder for the captured ID.
     */
    export function fixInternalLinks(html_string) {
      const pattern = /href="https:\/\/yoursite.com\/\?p=(\d+)"/g;
      const replacement = 'data-internal-link="true" href="/blog/$1"';
    
      return html_string.replace(pattern, replacement);
    }
    
    export function parseHtml(html) {
      // Replace 2+ sequences of '\n' with a single '<br />' tag
      const _content = html.replace(/\n{2,}/g, '<br />');
      const content = fixInternalLinks(_content);
    
      const options = {
        replace: ({ name, attribs, children }) => {
          // Convert internal links to Next.js Link components.
          const isInternalLink =
            name === "a" && attribs["data-internal-link"] === "true";
    
          if (isInternalLink) {
            return (
              <Link href={attribs.href} {...attribs}>
                {domToReact(children, options)}
              </Link>
        	  );
          } else if (name === "img") {
            attribs["width"] = "250";
            attribs["height"] = "150";
            return (
              <img {...attribs}/>
            );
          }
        },
      };
    
      return parse(content, options);
    }

    The fixInternalLinks() function uses a regular expression to find links to posts in your WordPress site from the HTML string. In the raw HTML, you can see that the post contains a List tag with links to other posts on your site, replacing those links with internal links to routes in your static site.

    The parseHTML() function finds multiple sequences of excess newlines, nand replaces them with <br /> tags. It also finds internal links and converts the anchor tags into Link tags. Then, this function resizes images using tag attributes.

  8. To generate the main UI for each dynamic route, add the following code:
    export default async function Post({ params }) {
      const post = await getPost(params.id);
    
      const content = parseHtml(post.content.rendered);
    
      return (
        <>
          <h1>
            {post.title.rendered}
          </h1>
     	 
          <div>{content}</div>
        </>
      );
    }

    After parsing the raw HTML from the Gutenberg data, the code returns JSX representing the page’s formatted UI.

Finally, when you run your project, the home page will display a list of posts on your WordPress. Also, when you click individual posts, you will see the parsed Gutenberg contents rendered properly.

Deploy your Next.js static site on Kinsta

When combining headless WordPress with cutting-edge frameworks like Next.js, finding a cost-effective deployment solution becomes essential, especially when using a powerful WordPress Hosting like Kinsta’s for your WordPress site. Kinsta’s Static Site Hosting service offers a seamless and affordable way to bring your site online.

Kinsta allows you to host up to 100 static websites for free. First, push your code to a preferred Git provider (Bitbucket, GitHub, or GitLab). Once your repo is ready, follow these steps to deploy your static site to Kinsta:

  1. Log in or create an account to view your MyKinsta dashboard.
  2. Authorize Kinsta with your Git provider.
  3. Click Static Sites on the left sidebar, then click Add site.
  4. Select the repository and the branch from which you wish to deploy.
  5. Assign a unique name to your site.
  6. Add the build settings in the following format:
    • Build command: npm run build
    • Node version: 18.16.0
    • Publish directory: out
  7. Finally, click Create site.

And that’s it! You now have a deployed site within a few seconds. A link is provided to access the deployed version of your site. You can later add your custom domain and SSL certificate if you wish.

As an alternative to static site hosting, you can opt to deploy your static site with Kinsta’s Application Hosting service, which provides greater hosting flexibility, a wider range of benefits, and access to more robust features — like scalability, customized deployment using a Dockerfile, and comprehensive analytics encompassing real-time and historical data. You also don’t need to configure your Next.js project for static rendering.

Summary

This guide has explained how to integrate and parse Gutenberg block content effectively as HTML via the WordPress API. This makes rendering any type of content on your front end possible when you use headless WordPress.

You can host your headless WordPress on our managed WordPress Hosting service and deploy your static site to our Static Site Hosting service. This means everything about your site is in one dashboard: MyKinsta.

By choosing Kinsta, you benefit from a hosting provider that prioritizes optimal site performance and scalability while strongly fortifying websites with advanced security measures. Try Kinsta today!

What do you think about headless WordPress and its rendering? Have a better way to integrate Gutenberg blocks? Share your ideas in the comments section!

Jeremy Holcombe Kinsta

Content & Marketing Editor at Kinsta, WordPress Web Developer, and Content Writer. Outside of all things WordPress, I enjoy the beach, golf, and movies. I also have tall people problems ;).