この記事は microCMS Advent Calendar 21日目の記事です。
こんにちは、でぃーすけと申します。
今回はWebフレームワークAstroを使ってmicroCMSの画面プレビューを実装する方法をご紹介します。
はじめに
事前準備として以下の公式ブログを参考に「ブログサイト」を構築してください。
https://blog.microcms.io/astro-microcms-introduction/
今回は上記で作成したブログサイトをベースに画面プレビューを実装していきます。
実装の全体像
今回作成したブログサイトはサーバーサイドでレンダリングする方式ではなく、静的サイトとしてレンダリングする方式をとっています。
そのため、ビルド時は画面プレビューしたいコンテンツの内容は不明であり、画面プレビューされたタイミングでコンテンツのデータを取得する必要があることがわかります。
もちろん画面プレビューが押されるたびに再ビルドしても良いですが、その度に数分待たなくてはいけないのはとても不便ですよね。
これらの理由からクライアントJavaScriptでmicroCMSにリクエストをしてデータを取得する形を取るのが良いでしょう。
microCMSの画面プレビュー機能
microCMSが提供している画面プレビューの機能は、下書き中のコンテンツ画面からユーザーが設定したURLに遷移することです。
このURLを設定する際に、コンテンツIDとdraftKey(下書きコンテンツを取得するためのキー)を含めることができます。
つまり、https://sample.com/preview?contentId=aaa&draftKey=bbb のようなページに遷移させて、URLクエリパラメータから必要な情報を取得するイメージです。
実装の流れ
ここで大まかに実装の流れを掴みます。
- microCMSの管理画面:API設定から画面プレビューURLを設定する
- ソースコード:pagesディレクトリにプレビューページを作成する
- ソースコード:クライアントJavaScriptでURLクエリパラメータからcontentId、draftKeyを取得する
- ソースコード:クライアントJavaScriptでmicroCMSにリクエストを送り、取得したデータをレンダリングする
1. 画面プレビューURLを設定する
ブログAPIの画面右上からAPI設定に遷移したのち、画面プレビューのメニューを選択します。
そして、http://localhost:3000/preview?contentId={CONTENT_ID}&draftKey={DRAFT_KEY}
と設定します。
ここでは一旦ローカルサーバーを指定しておき、開発中に確認しやすくしておきます。
2. プレビューページを作成する
ソースコードの方に移り、まずはページを作成します。
Astroでは (デフォルトで)src/pages
ディレクトリがページレイアウトを担当します。
今回は /preview
で開かれるページを作成したいので、src/pages/preview.astro
を作成します。
同じディレクトリにある [blogId].astro
を複製するのが良いでしょう。
そこから必要ない記述を削除すると以下のようになります。
---
import Layout from "../layouts/Layout.astro";
---
<Layout title="My first blog with Astro">
<main>
<h1 class="title"></h1>
<p class="publishedAt"></p>
<div class="post"></div>
</main>
</Layout>
getStaticPaths
や Astro.params
を使ったブログ詳細の取得はここでは必要ありません。
これはビルド時ではなく、クライアントサイドで動的に取得するためです。
3. URLクエリパラメータからcontentId、draftKeyを取得する
次にURLクエリパラメータから必要な情報を取得する実装をします。
ここでは window.location.search と URLSearchParams を使います。
<script>
const params = new URLSearchParams(window.location.search);
const contentId = params.get("contentId");
const draftKey = params.get("draftKey");
console.log(contentId, draftKey);
</script>
ここで一度、microCMSの管理画面から下書きコンテンツを作成し、画面プレビューボタンを押してみましょう。
表示されたページのコンソールに、画面プレビューを押した画面のコンテンツID、draftKeyが表示されていたら成功です。
4. microCMSにリクエストを送り、取得したデータをレンダリングする
そして最後はmicroCMSに実際にリクエストを送り、取得したデータをレンダリングします。[blogId].astro
でも使用した src/library/microcms.ts
の getBlogDetail
をそのまま使いたいところですが、今のままでは使用できません。
というのもAstroの環境変数はデフォルトでサーバーでのみ使用可能となっているため、PUBLIC_
というプレフィックスをつける必要があります。
PUBLIC_MICROCMS_SERVICE_DOMAIN=ドメイン
PUBLIC_MICROCMS_API_KEY=APIキー
のように名前を変更し、src/library/microcms.ts
の方も同様に変更してください。
※クライアントサイドでAPIキーを使用するということは調べようと思えば誰にでもこのAPIキーは知られてしまいます。microCMSではAPIキーの権限を設定できるため、GETのみにするなど適宜設定してください。
先ほど取得したURLクエリパラメータをもとにmicroCMSにリクエストを送ります。
<script>
import { getBlogDetail } from "../library/microcms";
const params = new URLSearchParams(window.location.search);
const contentId = params.get("contentId");
const draftKey = params.get("draftKey");
if (contentId && draftKey) {
getBlogDetail(contentId, { draftKey }).then((data) => {
console.log(data);
});
}
</script>
先ほどと同様のURLにアクセスし、コンソールに下書き中コンテンツのデータが表示されれば成功です。
次は実際にHTMLに反映します。
<script>
import { getBlogDetail } from "../library/microcms";
const params = new URLSearchParams(window.location.search);
const contentId = params.get("contentId");
const draftKey = params.get("draftKey");
if (contentId && draftKey) {
getBlogDetail(contentId, { draftKey }).then((data) => {
const titleElement = document.querySelector(".title");
const publishedAtElement = document.querySelector(".publishedAt");
const postElement = document.querySelector(".post");
titleElement && (titleElement.textContent = data.title);
publishedAtElement &&
(publishedAtElement.textContent = data.publishedAt ?? data.createdAt);
postElement && (postElement.innerHTML = data.content);
});
}
</script>
再度プレビューURLにアクセスし、コンテンツが表示されていたら成功です。
ソースコード
最後に実際のソースコードを記述します。
---
import Layout from "../layouts/Layout.astro";
---
<Layout title="My first blog with Astro">
<main>
<h1 class="title"></h1>
<p class="publishedAt"></p>
<div class="post"></div>
</main>
</Layout>
<script>
import { getBlogDetail } from "../library/microcms";
const params = new URLSearchParams(window.location.search);
const contentId = params.get("contentId");
const draftKey = params.get("draftKey");
if (contentId && draftKey) {
getBlogDetail(contentId, { draftKey }).then((data) => {
const titleElement = document.querySelector(".title");
const publishedAtElement = document.querySelector(".publishedAt");
const postElement = document.querySelector(".post");
titleElement && (titleElement.textContent = data.title);
publishedAtElement &&
(publishedAtElement.textContent = data.publishedAt ?? data.createdAt);
postElement && (postElement.innerHTML = data.content);
});
}
</script>
デプロイ
この方法ですとホスティング先は特に縛りなく、どのサービスも利用できるかと思います。
前回の記事同様、ビルドはnpm run build
で行います。
ローカルで一度チェックする際は、npm run preview
が便利です。(このコマンドは本番環境で動作するように設計されていないため、ホスティング先のベストプラクティスを別途調査ください)
プレビューの体験をさらに上げる
ここまでで画面プレビューとしての最低限の実装は完了していますが、さらに便利にすることが可能です。
現状だとサイトに訪れた最初のタイミングしかリクエストが送られません。
これを
プレビューサイト
↓
microCMSで下書き保存
↓
プレビューサイト
とタブを切り替えるだけで内容が更新されるようにします。
PreactとSWRの導入
こういったリアクティブな動作が必要なものはUIフレームワークを導入した方が効率が良いです。
そこでUIフレームワークとしてPreact、データフェッチライブラリとしてSWRを導入します。
(ReactやVueを導入してもいいのですが、Astroを使ったサイトは元々JSのサイズが抑えられるのが特徴なので、ここでサイズの大きいパッケージを入れるよりはPreactのようなものを採用するのが賢い選択であると考えています)
npm i @astrojs/preact preact swr
※ preact: v10.11.3、swr: v2.0.0 を使用しています。
次に astro.config.mjs
にPreact統合を追加します。
import { defineConfig } from "astro/config";
import preact from "@astrojs/preact";
export default defineConfig({
integrations: [preact({ compat: true })],
});
また、tsconfig.json
に jsx のファクトリー関数を Preact のモジュールからインポートするように設定したいので、
{
"extends": "astro/tsconfigs/strictest",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
}
}
※extendsにstrictestの設定ファイルを指定していますが、こちらはご自身のスタイルに合わせてお選びください。参考:https://docs.astro.build/ja/guides/typescript/#setup
Reactに依存したライブラリをPreactで使用するため、Astroの場合は使用しているパッケージマネージャに合わせて設定をする必要があります。
npmの場合は、package.json
に以下のように記述しましょう。
{
~~~
"overrides": {
"react": "npm:@preact/compat@latest",
"react-dom": "npm:@preact/compat@latest"
}
~~~
}
下準備が完了したので、実装に入りましょう。
プレビューコンテンツをレンダリングするコンポーネントを作成
先ほどはバニラのJSでmicroCMSにリクエストを送ったり、その後のレンダリングまで行っていました。
これをPreactと使って実装していきます。
まずは、コンポーネントファイルを作成します。src/components/BlogPreview.tsx
を追加し、一旦以下のように preview.astro
にあったHTMLをそのままレンダリングする形とします。
const BlogPreview = () => {
return (
<div>
<h1 class="title"></h1>
<p class="publishedAt"></p>
<div class="post" />
</div>
);
}
ここにSWRからインポートした useSWR を使うと、
import useSWR from "swr";
import { getBlogDetail } from "../library/microcms";
const BlogPreview = () => {
const params = new URLSearchParams(window.location.search);
const contentId = params.get("contentId");
const draftKey = params.get("draftKey");
const { data, error, isLoading, isValidating } = useSWR(
contentId === null || draftKey === null
? null
: ["/preview", contentId, draftKey],
([, contentId, draftKey]) => getBlogDetail(contentId, { draftKey })
);
if (error) return <div>エラーが発生しました</div>;
if (isLoading) return <div>読み込み中...</div>;
return (
<div>
<h1 class="title">{data?.title}</h1>
<p class="publishedAt">{data?.publishedAt ?? data?.createdAt}</p>
<div
class="post"
dangerouslySetInnerHTML={{ __html: data?.content ?? "" }}
/>
{isValidating && <div>更新中...</div>}
</div>
);
};
export default BlogPreview;
順番に見ていきましょう。
最初の2行はモジュールをインポートしているだけなので省略します。
そして、コンポーネントの最初の3行は、元々の実装にもあったURLクエリパラメータから必要な情報を取得する実装となります。
その次の行にあるのが肝心のuseSWRを使った箇所ですね。
const { data, error, isLoading, isValidating } = useSWR(
contentId === null || draftKey === null
? null
: ["/preview", contentId, draftKey],
([, contentId, draftKey]) => getBlogDetail(contentId, { draftKey })
);
useSWRは第1引数にリクエストのためのユニークなキー文字列(文字列、配列または関数)を指定でき、これが変わると再フェッチが走ります。
第2引数にはフェッチをする関数を指定できて、この際に引数として先ほど渡したキー文字列を受け取ることができます。
今回の場合は、contentIdかdraftKeyのどちらかがnullのケースではフェッチを走らせたくないので、その場合のみ第1引数にnullを渡して条件付きのフェッチをしています。
そしてここがSWRのかなり好きなところなのですが、第1引数で not null を確定させた上でフェッチを走らせているため、第2引数で渡ってくるcontentIdとdraftKeyはしっかりと string
のみの型推論がされるのです!
(他にも型推論がめちゃくちゃ効いているポイントはあるので、ぜひ使って感動してください)
少し話がそれましたが、こういった形でcontentIdとdraftKeyをURLクエリパラメータから取得し、それを使ってフェッチしています。
useSWRが返す値としては、data
、error
、isLoading
、isValidating
、mutate
があり、今回はmutate以外を使用しています。data
、error
は文字通り、取得したデータ、フェッチエラーです。isLoading
とisValidating
は似ているのですが、少し違います。isValidating
がフェッチ中必ずtrueになるのに対し、isLoading
はその時既にロードしているデータがない時のみtrueになります。
つまり、isLoading
は基本的には初回フェッチのみtrueになり、isValidating
はそれ以外でもtrueになるといった感じです。
これを利用して、error
がある時、isLoading
がtrueの時、それ以外の時、という形で分岐しています。isValidating
があることで、画面プレビューを見ている人が「今は更新中なんだ」とわかるため、非常にわかりやすくなります。
プレビューページに反映する
コンポーネントの作成が完了したので、src/pages/preview.astro
に反映しましょう。
---
import BlogPreview from "../components/BlogPreview";
import Layout from "../layouts/Layout.astro";
---
<Layout title="My first blog with Astro">
<main>
<!-- client:onlyがポイント -->
<BlogPreview client:only="preact" />
</main>
</Layout>
Astroでは上記のように、.astro
ファイルに任意のUIフレームワークのコンポーネントを使用することができます。
前回の記事でも紹介したように部分的にJSを実行することができるため、プレビューページ以外は即座にコンテンツが表示されパフォーマンスもバッチリです。BlogPreview
に client:only
という記述があります。
これはAstroにこのコンポーネントをインタラクティブにする(ハイドレーションさせる)ように指示を出しています。
その際、:only
とつけることでクライアントサイドのみでレンダリング、つまりビルド時(またはサーバーサイド)にレンダリングしないようにしています。
これは、
- プレビューコンテンツがビルド時には不明なこと
- ブラウザ側にしかないAPIを分岐なしで利用していること
が理由となります。
再度、画面プレビューボタンを押すなどしてプレビューページに下書きコンテンツが表示されていれば成功です。
この時、microCMSの管理画面からコンテンツを修正、下書き保存したのち、もう一度タブを切り替えてページを見てみると更新されることがわかると思います。
おわりに
Astroはコンテンツが重要視されるWebサイトにおいて最も価値を発揮します。
その時の現場の状況や人数、スキルなどによって選択すべき技術は異なるかと思いますが、ぜひAstroもその候補に入れていただけたら幸いです。
1度使うと病みつきになること間違い無しです!(個人的見解w)