Directus + Astroでブログっぽいウェブを作ってみる
しばらく放置してましたが、続きを実装していきます
2023-11-05追記
スクラップ記事投稿時点(2022年11月)では、DirectusもAstroもよくわからない状態だったので、とりあえず動かしてみて満足。その後しばらく放置してました。
ただ、Directusの使用感が良かったのと、ちゃんとしたサイトを作りたいので、1年越しに続きを頑張ってみることにします。
なお、2022年11月にこのスクラップを書き始めてから、いくつかのプロジェクトでAstroを採用したため、Astroは少しずつ使いこなせるようになってきました。一方、Directusはその良さを実感しつつも、外部のプロジェクトに採用することは難しかった[1]ので、初心者のままです。
本スクラップでは、追加で以下の項目について検証・学んでいきたいと思います:
- 多言語対応(i18n)
- GraphQL(生)でコンテンツを取得
- GraphQL(@directus/sdk)でコンテンツを取得
- 取得したコンテンツを一覧としてAstroに表示
-
DirectusをSelf-Hostingで採用できるような条件のプロジェクトはなかなかないですね…。 ↩︎
なぜAstro?
最近話題なので実際に少し試してみたところ、ビルドや生成されたウェブの動作が、軽快さで知られるHugo並みに速く、コードを書くにあたって学習コストが高くなさそうだったというのが主な理由です。
UIフレームワークを自由に選べるのも良いです。
なぜDirectus?
HeadlessCMSの選定にあたっては、Astroの公式ドキュメントに載っているものから選ぶことにしました。ここに載っていないCMSだと、他にmicroCMSやStrapiなどもAstroとの連携についての情報を公開しています。
条件1 : ローカルで動く
microCMSやContentfulは日本語の情報も多くセットアップも容易なのですが、ローカルで記事を管理したかったので、ローカルな環境で動くCMSに絞ります。Directus、PayloadCMS、Strapi、Ghostが候補です。
条件2 : SQLiteを使いたい
頻繁に記事を更新する予定はなく、管理するページ数も少なくなる予定なので、データベースは管理・運用が簡単[1]なSQLiteを使うことにします。この時点でPayloadCMS(MongoDB)、Ghost(MySQL)は候補から外れます。どちらもUIが綺麗で好みではありますが…残念。
結果、今のところDirectusが良さそう
以下の理由で、Directusにしました。
- Astroの公式ドキュメントで言及されている
- 主要なSSGフレームワークと連携するためのテンプレートが用意されている[2]
- StrapiよりUIが好き
ただし、行き詰まった場合には、情報量が多いStrapiに浮気する可能性も全然あります。
Directusのセットアップでいきなりつまずく
Directusの公式サイトにDirectus + Astroのセットアップ手順が書いてあるので、それに従います[1]。
ステップ 1 : サンプルリポジトリの取得
examples repoをまるっとダウンロードします。
ステップ 2 : リポジトリからDirectusのセットアップ
ダウンロードしたリポジトリの中のdirectus
フォルダからDirectusをセットアップ。
- 依存するライブラリをインストールします。
cd directus
npm install
- Directusを動かしてみます。
npx directus start
- ブラウザで
http://localhost:8055
にアクセス - Directusのログイン画面が表示されるので、
ID: admin@example.com
、パスワード: d1r3ctu5
でログイン。
異常発生(ログインができない)
ここまで進んだところ、自分の環境の問題なのか、ダウンロードしたリポジトリ[2]の問題なのか、ログイン試行時にUnexpected Error
エラーが表示され、ログイン画面から先に進めず。
ターミナルからログを確認すると、以下のようなエラーを吐いていました。
SQLITE_ERROR: table directus_sessions has no column named origin
"SQLiteデータベースのdirectus_sessions
テーブルにorigin
列が存在しない" そう。
素のDirectusをnpm経由でインストールした際にはそのようなエラーは起きなかったため困惑しつつ、一旦作業中断。
Strapiに切り替えるか、SQLiteをいじるか、素の状態のDirectusを使って再挑戦するかを検討中。
結局、素のDirectusをインストールすることに
気分と視野を変えるために、すこしStrapiを使ってみました。
[1]
Starpiは面倒くさいStrapiの公式にある、 Strapi + Astro + Tailwind 環境のセットアップの解説記事を読んでみるものの、ちょっとカスタマイズなどもしているためか、少々手順が多い。
そもそも、どうやら2022年11月現在、Strapiのセルフホスト版は全機能が無料で使えるわけではなく、この記事に書かれている手順の通りに実装するにはEnterpriseプランで課金が必要そう。
[1:1]
Directusは面倒くさくないシンプルな用途であれば、Directusはパーミッションの設定さえすれば、他にはほとんど何もせずともすぐにREST APIを叩けて、使えるJSONを取得可能。Strapiの方は、EnterpriseプランでなければDirectusほど簡単にはREST APIを活用できなそうです。
無料で使える範囲で簡単にREST APIを叩けて、セットアップも簡単なDirectusの方が今回の用途には相応しいと判断。何か大きな障害が顕在化しない限りはDirectusを使う方向性を変更せず進めることに。
ただし、example repoには頼らず、Astroのテンプレートも一から作ります。Astroは簡単なので、一から作る場合でもそこまで負担ではないと判断しました。
改めてDirectusのセットアップ
example repoのリポジトリは使わないので、今度はnpm
でDirectusをセットアップします。
以下のコマンドでdirectusのセットアップができます。
npm init directus-project プロジェクト名
コマンドを入力後、しばらくすると自動的にセットアップウィザードが起動し、データベースの種類、データベースの設定、初回ログイン用のアカウント名とパスワードの入力を求められます。画面の指示に従って入力するだけなので、困ることはないと思います。
セットアップが完了したら、該当フォルダに移動し、Directusを起動してみましょう。
cd プロジェクト名
npx directus start
特に問題がなければDirectusが立ち上がるので、ブラウザでhttp://0.0.0.0:8055
にアクセスして、先程設定したIDとパスワードでログインします。
ポート番号が重複する場合
ポート番号が他のサービスと重複する場合は、.env
ファイルを編集して、ポート番号を変更しましょう。
日本語UIに変更する
Settings > Project Settings > Default Language
からJapanese
を選択し、画面右上の✅をクリックすれば、反映されます。日本語は自然で、違和感なく使えます。
データモデルを作成する
Directusは、そのままだと記事の投稿などはできません。Settings > Data Model
からデータモデルを作成することで記事作成用のテンプレートのようなものが作れます。
データモデルで、ブログ記事用のフィールドを作ってみた様子
タイトルやテキスト、タグなど、投稿に必要だと思われる項目を好きに選んでOK。選択できる入力フィールドの種類の一部を貼り付けました。Strapiと比較すると非常に多いです。
選択できる入力フィールドの一部
パーミッションの設定
今回はAstroからAPI経由で記事データを読み込むので、パーミッションを設定します。
Settings > 役割・権限 > Public
まで進んで、読み取り権限に✅を入れます。
Directusを多言語(i18n)対応にする
Directusはコツさえ掴めば、多言語対応も楽に実装できます。
ここでは、pages
というデータモデルを多言語対応していきます。
1. 対応させたいデータモデル画面の操作
大切なポイントとしては、多言語対応したい項目は元々のデータモデルでは準備しないことです。今回はタイトルと本文を多言語対応したいので、それらは元々のデータモデルには追加しません。
- データモデルを作成画面に、
新規項目の追加
の下に、目立たない色でアドバンスモードでフィールドを作成
という項目があるので、クリック - 表示されたメニューの一番下にある
翻訳
をクリック
2. 翻訳項目の設定
(1.) の操作後に表示される設定画面から「インターフェース」と「ディスプレイ」を設定します。
- インターフェースの項目で、「翻訳」をクリックし、ハイライトされた状態にする
- 同じく、ディスプレイの項目で、「翻訳」をクリックし、ハイライトされた状態にする
3. データモデル一覧にある、翻訳モデルを選択
(1.) (2.) の操作後、データモデル一覧に、グレーアウトされた翻訳モデルが表示されます。通常は、モデル名_translations
という名称です。こちらを編集することで、対応データモデルを多言語対応することができます。
blog_posts
にblog_posts_translations
, pages
にpages_translations
というモデルが追加されている。language
モデルは、その際に自動的に生成された(…はず)
4. 翻訳モデルに、多言語対応したい項目を追加する
以上の画像のpages_translations
を編集してみましょう。
-
id
,pages_id
,language_code
という項目がすでにあるので、これらはいじらず、新規項目の追加をクリック - 新規フィールド画面が表示されるので、タイトル用に
input
を選択。キー名をtitle
に設定 - 同じ手順で、
wysiwyg
を選択し、キー名をcontent
に設定
上記の手順で、translationsデータモデルにタイトルと、本文入力欄が追加されました。
5. コンテンツ編集画面から、言語を設定
Directusでは上記手順で多言語対応する場合、英語、スペイン語、フランス語などが追加されます。これを英語と日本語に変更します。
- まずはサイドバーから、一番上の
コンテンツ
を選択 -
Languages
項目を選択 - 記事で扱える言語一覧が表示されるので、不要な言語を削除
- 画面右上の
+
アイコンをクリックし、Languagesのアイテム作成
画面を開く -
Code
にja-JP
と入力し、☑️をクリック
こんな感じで、日本語が一覧に追加できます
6. 編集してみる
通常通り、記事編集画面を開きます。そうすると、以下の画像のような編集画面になります。
Translationsの項目があるので、その右端にあるアイコンをクリックします。
そうすると以下の画像のように、日英の入力項目が横に並んだ状態で編集できます。
以上で、多言語対応の手順は完了です。
アクセスコントロール画面から、多言語対応項目へのアクセスを許可することを忘れないようにしましょう。
Astroのセットアップ
以下のコマンドで、Astroの最新版のセットアップウィザードが起動します。
npm create astro@latest
Directusよりも設定項目は多いですが、基本的にはrecommend
と表示されている項目を選択していけば大丈夫です。
DirectusのSDKを準備
Astroのサーバを立ち上げる前に、Directus用のSDKもインストールしておきます。
npm install @directus/sdk
Astroを走らせる
これで、DirectusのAPIを叩いてデータを読み込む準備が完了しました。
以下のコマンドでAstroの開発用のサーバが立ち上がります。
npm run dev
REST APIでAstroでDirectusのデータを読み込めるかを確認してみる
こちらが、最低限の動作確認用のコード。AstroのDebug
コンポーネントを使って、DirectusSDKで叩いたAPIの中身をそのまま出力します。
---
import { Debug } from 'astro/components' // デバッグ用のコンポーネント
import { Directus } from "@directus/sdk" // DirectusのSDK
const directus = new Directus('http://0.0.0.0:8055') // DirectusのURL
const blogPosts = await directus.items('blog').readByQuery({limit: -1}) // 'blog'データセットの記事一覧を取得
---
<Debug {blogPosts}/>
Directusで入力した内容と比較してみる
Directusの記事編集画面の入力内容をスクリーンショットしました。上記のコードでAPIを叩いて得られたデータと比較してみましょう。
出力結果は以下の通りとなっていて、Markdown形式で書き出されていたり、タグが配列になってたりすることがわかると思います。ここまで、トラブルがなければ最速で10分以内で実装できます。簡単。とても。
{
"data": [
{
"id": 1,
"status": "published",
"date_created": "2022-11-09T00:20:30.000Z",
"date_updated": "2022-11-09T00:20:45.000Z",
"title": "記事のタイトル",
"content": "# 見出し 1\n## 見出し2\n\nMarkdownで書かれたテキストのコンテンツ",
"tags": [
"gamedev"
]
}
]
}
REST APIを使って個別の記事を表示する
Directusのblog
データセットから記事を読み込んで、Astroで出力するための最低限のコード。
http://localhost:3000/posts/1
のようなurlにアクセスすれば、Directusの記事が表示されます。
MarkdownテキストはそのままだとHTMLに変換されずに表示されるので、Markdownをパースするためのコンポーネントも読み込んでいます。
---
// DirectusのSDK
import { Directus } from '@directus/sdk'
// Markdownをパースするためのコンポーネント
import {unified} from 'unified'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import rehypeStringify from 'rehype-stringify'
// 動的urlを処理するためのメソッド
export async function getStaticPaths() {
type BlogPosts = {
data: Array<any>
}
const directus = new Directus('http://0.0.0.0:8055')
const postResponse = await directus.items('blog').readByQuery({limit: -1}) as BlogPosts
const posts = postResponse.data
const result = posts.map((post) => ({
params:{id:post.id},
props: {
title: post.title,
content: unified().use(remarkParse).use(remarkRehype).use(rehypeStringify).processSync(post.content).toString(),
}
}))
return result
}
const { id } = Astro.params
const {title, content} = Astro.props
---
<html>
<head>{title}</head>
<body>
<h1>{title}</h1>
<article set:html={content} />
</body>
</html>
GraphQLでDirectusのコンテンツを取得してみる
さて、ここまではREST APIを使ってきましたが、今後はGraphQLを使ってみたいと思います。
まずは勉強のためdirectusSDK
は使わず、GraphQL
に直接リクエストを送って、Directusのコンテンツを取得してみたいと思います。
GraphQLについては今回初めて入門するので、Phindに聞きながら進めていきました。
Directus側の準備
特にありません。
ローカルでnpmを使ってセットアップした場合、Directus側のエンドポイントのURLはhttp://0.0.0.0:8055/graphql
です。
Astro側の準備
手早くGraphQLのリクエストを実行するためのモジュールをインストールします。
npm install graphql-request
準備としては以上です。
あとはコードを書いて、GraphQLを取得していきます。
GraphQLのリクエストを送って、動作確認をする
以下のようなデータモデルの記事一覧を取得したいと思います[1]。
コードでは以下のように書きました。Astroでページを生成する前に、コンソールへの出力を試みてみます。
import { request, gql } from "graphql-request";
let pages;
const query = gql`
query {
pages {
id
translations{
title
content
languages_code{
code
}
}
}
}
`
request("http://0.0.0.0:8055/graphql", query)
.then((data) => {
pages = data.pages;
console.log(pages); // コンソールに取得したコンテンツを出力
})
.catch((error) => console.error(error));
記事を1つだけ書いた状態だと、コンソールには以下のように出力されます。
[ { id: '1', translations: [ [Object], [Object] ] } ]
console.log(pages)
をconsole.log(pages[0].translations)
に変更し、translations
を展開して出力すると、以下のように出力され、きちんとGraphQL経由でコンテンツが取得できていること、多言語のコンテンツが入力・取得できていること確認できました。
[
{
title: 'title',
content: '<p>content</p>',
languages_code: { code: 'en-US' }
},
{
title: 'タイトル',
content: '<p>コンテンツ</p>',
languages_code: { code: 'ja-JP' }
}
]
GraphQLに関しては全くの無知な状態から始めたので、ここまで少し時間がかかりました。が、コードだけ見てみると、少ない行数でコンテンツの取得ができたことがわかります。
多言語対応をしない場合なら、慣れればDirectusのインストール > Directusのデータモデルの用意 > テスト記事の入力 > Astroのインストール > GraphQLの取得
まで15分もあればできそうだなという印象です。
-
i18n対応しているので、基本のデータセットと、多言語用のデータセットがあり、それぞれが対応関係となっています。 ↩︎
ちなみに@directus/sdkを使って、GraphQL
のリクエストを送ることもできます。
公式サイトに書いてあるサンプルコードは以下のとおりです。
import { createDirectus, graphql } from '@directus/sdk';
interface Article {
id: number;
title: string;
content: string;
}
interface Schema {
articles: Article[];
}
const client = createDirectus<Schema>('http://directus.example.com').with(graphql());
const result = await client.query<Article[]>(`
query {
articles {
id
title
content
}
}
`);
次は、実際に@directus/sdk経由でGraphQLを利用してみることにしましょう。
Directus SDKで取得した値をAstroで表示
GraphQLのテストが完了したので、今度は@directus/sdk + GraphQLを使ってみることにします。GraphQLの勉強をしたいので、コンテンツの取得は引き続きGraphQLで。
AstroにSDKのインストール
まずはAstroのプロジェクトに、@directus/sdk
をインストールします。
npm install @directus/sdk
Astroのファイルを編集する
インストールが完了したら、Astroのsrc/pages/index.astro
を編集します。
Directusの公式ドキュメントにあるコードをベースにして、自分のデータモデルとAstroのフォーマットに合うように対応箇所を書き換えていきます。
私のプロジェクトでは、多言語に対応する場合はGraphQLの返り値の構造が入り組んでるので、interface
を2つ用意しました。
以下のコードでは、多言語対応ページのうち、言語がen-US
に設定されているものだけを取得します[1][2]。
---
import { createDirectus, graphql } from '@directus/sdk';
interface I18nContent{
title: string;
content: string;
languages_code: {
code: string;
};
}
interface PageContent {
id: number;
translations: I18nContent[];
}
const client = createDirectus<PageContent[]>('http://0.0.0.0:8055').with(graphql());
const result = await client.query(`
query {
pages {
id
translations {
title
content
languages_code {
code
}
}
}
}
`);
const pages:PageContent[] = await result.pages;
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
{pages.map(page => {
const i18nContent = page.translations.find(item => {
return item.languages_code.code === 'en-US';
}) as I18nContent;
if (i18nContent !== undefined) {
return (
<div>
<h1>{i18nContent.title}</h1>
<p>{i18nContent.content}</p>
</div>
)}
})}
</body>
</html>
結果は以下のとおりです。スタイルを一切当ててないので非常に味気ないですが、動きました[3]。
http://0.0.0.0:8055をブラウザで表示した結果
DirectusのGraphQL APIではwhere
を使えなかったので、where
の部分をfilter
に変更したら、動きました。
例えば、言語で絞り込む場合には以下のように書く。lang_code
には"en-US"や"ja-JP"などが代入されています。
query {
pages(filter: {translations: {languages_code: {code: {_eq: "${lang_code}"}}}}) {
id
translations {
title
content
languages_code {
code
}
}
}
}
Tailwind CSSを使ってみる。
今まで「自分で書く方が自由が利くし実装も早い」という理由で、CSSフレームワークを敬遠していましたが、今回は後学のために使ってみることにしました。
Tailwind CSSを採用した理由は「なんだか流行ってるから」という単純な理由。htmlタグにずんずんTailwindのクラスを追加していくのは違和感があったものの、慣れると意外と良い感触です。
他にも言及している方が多いですが「クラス名を考えなくて良い」というのが快適です。ウェブサイト内で繰り返し使われている要素のスタイルを変更する際も、SSG + Tailwindだと比較的楽です。