🦊

Docker・Next.js・WordPressでヘッドレスCMS

2025/01/28に公開

はじめに

初めてZenn記事を投稿します。
こちらの記事を参考に同じように作成していきました。
ここまで詳しく分かりやすい記事がなかったのでほんとにありがたかったです。
WordPressからのデータ取得は別の方法で書いてます。
記録用です。
https://zenn.dev/fbd_tech/books/519201590c4e98
DockerでWordPressを作成していく。

ディレクトリ構造

next-wordpress/
 ├── wp/
     └── compose.yml

WordPressの設置

先にDockerを起動。
next-wordpressという名前の空のフォルダを作成。

next-wordpress/wp/compose.yml
services:
  db:
    image: mariadb:latest
    volumes:
      - db_data:/var/lib/mysql
    restart: always
    environment:
      - MYSQL_ROOT_PASSWORD=password
      - MYSQL_DATABASE=wordpress
      - MYSQL_USER=root
      - MYSQL_PASSWORD=password

  wordpress:
    depends_on:
      - db
    image: wordpress:latest
    volumes:
      # wordpressのファイルを一式マウント
      - ./wordpress:/var/www/html
    ports:
      - "8000:80"
    restart: always
    environment:
      - WORDPRESS_DB_HOST=db:3306
      - WORDPRESS_DB_USER=root
      - WORDPRESS_DB_PASSWORD=password
volumes:
  db_data:

wordpressコンテナのvolumesでマウントしないとローカルに反映されないので書いておく。
二つのコンテナのUSERとPASSWORDは一致させる。

compose.ymlがある階層で↓で、コンテナが作成される。
※たまに作成してすぐに接続するとエラーになることがあるので、少し時間をおいて接続してみる。

docker compose up -d

(http://localhost:8000/)
にアクセス、インストールしてトップページが表示されるようにする。

WordPressの新規テーマ作成

wpの下にwordpressフォルダが作成されるので、テーマを作成して3つのファイルを作成。

wordpress/
├── wp-content/
│ └── themes/
│   └── [独自テーマ名/] # 新規作成
│     ├── index.php # 新規作成(中は空でOK)
│     ├── style.css # 新規作成(中は空でOK)
│     └── functions.php # 新規作成(中は空でOK)
├── compose.yml

テーマを有効化。
他のデフォルトテーマは使用しないので削除。

WordPress初期設定

サムネイル有効化

functions.php
<?php
add_theme_support('post-thumbnails');

パーマリンク変更

設定>パーマリンク>投稿名

nvmのインストール

nodeは初めから入ってることとします。
nodeのバージョンを簡単に変更できるということなのでnvmをインストールしました。
ちょくちょくエラーが出てたのですが、nvmを使うことで簡単にnodeのバージョンを変更できてとても便利だと思いました。

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
source ~/.zshrc

Next.js作成

next-wordpress/
 ├── wp/
 │   ├── wordpress/
 │   └── compose.yml
 ├── front/ # これから作成します

インストール

nameはfront
TypeScriptはNo
TailWindCSSはYes
srcフォルダはYes
AppRouterはNo

next-wordpress/
yarn create next-app

eslintモジュールの依存関係が、現在使用しているNode.jsのバージョンと互換性がないので、nvmを使いnodeのバージョンを変更。
現在のNode.jsバージョンがv19.0.0だったので、

# 必要なバージョンをインストール
nvm install 18.18.0
# 必要なバージョンに切り替え
nvm use 18.18.0

frontフォルダに移動

cd front

各パッケージをインストール

yarn add next react react-dom

アプリを立ち上げる

yarn dev

http://localhost:3000/
にアクセス。

ヘッダーとフッターをコンポーネント化

フォルダ構成

src/
├── components/
│   ├── Header.jsx
│   └── Footer.jsx
├── pages/
│   └── posts/
│       ├── [id].jsx
│       └── index.jsx
├── styles/
│   └── globals.css
.env.local

globals.css

globals.css
@import url("https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@300;400;700&display=swap");

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  *,
  *::before,
  *::after {
    @apply box-border;
  }
  body {
    @apply font-body;
  }
}

.env.local

環境変数を定義するためのもの。

.env.local
NEXT_PUBLIC_WP_ENDPOINT=http://localhost:8088/wp-json/wp/v2/posts

ヘッダー

ヘッダーとフッターを共通化する。

Header.jsx
export const Header = () => {
  return (
    <header>
      <h2>ヘッダーコンポーネント</h2>
    </header>
  );
};

フッター

Footer.jsx
export const Footer = () => {
  return (
    <footer>
      <p>フッターコンポーネント</p>
    </footer>
  );
};

WordPressのデータを取得

fetchでWP REST APIのURLからデータを取得。

記事一覧ページ

pages/posts/index.jsx
import { Footer } from "@/components/Footer";
import { Header } from "@/components/Header";
import Link from "next/link";

// 動的パスの生成(全投稿を取得)
export async function getStaticProps() {
  const res = await fetch(`${process.env.NEXT_PUBLIC_WP_ENDPOINT}?_embed`);
  const posts = await res.json();

  return {
    props: {
      posts, // 投稿データをpropsとして渡す
    },
    revalidate: 10, // 一定間隔を超えた時点でページを再生成する
  };
}

// 投稿一覧ページ
const Posts = ({ posts }) => {
  return (
    <>
      <Header />
      <ul className="w-[95%] mx-auto py-8">
        {posts.map((post) => {
          return (
            <>
              {/* 記事一覧 */}
              <li className="border-t first:border-b-0 border-b py-8 border-gray-300">
                <Link
                  href={`/posts/${post.id}`}
                  passHref
                  key={post.id}
                  className="hover:opacity-50"
                >
                  <p className="md:text-[16px] pt-[8px]">
                    {post.title.rendered}
                  </p>
                </Link>
              </li>
            </>
          );
        })}
      </ul>
      <Footer />
    </>
  );
};
export default Posts;

http://localhost:3000/posts
にアクセス。

const res = await fetch(${process.env.NEXT_PUBLIC_WP_ENDPOINT}?_embed);
の部分は.env.localファイルに環境変数として保存し呼びだす。

https://[ドメイン名]/wp-json/wp/v2/posts
この/wp-json/wp/v2/postsを指定するだけでWordPressから情報を取得することができる。

/v2/postsの後に?_embedを入れることによって、追加の情報(投稿に関連する画像や著者情報など)も含まれるようになる。

propsとして投稿一覧ページにpostsを渡し記事をmapさせ取り出していく。

記事詳細ページ

pages/posts/[id].jsx
import { Header } from "@/components/Header";
import { Footer } from "@/components/Footer";

// URL(パス)を指定する
export async function getStaticPaths() {
  const res = await fetch(process.env.NEXT_PUBLIC_WP_ENDPOINT);
  const posts = await res.json();

  // 各投稿のIDをパラメータとして渡す
  const paths = posts.map((post) => ({
    params: { id: post.id.toString() }, // idを文字列に変換して渡す
  }));

  return {
    paths,
    fallback: false, // 追加されたIDに対して404を返す
  };
}

// データを取得して渡す
export async function getStaticProps({ params }) {
  const postRes = await fetch(
    `${process.env.NEXT_PUBLIC_WP_ENDPOINT}/${params.id}?_embed`
  );
  const post = await postRes.json();

  return {
    props: {
      post, // 投稿データをpropsとして渡す
    },
    revalidate: 10, // 一定間隔を超えた時点でページを再生成する
  };
}

// 記事が存在しない場合の処理
const PostDetail = ({ post }) => {
  if (!post) {
    return <div>記事がありません。</div>;
  }

  return (
    <>
      <Header />
      <div className="mt-8 border-t pt-8 border-gray-300 max-w-3xl px-4 pt-6 lg:pt-10 pb-12 sm:px-6 lg:px-8 mx-auto">
        <div className="max-w-2xl">
          <div className="grow mb-6 mt-8">
            <h2 className="text-2xl text-center mb-8 font-bold md:text-3xl dark:text-white">
              {post.title.rendered}
            </h2>
            <div>
              投稿日:{" "}
              {`${new Date(post.date).getFullYear()}.${String(
                new Date(post.date).getMonth() + 1
              ).padStart(2, "0")}.${String(
                new Date(post.date).getDate()
              ).padStart(2, "0")}`}
            </div>
          </div>
          <main>
            {/* 本文 */}
            <blockquote className="p-6 sm:px-7">
              <div className="text-left leading-7">
                <div
                  dangerouslySetInnerHTML={{
                    __html: post.content.rendered,
                  }}
                ></div>
              </div>
            </blockquote>
          </main>
        </div>
      </div>
      <Footer />
    </>
  );
};

export default PostDetail;

getStaticPathsでURL(パス)を指定し、getStaticPropsparamsを受け取りpropsとして渡す。

getStaticPropsとは

ビルド時にgetStaticPropsを使用してページの事前レンダリングを行う。
ビルド時にデータの取得を行うので、ブログ更新などリアルタイム性のあるコンテンツには適しておらず、更新の必要があるたびに再ビルドが必要。
簡単な方法としては、revalidateオプションを使用する。

  • getStaticPropsはサーバーサイドで実行される
  • pagesフォルダのみで利用できる
  • propsを返さないといけない
  • propsで返す値もjsオブジェクトでなければならない(res.json()でjsオブジェクトに変換しているのはそのため)
  • サーバーサイドで動くので、ログを確認するときはブラウザではなくターミナルを確認する。

Discussion