🚀

AstroとWordPressでHeadless CMSブログを構築する

に公開

WordPressをHeadless CMS(ヘッドレスCMS:フロントエンドとバックエンドを分離したCMS)として活用し、Astroフレームワークでブログを構築します。

技術選定の理由

Astroを選んだ理由

Next.jsやNuxt.jsでも静的サイトを生成できますが、これらはアプリケーション開発に最適化されたフレームワークです。シンプルなサイト制作には少しオーバースペックです。

Astroの特徴:

  • シンプルな構成で学習コストが低い
  • 必要に応じてReactやVueなどのUIライブラリも使用可能
  • 静的HTMLとして出力され、高速な表示が可能(インタラクティブなコンポーネント以外)

WordPressを選んだ理由

  • 世界中で高いシェア率を持つCMS
  • 多くのユーザーが慣れ親しんだUI
  • REST APIによるHeadless CMSとしての利用が可能

実装手順

1. WordPress環境の構築

「Local」を使用して簡単にWordPress環境を構築します。

Local: https://localwp.com/

ガイドに従って進めると、以下のような管理画面が表示されます。

2. Astroのセットアップ

公式ドキュメント: https://docs.astro.build/ja/install/auto/

パスエイリアスの設定(オプション):

// tsconfig.json
{
  "extends": "astro/tsconfigs/strict",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@src/*": ["src/*"]
    }
  }
}

環境変数の設定:

# .env
API_URL=http://{your-domain}/wp-json/wp/v2

使用例:

import.meta.env.API_URL

3. 投稿一覧の取得と表示

エンドポイント: /wp-json/wp/v2/posts

Astroではトップレベルでawaitを使用できるため、非同期処理をシンプルに記述できます。

index.astro
---
import Layout from '@src/layouts/Layout.astro';

type Post = {
  link: string;
  title: {
    rendered: string;
  };
};

const res = await fetch(`${import.meta.env.API_URL}/posts`);
const posts = await res.json() as Post[];
---

<Layout title="投稿一覧">
  <main>
    <ul>
      {posts.map((post) => (
        <li>
          <a href={post.link} set:html={post.title.rendered} />
        </li>
      ))}
    </ul>
  </main>
</Layout>

4. 投稿詳細ページの表示

エンドポイント: /wp-json/wp/v2/posts/{id}

ダイナミックルーティング(動的ルーティング)を使用するため、getStaticPaths関数で事前にパス情報を取得します。

pages/posts/[id].astro
---
import Layout from '@src/layouts/Layout.astro';

type Post = {
  link: string;
  title: {
    rendered: string;
  };
  content: {
    rendered: string;
  }
};

export async function getStaticPaths() {
  const res = await fetch(`${import.meta.env.API_URL}/posts`);
  const posts = await res.json() as { id: number }[];

  return posts.map(post => (
    { params: { id: post.id.toString() } }
  ))
}

const { id } = Astro.params;

const res = await fetch(`${import.meta.env.API_URL}/posts/${id}`);
const post = await res.json() as Post;
---

<Layout title={post.title.rendered}>
  <main>
    <h1 set:html={post.title.rendered} />
    <p set:html={post.content.rendered} />
  </main>
</Layout>

5. カスタム投稿の取得

WordPressのカスタム投稿タイプ(独自の投稿タイプ)もREST API経由で取得できます。

エンドポイント: /wp-json/wp/v2/{custom_post_type}

お知らせカスタム投稿(news)の例:

news/index.astro
---
import Layout from '@src/layouts/Layout.astro';

type News = {
  id: number;
  title: {
    rendered: string;
  };
};

const res = await fetch(`${import.meta.env.API_URL}/news`);
const news = await res.json() as News[];
---

<Layout title="お知らせ一覧">
  <main>
    <ul>
      {news.map((item) => (
        <li>
          <a href={`/news/${item.id}`} set:html={item.title.rendered} />
        </li>
      ))}
    </ul>
  </main>
</Layout>

ページネーション実装

先の投稿一覧ページではページネーションがありませんでした。実は投稿エンドポイントのヘッダーにページネーション情報が含まれています。

x-wp-totalpages

これを使用してページネーションを実装してみます。

pages/posts/index.astro
---
import Layout from '@src/layouts/Layout.astro';
import Paginate from '@src/components/Paginate.astro';

type Post = {
  id: number;
  title: {
    rendered: string;
  };
};

export async function getStaticPaths() {
  const res = await fetch(`${import.meta.env.API_URL}/posts`);
  const totalCount = res.headers.get('x-wp-totalpages') as string;
  const pages: number[] = Array(+totalCount).fill(null).map((_, i) => i + 1);

  return pages.map(page => (
    { params: { id: page.toString() } }
  ))
}

const { id } = Astro.params;

const res = await fetch(`${import.meta.env.API_URL}/posts?page=${id}`);
const posts = await res.json() as Post[];
const totalCount = res.headers.get('x-wp-totalpages') as string;
const pages: number[] = Array(+totalCount).fill(null).map((_, i) => i + 1);
---

<Layout title={`投稿一覧 | ${id}ページ目`}>
  <main>
    <ul>
      {posts.map((post) => (
        <li>
          <a href={`/posts/${post.id}`} set:html={post.title.rendered} />
        </li>
      ))}
    </ul>
    <Paginate pages={pages} currentPage={id} />
  </main>
</Layout>
components/Paginate.astro
---
const { pages, currentPage } = Astro.props as { pages: number[], currentPage: string };
---

<style>
  nav {
    display: flex;
    justify-content: center;
  }

  ul {
    display: flex;
    list-style: none;
    padding: 0;
  }

  li {
    margin: 0 0.5rem;
  }

  a {
    display: grid;
    place-items: center;
    width: 2em;
    height: 2em;
    text-decoration: none;
    color: #333;
  }

  .is-active {
    pointer-events: none;
    background-color: #333;
    color: #fff;
  }
</style>

<nav>
  <ul>
    {pages.map((page) => (
      <li>
        <a class:list={[{ 'is-active': page === +currentPage }]} href={`/posts/page/${page}`}>{page.toString()}</a>
      </li>
    ))}
  </ul>
</nav>

ビルドしてファイルを見てみる

ここまでできたらビルドしてみましょう。jsがなく、静的サイトとして出力されていることがわかります。

npm run build

ここからは余談

フォームプラグインとの連携

WordPressのフォームプラグインとしてContactForm7が有名ですが、実はRest APIを使用してフォームの送信ができます。
以下の記事が参考になります。
https://qiita.com/gungungggun/items/d5736649529e60db50ac

さらにSEOプラグインとしてYoast SEOが有名ですが、こちらもRest APIを使用してSEO情報を取得できます。
https://developer.yoast.com/customization/apis/rest-api/

使用するプラグインによりますが、Rest APIが提供されている場合はAstroからも使用できます。

さいごに

ここまでできたら、あとは自由にカスタマイズしていきましょう。そのままastro記法を使用しても良いですし、ReactやVueなどの記法を使うことも使用できます。WordPressのプラグインを活かしてAstroに表示させることもできます。
ビルドされたファイルは静的なHTMLファイルとして出力されるため、高速なサイト表示ができます。

今回は機能としてほんの一握りを紹介しましたが、より多くの機能がAstroにはあります。
ぜひ試してみてください。

参考

Astroの公式ドキュメントは以下を参照にしてください。
https://docs.astro.build/en/getting-started/

基本的なWordPress Rest Apiのエンドポイントは以下を参照にしてください。
https://developer.wordpress.org/rest-api/reference/

Discussion