RemixでmicroCMSを使ったブログサイトを構築してみた

ReactベースのフレームワークRemixで日本製ヘッドレスCMSのmicroCMSを使ってブログサイトを構築してみました
2022.01.28

はじめに

こんにちは、CX事業本部MAD事業部の森茂です。

RemixとヘッドレスCMSのmicroCMSを使って簡易的なブログサイトを構築を試してみました。

microCMSはmicroCMS社が開発、提供されている多機能ながら洗練されたUIでとても使いやすいヘッドレスCMSです。利用枠や機能に制限はありますが、利用料金が無料のHobbyプランも提供されており、基本的な機能は十分に試すことができます。

また、公式のSDKが提供されており少ない記述で手軽にAPIから情報を取得できます。SDKにはAPIの型情報も用意されており開発工数はかなり削減できそうです。

今回作成したソース

Next.jsやS3環境などでmicroCMSを利用する記事もありますのでこちらも参照ください

環境構築

技術スタック・サービス

  • Remix v1.1.3
  • microCMS + microcms-js-sdk v2.0.0
  • Tailwind CSS v3.0.17
  • Vercel

今回はSWR(stale-while-revalidate)を手軽に利用したいのでVercelにデプロイを前提として構築を行っていきます。またスタイリングにはTailwind CSSを使っていきます。RemixとSWRについては下記記事も参照ください。

Remixのインストール

今回はRemixをremix-microcms-exampleというディレクトリにインストールします。(執筆時のバージョンはv1.1.3)
なおデプロイ先にはVercelを選択しています。

$ npx create-remix@latest

Tailwindのインストール

スタイリングにはTailwind CSSを使います。今回はテキストまわりのスタイルで楽をするためにTypographyプラグインも合わせてセットアップします。

$ npm add -D concurrently tailwindcss @tailwindcss/typography

次にTailwind CSSの設定ファイルを用意します。

tailwind.config.js

module.exports = {
  content: ['./app/**/*.{ts,tsx,jsx,js}'],
  theme: {
    extend: {},
  },
  variants: {},
  plugins: [require('@tailwindcss/typography')],
};

package.jsonのビルド用スクリプト部分を変更してtailwindcssをwatchモードで動作できるようにします。

package.json

{
  // ...
  "scripts": {
    "build": "npm run build:css && remix build",
    "build:css": "tailwindcss -o ./app/tailwind.css",
    "dev": "concurrently \"npm run dev:css\" \"remix dev\"",
    "dev:css": "tailwindcss -o ./app/tailwind.css --watch",
    "postinstall": "remix setup node",
    "start": "remix-serve build"
  }
  // ...
}

最後にアプリケーションのroot.tsxファイルに生成されるcssファイルを読み込ませるように記載を追加します。

app/root.tsx

// ...
import styles from "./tailwind.css";

export function links() {
  return [{ rel: "stylesheet", href: styles }];
}

tailwind.cssファイルはclassNameを記載するたびに自動生成されるので.gitignoreファイルにも追記しておくとよいでしょう。(デプロイ先の設定によって.gitignoreファイルの内容が若干異なります)

.gitignore

node_modules

.cache
.vercel
.output

public/build
api/_build
/app/tailwind.css

ここで一度アプリケーションを起動して動作を試しておきます。

$ npm run dev

Tailwind CSSのリセットスタイルが反映されたページを確認できました。

microCMSのセットアップ

microCMSを利用するにあたりアカウント登録、サービス、API、コンテンツの作成を行っていきます。 公式のドキュメントにガイドがありますので下記を参考に作成してください。

今回は、リスト形式のAPIを作成し、APIのスキーマとしては下記のような形式としました。

  • サービスドメイン XXXX(XXXX.microcms.ioのXXXX部分)
  • エンドポイント blog
フィールドID 表示名 種類
title タイトル テキストフィールド
image 画像 画像
body 本文 リッチエディタ

microcms-js-sdk

micocms-js-sdkの利用のためにAPIキーとサービスドメイン名が必要になります。

  • サービスドメイン XXXX(XXXX.microcms.ioのXXXX部分)
  • エンドポイント blog
  • APIキー xxxxxxxxxxxxxxxxxxxxxxx

APIキーは下記箇所からも確認できます。

microcms-js-sdkをインストールします。またAPIキー、サービスドメイン部分は環境変数から読み込みを行うようにしたいため、dotenvライブラリもあわせてインストール、設定します。

$ npm install microcms-js-sdk
$ npm install -D dotenv

package.jsonファイルの起動スクリプト部分もあわせて変更します。

package.json

{
  // ...
  "scripts": {
    "build": "npm run build:css && remix build",
    "build:css": "tailwindcss -o ./app/tailwind.css",
    "dev": "concurrently \"npm run dev:css\" \"npm run dev:remix\"",
    "dev:remix": "node -r dotenv/config node_modules/.bin/remix dev",
    "dev:css": "tailwindcss -o ./app/tailwind.css --watch",
    "postinstall": "remix setup node",
    "start": "remix-serve build"
  }
  // ...
}

環境変数ファイル作成してサービスドメイン名とAPIキーを記載します。

.env

MICROCMS_SERVICE_DOMAIN=サービスドメイン名
MICROCMS_API_KEY=APIキー

事故防止のため忘れずに.gitignoreファイルにも追記しておきます。

.gitignore

...
.env

テスト用の投稿も2件ほど登録しておきます。

ここまででいったんmicroCMSのセットアップは完了です。

RemixからmicroCMSの情報を取得する

ひととおり準備ができたところでRemixからmicroCMSのコンテンツを取得する部分をつくっていきます。

microcms-js-sdk クライアント

microcms-js-sdkを使ったclientを用意します。Remixではサーバーサイドでのみ動作するスクリプトは明示的に*.server.tsとすることでクライアント側のビルドファイルから除外できます。環境変数などサーバーサイドでしか利用しないものは*.server.tsファイルで利用するのがよいでしょう。(*.client.tsとするとクライアントサイドのみ有効になりサーバーサイドのビルドファイルから除外できます)

app/lib/client.server.ts

import { createClient } from 'microcms-js-sdk';

export const client = createClient({
  serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN!,
  apiKey: process.env.MICROCMS_API_KEY!,
});

APIの型情報

microCMSのAPIから取得するコンテンツの型情報を用意します。SDKにはMicroCMSImageMicroCMSDateなどAPIの定形の型があらかじめ用意されているので自前の部分だけ用意すればよくとても便利です。

app/types/index.ts

import type { MicroCMSDate, MicroCMSImage } from 'microcms-js-sdk';

export type Content = {
  id: string;
  title: string;
  image: MicroCMSImage;
  body: string;
} & MicroCMSDate;

トップページ

トップページを書き換えていきます。
今回はデザインを作っていないのでTailwind CSSのプラグイン@tailwindcss/typographyを使ってスタイリングをお任せにしています。classNameにproseを入れるだけでよい感じにしてくれます:)

/app/routes/index.tsx

import { HeadersFunction, Link, LoaderFunction, useLoaderData } from 'remix';
import { client } from '~/lib/client.server';
import type { Content } from '~/types';

// stale-while-revalidateの設定
export const headers: HeadersFunction = () => {
  return {
    'Cache-Control': 'max-age=0, s-maxage=60, stale-while-revalidate=60',
  };
};

export const loader: LoaderFunction = async () => {
  // microcms-js-sdkを使って一覧を取得
  const { contents } = await client.getList<Content[]>({
    endpoint: 'blog',
  });
  return contents;
};

export default function Index() {
  const contents = useLoaderData<Content[]>();
  return (
    <div className="prose p-4">
      <h1>Index Page</h1>
      <ul>
        {contents.map((item) => (
          <li key={item.id}>
            <Link to={`/posts/${item.id}`}>{item.title}</Link>{' '}
            {new Date(item.createdAt).toLocaleString()}
          </li>
        ))}
      </ul>
    </div>
  );
}

詳細ページ

トップページの記事一覧からリンクする記事詳細ページを用意します。記事本文はリッチエディタを利用しているためHTMLタグが返ってきます。今回はHTMLタグをパースするためにhtml-react-parserを利用します。

$ npm install html-react-parser

app/routes/posts/$postId.tsx

import parse from 'html-react-parser';
import {
  HeadersFunction,
  json,
  LoaderFunction,
  MetaFunction,
  useLoaderData,
} from 'remix';
import { client } from '~/lib/client.server';
import type { Content } from '~/types';

// stale-while-revalidateの設定
export const headers: HeadersFunction = ({ loaderHeaders }) => {
  const cacheControl =
    loaderHeaders.get('Cache-Control') ??
    'max-age=0, s-maxage=60, stale-while-revalidate=60';
  return {
    'cache-control': cacheControl,
  };
};

// MetaFunctionにはLoaderFunctionで取得したdataの他にparamやlocationを受け取ることができる
// https://remix.run/docs/en/v1/api/conventions#page-context-in-meta-function
export const meta: MetaFunction = ({ data }: { data?: Content }) => {
  return { title: data?.title ?? 'Not Found' };
};

// microCMS APIから記事詳細を取得する
export const loader: LoaderFunction = async ({ params, request }) => {
  // 下書きの場合
  const url = new URL(request.url);
  const draftKey = url.searchParams.get('draftKey');

  const content = await client
    .get<Content>({
      endpoint: 'blog',
      contentId: params.postId,
      queries: { draftKey: draftKey ?? '' },
    })
    // 記事が404の場合は404ページへリダイレクト
    .catch(() => {
      throw new Response('Content Not Found.', {
        status: 404,
      });
    });

  // 下書きの場合キャッシュヘッダを変更
  const headers = draftKey ? { 'Cache-Control': 'no-store, max-age=0' } : undefined;

  return json(content, { headers });
};

export default function PostsId() {
  const content = useLoaderData<Content>();

  return (
    <div className="prose p-4">
      <h1>{content.title}</h1>
      <div>
        <img src={content.image.url} alt="" />
      </div>
      <div>{parse(content.body)}</div>
    </div>
  );
}

エラーハンドリング

最後にページが見つからない場合の404ページやエラーをキャッチする部分をroot.tsxに用意します。エラーは一番近いコンポーネントでキャッチされるので、ページごとに独自のエラーが欲しい場合はページごとに設置することで対応が可能です。
今回は一番外側にあるroot.tsxだけに用意します。なんとなくのheaderも追加:)

app/root.tsx

import type { MetaFunction } from 'remix';
import {
  Link,
  Links,
  LiveReload,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
  useCatch,
} from 'remix';
import styles from './tailwind.css';

export const meta: MetaFunction = () => {
  return { title: 'New Remix App' };
};

export function links() {
  return [{ rel: 'stylesheet', href: styles }];
}

export const loader: LoaderFunction = async () => {
  return null;
};

export default function App() {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <header className="mb-4 bg-slate-200 p-4">
          <h1 className="text-5xl font-bold">
            <Link to="/">Welcome to Remix</Link>
          </h1>
        </header>
        <div className="p-4">
          {/* Outlet部分にページがレンダリングされます */}
          <Outlet />
        </div>
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}

// エラーがここでキャッチされる
// 404以外はさらにErrorBoundaryへ送っています
export function CatchBoundary() {
  const caught = useCatch();
  if (caught.status === 404) {
    return (
      <div className="prose">
        <h1>{caught.statusText}</h1>
      </div>
    );
  }
  throw new Error(`Unhandled error: ${caught.status}`);
}

// CatchBoundaryでキャッチできなかったエラーはこちらでキャッチする
export function ErrorBoundary({ error }: { error: Error }) {
  return (
    <div className="prose">
      <h1>{error.message}</h1>
    </div>
  );
}

これで記事一覧、記事詳細ページが作成できました。

動作確認

最低限のページができたところで動作を試していきます。

$ npm run dev

見た目や機能はかなり簡素ではありますが、、
microCMSを利用したブログサイトを作ることができました:)

一覧、詳細ページのheaderにはstale-while-revalidateを記載しているのでSWRに対応できる環境(Vercelなど)にデプロイすることでキャッシュも活かすことができます。

さいごに

RemixでmicroCMSとSDKを利用することでとても簡単にブログサイトを構築することができました。microCMSなどヘッドレスなCMSを利用することで運用コストの高いデータベースやコンテンツの管理をサービス側に任せることができます。そのぶんを企画やフロントエンドの開発など力をいれることができるのではないでしょうか。