Headless CMSと言えば、クラウドならContentfulやmicroCMS、オンプレミスならGhostやStrapiなどを想像する人が多いでしょう。
VCbornの公式サイトも以前はGhostを使っていたのですが、カスタマイズ性があまり高くないことや日本語入力が時折おかしくなるなどの問題があり、他のものを探すことにしました。
選定
Headless CMSの選定にはJamstackのページを参考にしました。
Strapi
Ghostを導入する前に一度試しました。悪くはないのですが、localとproductionで一々編集してアップしてをするのは面倒だったので除外しました。
Tina
基本ローカルでもできるそうですが、production環境ではTina Cloudを使用しないといけないそうです。
Payload CMS
UIが超絶シンプルだったので候補になりましたが、対応DBがmongoDBのみでした。
Directus
APIベースのデータ管理プラットフォームです。
クラウド版のDirectus Cloudもありますが、GPL-3.0で提供されているセルフホスト版もあります。
良いところ
- セルフホストできる
- MySQL(MariaDB)で使える
- UIがシンプルでカッコいい
- リポジトリの更新頻度が高い
- 詳細な権限設定が可能
悪いところ
- ドキュメントが分かりづらい
- 日本語記事がほぼない
- クラウド版の一部機能が使えない
導入
セットアップ
LTSのNode.jsが入っている環境で行ってください。
まずdirectusのプロジェクトを作成します。
npm init directus-project example-project
MySQL/MariaDBが入っている環境の場合はアドレスやポート番号、ユーザー名・パスワード、データベース名を聞いてくるので、それらを入力します。
管理者ユーザーは適当なユーザーにしましょう。
Create your first admin user:
? Email: [email protected]
? Password: ********
一通り終わるとプロジェクトが生成されるので、npx directus start
でサーバーを開始します。
デフォルトのポートは8055です。
コレクションの作成
初期状態ではまだ何も作成されていません。
Settings > Data Modelから追加を選択し、欲しい形のモデルを作成します。
例えば、VCbornのニュースだとこのようになっています。
各項目の値はこのようにしています。
id | 記事のパーマリンク |
title | 記事のタイトル |
thumbnail | 記事のサムネイル |
date_created | 投稿日 |
date_updated | 更新日 |
user_created | 作成したユーザー |
draft | 下書きかどうか |
content | 内容 |
各フィールドは様々な物から選ぶことができます。
アイテムの作成
コレクションを作成し終えたら、それを元にアイテムを作ります。
権限設定
このままでは認証していないクライアントで画像を表示することができないので、権限をいじる必要があります。
Settings > Roles & Permissionsを開き、PublicのSystem CollectionsからDirectus Filesの読み取りのみ許可してやります。
Next.jsで取得
データの作成はできたので、今度は実際にデータを取得して表示してみましょう。
公式のNext.js用Exampleがあるのでそれを参考にして作っていきます。
Directus SDKを既存のプロジェクトに入れておきます。
npm i @directus/sdk
認証用にトークンも生成しておきます。
まずは認証済みのDirectusクライアントを作成します。
import { Directus } from "@directus/sdk"
import getConfig from "next/config"
const { publicRuntimeConfig, serverRuntimeConfig } = getConfig();
const { url } = publicRuntimeConfig;
const { email, password, token } = serverRuntimeConfig;
type News = {
id: string
title: string
thumbnail: string
date_created: Date
date_updated: Date
user_created: string
draft: Boolean
content: string
}
type Collections = {
news: News
};
const directus = new Directus<Collections>(url);
export async function getDirectusClient() {
if (email && password) {
await directus.auth.login({ email, password });
} else if (token) {
await directus.auth.static(token);
}
return directus;
}
next.config.jsに環境変数の設定を追加します。
const moduleExports = {
...
publicRuntimeConfig: {
url: process.env.DIRECTUS_URL,
},
serverRuntimeConfig: {
token: process.env.DIRECTUS_STATIC_TOKEN,
},
}
module.exports = moduleExports
.envにも環境変数を追加しておきます。
DIRECTUS_URL=http://localhost:8055
DIRECTUS_STATIC_TOKEN=<生成したトークン>
最後にgetDirectusClientでクライアントを呼び出し、queryすれば完成!
import { format } from 'date-fns'
import Image from 'next/image'
import Link from 'next/link'
import { getDirectusClient } from '@/lib/directus'
const Home = ({news}) => {
return (
<div>
{news.map((item, index) => {
return (
<article key={item.id}>
<Link
className='mx-auto flex flex-col gap-8 md:flex-row md:items-center md:mx-0 duration-200 hover:bg-gray-100'
href={`/news/${item.id}`}
>
<div>
<Image
alt={item.id}
src={`http://localhost:8055/assets/${item.thumbnail}`}
width={360}
height={180}
/>
</div>
<div>
<time className='font-semibold text-xl'>
{format(new Date(item.date_created), 'yyyy.MM.dd')}
</time>
<h3 className='font-semibold text-3xl'>{item.title}</h3>
</div>
</Link>
</article>
)
})}
</div>
)
}
export const getServerSideProps: GetServerSideProps = async (context) => {
const directus = await getDirectusClient()
const res = await directus.items('news').readByQuery({
sort: ['-date_created'],
filter: {
draft: false,
},
})
const news = res.data
return {
props: { news },
}
}
まとめ
大分気に入ったのでしばらくDirectusを使っていきます。
ただ公式のSDKのドキュメントが大雑把で少しわかりづらかったです。