この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
こんにちは、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にはMicroCMSImage
やMicroCMSDate
など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を利用することで運用コストの高いデータベースやコンテンツの管理をサービス側に任せることができます。そのぶんを企画やフロントエンドの開発など力をいれることができるのではないでしょうか。