天然パーマです。

Next.js+microCMS+Vercel面白い

Next.js と microCMS と Vercel が面白い。それぞれ面白いし、組み合わせるとさらに面白い。なにせ、メディアサイトがデプロイも含めて 2 時間で出来る。

Next.js + microCMS + Vercel すごいな。メディアサイト(中身スッカスカだけど)がものの 2 時間でデプロイまでできた。

https://twitter.com/yusukebe/status/1435708770705760256

ということで、メディアサイトを作りながら、Next.js と microCMS と Vercel の面白さをまとめる。

2 時間で作るメディアサイト

例として「ラーメンまとめ!」というメディアサイトを作ってみる。このサイトには

  • ラーメン屋
  • ラーメン屋のまとめ記事

の 2 つの種類のコンテンツがある。「ラーメン屋」が「名前」「場所」「ラーメン写真」というプロパティを持っていて、個別のラーメン屋を表現する。それをいくつか束ねて記事とするのが「まとめ記事」である。「ひとことコメント」とともにラーメン屋を紹介する。

これがトップページ。まとめがリストアップされている。

まとめの個別ページがこれである。2 つの家系ラーメン屋が紹介されている。

それでは作ってみる。

Next.js の雛形

Next.js は言わずとしれた「React コンポーネントを扱うためのオールインワンパッケージ」である。というと未来感があるが、おじさんにとっては「MVC」の V に特化した Web フレームワークとまず解釈すると入りが楽だ。Twig でも Slim でも erb でも Jinja でも Emmbed Perl でも Template-Toolkit でも何でもいいんだけど、 include 'partial/header' するとヘッダーが再利用できるよね〜、と似たノリで React のコンポーネントも捉えちゃう。

で、いざ使っていくと、Next.js だけじゃ足りないのと、便利な機能を追加したくなった。「設定ファイル」がどんどん増えていった。

  • package.json
  • eslintrc.js
  • jsconfig.js
  • next.config.js
  • postcss.config.js
  • stylelint.conf.js
  • tailwind.config.js

ちょっと、どんだけあるのよw もう、どれが何してるか分からないw

何度もやりたくないから boilerplate を作った。

こんだけ設定しておけば便利だ。例えば、VSCode では、Tailwind CSS のクラス名を補完してくれて、どんな色かまで色で教えてくれる。すごい!

microCMS で API を生やす

microCMS はよくできた国産のヘッドレス CMS である。ヘッドレスというのは「ガワ」を持たないってことで、管理画面はあるんだけど、コンテンツの情報を API 経由で取得するのが前提となっている。例えば、今回は

  • ラーメン屋
  • まとめ記事

という 2 つの種類のコンテンツ API を作った。それぞれ、プロパティを設定すれば、勝手に API が生える。優れものだ。「ラーメン屋」のスキーマはこんな感じ。

それに対してエンドポイントがあるので、例えば

$ curl "https://ramen-matome.microcms.io/api/v1/shop" -H "X-API-KEY: MY_API_KEY"

を叩くと、

{
  "contents": [
    {
      "id": "e2s_0m9kzr",
      "createdAt": "2021-09-16T00:37:44.213Z",
      "updatedAt": "2021-09-16T00:37:44.213Z",
      "publishedAt": "2021-09-16T00:37:44.213Z",
      "revisedAt": "2021-09-16T00:37:44.213Z",
      "name": "たかさご家 関内店",
      "place": "関内",
      "photo": {
        "url": "https://images.microcms-assets.io/assets/44994daa95884a0b978f9d5d6130c8ff/999f4fa1936f43788413fec37a73e6c1/IMG_3788.JPG",
        "height": 3024,
        "width": 4032
      }
    },
    {
      "id": "lj0-1lmkos",
      "createdAt": "2021-09-16T00:34:56.874Z",
      "updatedAt": "2021-09-16T00:34:56.874Z",
      "publishedAt": "2021-09-16T00:34:56.874Z",
      "revisedAt": "2021-09-16T00:34:56.874Z",
      "name": "たまがった",
      "place": "横浜駅西口",
      "photo": {
        "url": "https://images.microcms-assets.io/assets/44994daa95884a0b978f9d5d6130c8ff/52456c5cc0904c8999a6e08719eef302/IMG_4983.JPG",
        "height": 3024,
        "width": 4032
      }
    },
    {
      "id": "f9sb50o1a7cw",
      "createdAt": "2021-09-16T00:31:57.808Z",
      "updatedAt": "2021-09-16T00:31:57.808Z",
      "publishedAt": "2021-09-16T00:31:57.808Z",
      "revisedAt": "2021-09-16T00:31:57.808Z",
      "name": "らーめん まつや",
      "place": "茅ヶ崎",
      "photo": {
        "url": "https://images.microcms-assets.io/assets/44994daa95884a0b978f9d5d6130c8ff/e6da83cb6032468393485616f0ada16a/IMG_4526.JPG",
        "height": 3024,
        "width": 4032
      }
    },
    {
      "id": "0q1-7rw7n1su",
      "createdAt": "2021-09-16T00:30:01.008Z",
      "updatedAt": "2021-09-16T00:30:24.719Z",
      "publishedAt": "2021-09-16T00:30:01.008Z",
      "revisedAt": "2021-09-16T00:30:24.719Z",
      "name": "維新商店",
      "place": "横浜駅西口",
      "photo": {
        "url": "https://images.microcms-assets.io/assets/44994daa95884a0b978f9d5d6130c8ff/5d53778942da47938e1c69c2a83b9dc6/IMG_4499.JPG",
        "height": 3024,
        "width": 4032
      }
    },
    {
      "id": "9t2ml0zbi0ck",
      "createdAt": "2021-09-16T00:26:48.272Z",
      "updatedAt": "2021-09-16T00:30:13.450Z",
      "publishedAt": "2021-09-16T00:26:48.272Z",
      "revisedAt": "2021-09-16T00:30:13.450Z",
      "name": "吉村家",
      "place": "横浜駅西口",
      "photo": {
        "url": "https://images.microcms-assets.io/assets/44994daa95884a0b978f9d5d6130c8ff/77781624aec34868b3c017d34cdaab32/IMG_4945.JPG",
        "height": 3024,
        "width": 4032
      }
    }
  ],
  "totalCount": 5,
  "offset": 0,
  "limit": 10
}

こういう JSON が返ってくる。

microCMS で面白いのは、オブジェクトのネストみたいなことが出来ること。今回、「まとめ記事」の中に「ラーメン屋」を複数件、コメントとともに埋め込んでいる。

これを工夫すれば、ちょっとくらい難しいデータ構造なら表現できてしまう。「吉村家」を使いまわして「家系ラーメンまとめ」と「横浜駅周辺のラーメン屋まとめ」どちらにも表示できるし、なんなら、コメント添えて、ランキングにすることも出来る。

管理画面って作るのめんどいので、microCMS は「それを代行してくれてる」って考えると素晴らしい。

Next.js でページを作る

  • ラーメン屋 ramen-matome.microcms.io/api/v1/shop
  • まとめ記事 ramen-matome.microcms.io/api/v1/topic

この 2 つのエンドポイントができたので、これを叩いてコンテンツをとってきて描画する Next.js アプリを作る。これはまぁ愚直にコードを書けばいい。

Next.js で面白いのは、SPA っぽくするか、サーバーサイドでレンダリングするか(SSR)、静的 HTML を生成させるか(SSG)を切り替えられることだ。今回は SSG をした。

SPA/SSR 前提で書いてて、SSG にした時に問題となるが、「どのページをレンダリングすればいいのか、そのままでは分からない」という点である。そこで、getStaticPaths を使う。まとめ記事、つまり /topic/[id] にはこんな記事がありますよ〜ってのは topic の一覧を API から取得して指定する。

export const getStaticPaths = async () => {
  const data = await client.get({ endpoint: "topic", queries: { depth: 1 } })

  const paths = data.contents.map((content) => `/topic/${content.id}`)
  return { paths, fallback: false }
}

コンテンツを書く

管理画面ができて、ガワができたら、仮でもいいので最初のコンテンツを入れる。

コンテンツを入れながら、手元の Next.js のページの調整をする。microCMS の管理画面に「ラーメン屋」を投稿。localhost:3000 を確認。今回は目視。

ページを確認する

できたら next buildしよう。

Page                                Size     First Load JS
┌ ● / (809 ms)                      444 B          68.9 kB
├   /_app                           0 B            68.5 kB
├ ○ /404                            272 B          68.7 kB
├ ○ /about                          271 B          68.7 kB
└ ● /topic/[id] (1603 ms)           3.73 kB        72.2 kB
    ├ /topic/rf3_kh8tm (807 ms)
    └ /topic/7l3j1pv3bwh6 (796 ms)
+ First Load JS shared by all       68.5 kB
  ├ chunks/framework.895f06.js      42 kB
  ├ chunks/main.c4f254.js           23.6 kB
  ├ chunks/pages/_app.97eedb.js     2.12 kB
  ├ chunks/webpack.1a8a25.js        729 B
  └ css/ee45ea2207ca4ade5bc9.css    306 kB

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
○  (Static)  automatically rendered as static HTML (uses no initial props)
●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)
   (ISR)     incremental static regeneration (uses revalidate in getStaticProps)

いちいち表示される文字列がおしゃれだよね。

Vercel へデプロイする

いよいよデプロイしよう。こういうのってデプロイが一番大変なんだけど、Vercel を使えば、以下の工程を一気通貫でやってくれる。CI と CDN とサーバーレスの機能を混ぜ込んだみたいだ。

  1. Git レポジトリと連携
  2. ソースの取得
  3. ビルドしてページの生成
  4. HTML ページを CDN へ置く
  5. ドメインにアサイン

master なり に push すると自動的にビルドが走って、いつの間にかデプロイが完了する。楽だし、裏で動いてる感が楽しい。

ドメインに関して、独自ドメインを持ってくることも出来るし、もともとついている vercel.app のサブドメインが使える。ramen-matome.vercel.app にした。SSL も付いてくる。

これ、面白いのは、プレビュー向けのドメインもアドホックに発行してくれる点だ。デプロイの度に ramen-matome-c1wgyfbyl-yusukebe.vercel.app といっためちゃくちゃなホスト名の URL になる。この URL 経由のページに関してはヘッダに x-robots-tag: noindex が付加される。つまりインデックスされない。この URL を検証用にして、それを他のメンバーが見て確認する、なんてことが出来る。

Webhook する

Vercel には SSG したファイルがのるわけだから、microCMS でコンテンツを更新したとしてもそのままではページは変更されない。その点は microCMS から Vercel に Webhook を飛ばして、都度ビルドしなおすってことが出来る。

まとめ

これでパーフェクトだ!2 時間以内にできただろうか!今回作ったサイトは以下で確認出来る。

面白い。

例えば、Vercel は Next.js だけじゃなくて、他のサイトジェネレーターにもデフォルトで対応していて、Hugo もそのひとつなので、このブログを Vercel でビルド&ホストすることも楽勝である。

この 3 つ。まだの人は体験するといいだろう。