GutenbergはWordPressのデフォルトエディターです。このエディターでは、テキスト、画像、動画、その他のサイト要素を個別のブロックを使って、ドラッグ&ドロップのインターフェースでコンテンツを作成し、スタイルを設定することができます。このアプローチにより、WordPressでの柔軟性とデザイン機能が便利なものになっています。

今回の記事では、Next.jsの静的サイトでWordPress REST APIを使用して GutenbergコンテンツをHTMLとして取得・表示する方法をご説明します。

前提条件

この説明に従うには、以下が必要です。

REST APIを使ってGutenbergのコンテンツを取得する

WordPressサイトとプログラムでやり取りし、Gutenbergブロックで構造化されたコンテンツを取得するには、WordPress REST APIまたはWPGraphQLプラグインを使用します。これらを使用すると、WordPress のコンテンツをJSON形式で取得できます。

REST API経由でのJSONデータアクセスを有効にするには、WordPressのパーマリンク設定をデフォルトの「基本」以外に調整します。これにより、以下のように構造化されたURLでAPIにアクセスできるようになります。

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

このURLにAPIリクエストを行うことで、WordPressサイトのさまざまな情報をプログラムを介して取得し、操作を実行することができます。例えば、GETリクエストを送信することで、投稿の一覧を取得できます。

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

すると、タイトル、コンテンツ、著者など、WordPressサイトの投稿に関する情報を含むJSONオブジェクトが返されます。

GutenbergブロックをHTMLとしてパースする

Gutenbergエディターを使用しているWordPressサイトから投稿を取得する場合、データベースに保存されたコンテンツには、引用やギャラリーなどのさまざまなブロックタイプを記述するための HTMLとJSONメタデータが混在していることがあります。例えば次の通りです。

<!-- 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 -->

このスニペットは、引用とギャラリーという2つのGutenbergブロックを示しています。それぞれ、HTMLコメント内にカプセル化され、JSONメタデータが補われています。このメタデータが、クラス名、スタイル、その他ブロックの表示に関連する設定などの属性を定義します。

WordPress REST APIまたはWPGraphQLを通してこれらのブロックを取得することで、HTMLとJSONメタデータの組み合わせを完全にレンダリングされたHTML要素に変換し、ウェブページに直接組み込むことができるようになります。上記のブロックを変換したHTMLは以下のように表示されます。

<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>

Next.jsのようなJavaScriptフレームワークを使用して非連結またはヘッドレスのアプリケーションを構築している開発者にとっては、dangerouslySetInnerHTMLプロパティを使用してHTMLをページに直接挿入し、マークアップをレンダリングすることでコンテンツを表示する簡単な方法となります。

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

リンクなどの要素に対してさらなるフォーマットを実行したり、余分な改行文字(\n)を処理したりする必要があるかもしれませんが、これについては後ほどご説明します。

GutenbergブロックのコンテンツをNext.js静的サイトにパースする

このセクションでは、WordPressのコンテンツをNext.jsプロジェクトに取り込み、GutenbergブロックをHTMLとしてパースしてみましょう。

  1. まず、WordPressサイトから投稿を取得する関数を設定します。プロジェクトのsrc/page.jsファイルを開き、その内容を次のコードで置き換えます。
    const getWpPosts = async () => {
    	const res = await fetch('https://yoursite.com/wp-json/wp/v2/posts');
      	const posts = await res.json();
    	return posts;
    };

    この非同期関数が、WordPress REST APIへのAPIリクエストを実行します。この非同期関数はWordPress REST APIへのAPIリクエストを実行し、サイトで利用可能なすべての投稿をフェッチして配列として返します。

  2. 次に、シンプルなNext.jsページコンポーネントの中で、取得した投稿をコンソールに記録し、基本的なメッセージをレンダリングしてみましょう。
    const page = async () => {
      const posts = await getWpPosts();
      console.log(posts);
      
      return (
        <div>
          <h1>Hello World</h1>
        </div>
      );
    };
    
    export default page;

    npm run devを使ってプロジェクトを実行すると、「Hello World」メッセージが表示され、取得した投稿がターミナルに記録されます。

    [
      {
        "_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"
       },
      },
      ...
    ]

    個々のGutenberg投稿データを表すJSONオブジェクトには、さまざまなフィールドが含まれますが、その中でもコンテンツと抜粋フィールドは、GutenbergブロックがHTML文字列にパースされたものとして返されます。

  3. このHTMLコンテンツをNext.jsでレンダリングするために、dangerouslySetInnerHTMLプロパティを使用します。
    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;

    このコンポーネントでは、フェッチされた投稿の配列にマッピングして、投稿の抜粋一覧を生成します。各抜粋は、ナビゲーションのためにLinkコンポーネントでラップされ、投稿のタイトルと内容のスニペットが表示されます。 dangerouslySetInnerHTMLプロパティは、excerpt.renderedフィールドに含まれるHTMLコンテンツを解析してレンダリングするのに使用されます。

  4. 次に、appディレクトリ内にblog/[id]/page.jsファイルを作成します。フォルダを使用してルートを定義します。つまり、blogフォルダを作成することで、blogルートを定義します。これを動的ルーティングと組み合わせて、各投稿のルートを生成します。
  5. 各投稿にはIDがあります。このIDを使って、アプリケーション内で固有のルート/blog/{post_id}を生成します。次のコードを追加してください。
    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;
    }

    generateStaticParams()関数は、各投稿で返された対応するIDに基づいて、ビルド時に静的にルートを生成します。getPost()関数は、渡されたIDを持つ投稿のGutenbergデータをREST APIから取得します。 先のセクションでは、REST APIから返された投稿のGutenbergデータをパースしたサンプルを示しました。ここでは、content.renderedフィールドにのみ注目します。

    [
      {
        ...
        "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"
        },
        ...
      }
    ]

    このフィールドには、投稿の生のHTMLが含まれています。dangerouslySetInnerHTMLプロパティを使用して、<div dangerouslySetInnerHTML={{ __html: <raw_html_string> }} />のように直接レンダリングできます。

  6. 次に、内部リンクを解析したり、画像のサイズを変更したりして、データを処理することができます。タグの解析プロセスを簡略化するために、html-react-parserパッケージをインストールしてください。
    npm install html-react-parser --save
  7. blog/[id]/page.jsファイルに以下のコードを追加します。
    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);
    }

    fixInternalLinks()関数が、正規表現を使ってHTML文字列からWordPressサイト内の投稿のリンクを特定します。生のHTMLからは、投稿内にListタグとサイト上の他の投稿への複数のリンクがあることがわかります。これらのリンクが静的サイト内のルートに合致した内部リンクに置き換えられます。 parseHTML()関数は、複数の余分な改行列nを見つけ、それを<br />タグに置き換えます。また、内部リンクを見つけ、アンカータグをLinkタグに変換します。そして、タグ属性を使用して画像のサイズを変更します。

  8. 各動的ルートのメインUIを生成するには、次のコードを追加します。
    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>
        </>
      );
    }

    Gutenbergデータから生のHTMLをパースした後、ページのフォーマットされたUIを表すJSXが返されます。

最後に、プロジェクトを実行すると、トップページにWordPressの投稿一覧が表示されます。また、個々の投稿をクリックすると、パース後のGutenbergのコンテンツが問題なくレンダリングされます。

Next.js静的サイトをKinstaにデプロイする

ヘッドレスWordPressとNext.jsのような最先端のフレームワークを組み合わせる場合には、KinstaのWordPressホスティングなどの高性能ソリューションを確保することが重要です。Kinstaの静的サイトホスティングサービスを使うことで、サイトを手軽に公開できます。さらにKinstaでは、100個までの静的ウェブサイト無料でホスティングすることができます。まず、お好みのGitサービス(BitbucketGitHub、またはGitLab)にコードをプッシュします。リポジトリの準備ができたら、以下の手順に従って静的サイトをKinstaにデプロイしてください。

  1. ログインするか、アカウントを作成してMyKinstaを表示
  2. 好みのGitサービスでKinstaを認証
  3. 左サイドバーの「静的サイト」から「サイトを追加」をクリック
  4. デプロイしたいリポジトリとブランチを選択
  5. サイトに一意の名前を割り当てる
  6. 以下のフォーマットでビルド設定を追加
    • ビルドコマンドnpm run build
    • Nodeのバージョン18.16.0
    • 公開ディレクトリout
  7. 最後に、「サイトを作成」をクリック

数秒でデプロイが完了します。これが完了すると、サイトにアクセスするためのリンクが表示されます。必要に応じて、独自ドメインSSL証明書を追加することもできます。

静的サイトホスティングの代わりとして、Kinstaのアプリケーションホスティングサービスで静的サイトをデプロイすることもできます。このサービスには、ホスティングの柔軟性、幅広いメリット、スケーラビリティ、Dockerファイルを使用したカスタムデプロイ、リアルタイムデータと履歴データを含む包括的な分析など、堅牢な機能が付帯します。また、静的レンダリングにNext.jsプロジェクトを設定する必要もありません。

まとめ

今回の記事では、WordPress APIを介してGutenbergブロックコンテンツをHTMLとして効率的に統合する方法をご説明しました。これにより、ヘッドレスWordPressを使用する際に、フロントエンドであらゆるタイプのコンテンツをレンダリングできるようになります。

Kinstaでは、例えば、ヘッドレスWordPressをWordPress専用マネージドホスティングでホストし、静的サイトを静的サイトホスティングでデプロイすることが可能です。これにより、サイトの全ての要素が1つのコントロールパネル「MyKinsta」で管理できます。

Kinstaを選択することで、サイトパフォーマンスとスケーラビリティを確保しながら、高度なセキュリティ強化策でウェブサイトを徹底的に守ることができます。その性能や機能をチェックするには、お気軽にKinstaをお試しください。

ヘッドレスWordPressという概念やレンダリングについてご意見はございますか?Gutenbergブロックを統合するその他の方法をご存知でしょうか?コメント欄でお聞かせください。

Jeremy Holcombe Kinsta

Kinstaのコンテンツ&マーケティングエディター、WordPress開発者、コンテンツライター。WordPress以外の趣味は、ビーチでのんびりすること、ゴルフ、映画。高身長が特徴。