1/22/2023

Contentfull + Next.js + Vercel で個人サイト構築

Contentfull + Next.js + Vercel で個人サイトを構築してみました。まだ色々と改善したい点はありますが、一旦共有したいと思います。

サイト構成

このサイトは以下のような構成でできています。いわゆる、Jamstack構成になっています。

haru256.dev Architecture

ブログ記事やタグなどのコンテンツは Headless CMS の Contentful で管理しています。フロントは Next.jsを用いており、ホスティングサービスは Vercel を使用しています。 現在全て無料枠で運用しており、アクセス数によってはVercelの制限にかかるかも?ですが、大丈夫だと思います。

動機

最近、趣味でWeb系の技術本を読んでいます。知識はなんとなく身についた気がするのですが、Webアプリを作ったことがなかったため、実践的な経験はほぼゼロで、自信がありませんでした。趣味なので別に良いかなと思っていたのですが、一度くらいWebアプリを作って、知った気になるのもいいのではないかと思いました。

何を作るかですが、まずは簡単ですぐにアウトプットできるものが良いなと思いました。私は、Webアプリの構成要素は大きく3つあるのではないかと考えています。全部を実装するのは難しいため、まずは1のフロント、特に静的コンテンツのみでできるWebアプリにしようと思いました。フロントの中でも状態管理、認証・認可、キャッシュなど考えるべきことはたくさんあるのですが、静的コンテンツに限定することで難易度をぐっと下げました。

  1. フロント:データをフェッチして、レンダリングした結果をユーザーに提供
  2. バックエンド:データを受信し格納、要求に応じてデータを加工して配信
  3. インフラ:バックエンド・インフラがリクエスト数に応じてスケールするようにサーバーを管理・監視

静的コンテンツで良さげな題材はなんだろう?と考えたときに思いついたのが個人サイトでした。ということで、この個人サイトは以下を目的に作られたものです。今後もメンテナンスしていくかもです。

  • 目的:Jamstack構成の個人サイトの作成を通して、静的コンテンツ配信Webアプリの実践的な経験を得る&自信をちょっとつける

CMSについて

Contentfulを用いて、ブログ記事やタグなどのコンテンツ記事の管理を行っているのですが、そこに至る経緯や、CMSの選定理由について解説します。

CMSの選定

まず、そもそもCMSを外部機能で行うか、ローカルで管理するかについて考えました。ローカルで管理する場合、1記事を MDX で管理し、Next.js でそのMDXファイルを読み込んでレンダリングするという手法が一般的のようです。MDXとは、MarkdownにJSXの考え方を取り入れたもので、以下のように動的にコンテンツを変えたり、JSXで定義したコンポーネントを取り込むことができます。このような方法で管理することも考えたのですが、アセットの管理や、タイトル・公開日・概要・タグリレーションのようにコンテンツを構造的に保持することを考えた結果、ローカルではなく、CMSを使用することにしました。

mdx-example

CMSですが、現在様々なプロダクトがあります。Jamstackサイトに記載があるだけで90以上のプロダクトがあるようです。どれが自分に適しているのかは使ってみないとわからないのですが、全部を使ってみるのは大変なので、G2サイトのランキングを見てランキングの高い以下3つを試してみました。

まず初めに私が触ったのはStrapiです。Strapi はコンテンツ管理Webアプリのフレームワークを提供するにとどまり、コンテンツ管理Webアプリサーバー・コンテンツ配信サーバー・コンテンツを保存するDBは自身でホストしなければなりません。例えば Google Cloud でStrapiをホストする場合、以下のような構成を作り、自身で管理する必要があります。

Strapi Architecture on Google Cloud

Strapi自体の使用料は無料ですが、配信サーバー・DBのホスト代が別途発生してしまいます。最初は自分で作る初めてのWebアプリなのでモチベーションも高く、自分でホストしてみるかと意気込んでいましたが、Google Cloudで使用する場合、Cloud Runを自身のVPCに引き込む Serverless VPC Access を構築する際の Serverless VPC Access connector のコストが高く、諦めました。このあたりからとりあえず、CMSは簡単なもので良いのでは?と思うようになりました、、、😇

次に触ったのがSanityです。Sanityで管理する場合、CMSは以下のような構成になります。

Sanityでは、コンテンツ管理Webアプリ (Sanity Studioと呼ばれます) はStrapiと同じく自身でホストしなければなりませんが、コンテンツの配信サーバー・コンテンツを保存するDBはSanity自体が管理しており、Sanityが提供するAPI経由でコンテンツを取得します。WebアプリはStrapi同様オープンソースであり、pluginを導入することでカスタマイズが可能です。良さそうに思えたのですが、ここまで来るとコンテンツ管理Webアプリ自体も管理してほしいと思うようになり、Sanityも見送りました。

最後に触ったのがContentfulです。Contentfulは、コンテンツ管理Webアプリ・配信サーバー・DB全てをContentfulが管理しています。そのため、すぐに使用することができ、大抵の需要は満たせます。ただ、markdownで書いた内容のレンダリング結果が自身のサイトのスタイルではないため、Contentfulではうまくレンダリングできたが、自身のサイトではうまくできていないなど、整合性が取れない欠点があります。とは言え、容易に導入でき、最初に構築するサイトのCMSとしては十分なので、今回はContentfulを採用しました。

contentful-editor

Strapi・Sanity・Contentfulの比較結果をまとめると以下のようになりました。基本どのCMSも無料枠があり、GraphQLが使用できるため、あとは管理容易性とカスタマイズ性の2つを天秤にかけて決めました。今回は先程述べたとおり、管理容易性を重視してContentfulを使用することにしました。

CMS管理容易性無料枠カスタマイズ性
Strapi
Sanity
Contentful

Contentfulの構成

現在Contentfulでは、以下2つのコンテンツを管理しています。Contentfulでは各コンテンツで管理するデータ(タイトル、概要など)に対してバリデーションをかける事ができ、必須のものを定義できます。

  1. ブログのコンテンツ(タイトル、概要、公開日、記事本文、コンテンツに付与するタグリスト)
  2. ブログのタグ

コンテンツの取得自体はGraphQLを用いています。RESTもあったのですが、Contentfulの無料枠の制限にAPIコール回数があり、それを意識してGraphQLにしました。無料枠の制限は 2,000,000回 / 月 なので基本超えることは無いです。今回は折角勉強したGraphQLを使用したかったのでGraphQLを採用しています 😅

ブログの記事はMarkdownで書いており、Contentfulからの配信もMarkdownの文字列です。例えば、この記事に関しては以下のようなレスポンスが帰っていきます。フロントはこのMarkdownを受け取ってHTMLに変換して、そのHTMLを表示しています。

contentful-response-example-via-graphql

フロント

フロントの構成

フロントの技術スタックと選定理由について解説します。

フロントの技術スタックは以下のとおりです。

フロントフレームワークとCSSに関しては、一度本で勉強したことがあり現在広く使われているため使用しました。GraphQLクライアントに関しては、Apollo Clientを使用しています。今回はSSGで配信できる内容のため、データフェッチのキャッシュを意識する必要が無く、Apollo Client はオーバースペックだと思われますが、使い慣れているため採用しました。

前述の通り、ブログの記事はMarkdown形式で書いており、Contentfulからの配信もMarkdown形式の文字列です。そのため、MarkdwonをHTMLに変換する必要があります。その変換には、next-mdx-remote を用いました。次に、その next-mdx-remote の解説と各Markdown文法がこのブログではどのようにレンダリングされるのかを紹介します。

Markdownのレンダリング

next-mdx-remoteMDX の文字列を受け取り、それをHTMLに変換するツールです。MDXはMarkdownを拡張したもののため、Markdownも受け付けることができます。例えば、以下のMarkdown文字列 hoge.mdnext-mdx-remote に入力すると、hoge.htmlが出力されます。

hoge.md
### Markdownのレンダリング
[next-mdx-remote](https://github.com/hashicorp/next-mdx-remote)
hoge.html
<h3>Markdownのレンダリング</h3>
<p>
  <a href="https://github.com/hashicorp/next-mdx-remote">next-mdx-remote</a>
</p>

以下に、Githubにあるサンプルを記載します。jsx を使用する場合は以下のように MDXRemote コンポーネントと serialize を使用するだけでMarkdownをHTMLに変換することが可能です。これだけで基本的なMarkdownをHTMLに変換することができます。

example.jsx
import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote } from 'next-mdx-remote'
 
import Test from '../components/test'
 
const components = { Test }
 
export default function TestPage({ source }) {
  return (
    <div className="wrapper">
      <MDXRemote {...source} components={components} />
    </div>
  )
}
 
export async function getStaticProps() {
  // MDX text - can be from a local file, database, anywhere
  const source = 'Some **mdx** text, with a component <Test />'
  const mdxSource = await serialize(source)
  return { props: { source: mdxSource } }
}

next-mdx-remote の内部では、 remarkremark-rehype を用いてMarkdwonをHTMLに変換します。remarkはMarkdownをMarkdown AST (mdast)に変換し、remakr-rehypeはMarkdown ASTをHTML AST (hast)に変換するツールです。

mdast, hast に変換後は容易に木のノードにアクセスでき、またノードの修正ができるため、様々な拡張ができます。

例えば、このブログでは以下の拡張を適用しています。

rehype-pretty-code を用いると、Markdownのcode blockはhtmlでは<code>タグに変換されますが、そこにシンタックスハイライトや行番号を表示できます1。それだけでなく、行のハイライトや差分の表現などができるようです。例えば、rehype-pretty-code-example.md のように書くだけで、rehype-pretty-code-example.py のようにレンダリングされます。

rehype-pretty-code-example.md
```python title="rehype-pretty-code-example.py"
import numpy as np
np.asarray([1, 2])
```
rehype-pretty-code-example.py
import numpy as np
np.asarray([1, 2])

rehype-katexLaTeX\LaTeX形式の数式 KaTex でレンダリングします。 WebページでLaTeX\LaTeXを表現するツールに MathJax が知られていますが、KaTexMathJax よりも速くて軽いようです。markdownの書き方と、そのレンダリング結果は以下のとおりです。

rehype-katex.md
$\sum_n x_n$ でインライン数式が使え、
以下でブロック数式が使えます。
$$
\begin{aligned}
\operatorname{LogLike}(\boldsymbol{w}) & =\sum_{j=1}^M \log \left(\mathcal{P}_{L(\boldsymbol{w})}\left(Y_j\right)\right) \\
& =\sum_{j=1}^M\left[\log \left(\operatorname{det}\left(L(\boldsymbol{w})_{Y_j}\right)\right)-\log (\operatorname{det}(L(\boldsymbol{w})+I))\right]
\end{aligned}
$$

nxn\sum_n x_n でインライン数式が使え、 以下でブロック数式が使えます。

LogLike(w)=j=1Mlog(PL(w)(Yj))=j=1M[log(det(L(w)Yj))log(det(L(w)+I))]\begin{aligned} \operatorname{LogLike}(\boldsymbol{w}) & =\sum_{j=1}^M \log \left(\mathcal{P}_{L(\boldsymbol{w})}\left(Y_j\right)\right) \\ & =\sum_{j=1}^M\left[\log \left(\operatorname{det}\left(L(\boldsymbol{w})_{Y_j}\right)\right)-\log (\operatorname{det}(L(\boldsymbol{w})+I))\right] \end{aligned}

remark-emojiこちらでまとめられているような記法でemojiを導入することができます。例えば、以下は

remark-emoji.md
:grinning: :smiling_imp: :oden:

以下のようにレンダリングされます。

😀 😈 🍢

他にも、目次をつけたり、Mermaidを使用するなど様々なことができるようです。

ホストティング

フロントは Next.js で構成したこともあり、相性の良い Vercel をホスティングサービスに採用しました。 Vercel にすることでGithubのPRを出すだけで PRの差分を適用した場合のサイトのpreviewを表示してくれるなど、すごく快適にデプロイができました。

vercel pr preview

学んだこと&今後やってみたいこと

この個人サイトを作ることで、静的コンテンツ配信であればそれなりにモダンな技術で構築できる自信がつきました。 本を読むと基本つまらずにこなせるのですが、実際にアプリを作ると色々なことで躓きました。その小さな段差を乗り越えることで、実践的な知識が身についたと思います。また、CSSの重要さを知りました。今まではCSSを真面目に勉強したことはなく、なんとなくやり方を知っている程度でした。ですが、Webアプリを使う上でインターフェースの見た目や動き(hover時に色を変えたりなど)はUI/UXに非常に重要だと思いました。一度真面目にCSSを勉強しようと考えています。

今後やってみたいこととしては、以下の2方向があります。

  1. この個人サイトの改修
  2. 静的コンテンツにとどまらないWebアプリの構築

前者は、個人サイトで足りないタグ検索の実装や目次の作成などです。また、Contentfulではどうしてもサイト上でのレンダリング結果とContentful上でのレンダリング結果に差分があるため扱い肉です。それを解消するためにSanityに再度挑戦することも考えています。今後、徐々に実施できればと思います。後者では、「料理レシピ検索」アプリを作ろうとしていて、類似のアプリは腐るほどあるのですが、動的コンテンツのWebアプリの題材として良さそうなので取り組んでいます。実は3月にGoogle Cloudの無料5万円枠が切れるので、急ピッチで進めています。

参考にした個人サイト

今回個人サイトを作ってみる際に、色々と他の方の個人サイトを参照させていただきました。作り方だけでなく、CSSやレイアウトなど色々参考にさせていただきました :bow:

Footnotes

  1. 正確には、タグにidやクラス名が適用されそこに自前で定義したCSSを当てることで行番号を表示できます。