Next.jsとmicroCMSでカップラーメン食べた回数カウンターを作る

18 min read読了の目安(約16400字 2

まえがき

https://github.com/hisasann/cup-ramen-counter

コロナの影響で食生活がおうち中心になり、どうしても カップラーメン or カップ焼きそば を食べる機会が増えました。

そのなかで、どれぐらい今食べているのかを把握することは大切(健康のために)かと思い、この自分しか得をしないサイトを構築してみました。

こんなことに結構な工数とお金を使った気がしますが、いったん気にしないことにしてます。

これで、栄養や健康について意識するようになっていけると思います。(いいぞ!)

アーキテクチャの構想

  • Next.js(SSG)
  • microCMS(ヘッドレスCMS)
  • MESH(IoT)
  • Github Actions(JAMStack build)
  • Slack(通知用)
  • お名前.com(ドメイン)
  • AWS
    • S3(ファイル配置)
    • CloudFront(ファイルの配置とドメインを紐づけ)
    • Lambda@edge(Edge側にいる JavaScript)
    • ACM(SSL)
    • Route 53(お名前.comからのドメイン管理)

ちょっと普段触れてなかったものや、実験として面白そうなものをあえて取り入れて勉強がてらゆっくりやっていこうと思いました。

ざっくりインフラは AWS 、データストアは microCMS でいってみようと考えました。

Netlify や Vercel などは普段から触っているので、こちらは使わない方向にしました。

悩んでいたのが、 microCMS の画面で数字をいじって、 AWS の CodePipeline とかで手動で実行して JAMstack デプロイするのもなーと思い、トリガーとなる何かを妄想してました。

そこで知り合いから教えてもらった、 MESH なんかどう?というアドバイスで これだ! となり速攻で購入しました。

MESH はいろんな種類のセンサーを個別で販売している IoT デバイスで、ちょっとお値段ははりますが、シンプルですぐに使えるのが特徴です。

Amazon Dash ButtonとUnityをOSCで繋いでみた - DJ lemon-Sour's diary (prod.hisasann) こんなことをして IoT ボタンと遊んだことがあるのでイメージがつきやすかったです。

このデバイスをトリガーにして、何かしらの方法で microCMS を更新して、何かしらの方法で Next.js でビルドしたファイルを AWS でホスティングさせることに決めました。

アーキテクチャ図

Github Actions のトリガーとして microCMS 側に Webhook を設定できるのですが、全自動というよりは、 MESH の長押しなどの挙動も使いたかったので、自前でトリガーを書きました。

GitHub ActionsへのWebhook通知に対応しました | microCMSブログ

Next.jsでビルドするまで

静的HTMLをエクスポートする

Advanced Features: Static HTML Export | Next.js

"scripts": {
  "build": "next build && next export"
}

Next.js で完全な静的ファイルを生成するには next export が必要になりますので、そちらを scripts に追加しています。

next/imageのエラーが出てしまう問題

$ yarn build

すると、以下のようなエラーが出てしまい SSG モードでのビルドが失敗してしまいました。

- Use any provider which supports Image Optimization (like Vercel).
- Configure a third-party loader in `next.config.js`.
- Use the `loader` prop for `next/image`.

Read more: https://nextjs.org/docs/messages/export-image-api

Next.js + TypeScript + Material-UI で静的HTMLをエクスポートするまで - Qiita

こちらにあるように、 pages/index.tsx

import Image from 'next/image'

の部分が問題のようなので、こちらを今回は使わないようにします。

(画像の最適化、 width や height で指定した画像を生成する機能が使えないのかー)

http-serverモジュールで生成されたファイルを確認する

$ cd out
$ npx http-server

ブラウザで http://localhost:8080/ にアクセスして、 yarn dev したときと同じ画面が開けば OK です。


AWS S3とCloudFrontで静的ファイルを配信する

CloudFrontを使用してS3静的ウェブサイトを提供する手順 | DevelopersIO

CloudFrontS3 をつなぐ部分はここでは端折ります。

すでに良い記事があるので、そちらを貼っておきます。

とはいえ、ここはちょっと面倒な部分ではあるので、少しメモ。

S3バケットを作成する

以下の2つを作りました。

  • CloudFrontアクセスログ用バケット
  • コンテンツ用バケット

CloudFront Distributionを作成する

Default Root Objectindex.html を指定している部分がポイントです。

Lambda@Edgeを使ってリクエストにフックする

以下のような問題があり、

Next.js で SSG したサイトを AWS CloudFront + S3 にデプロイする - Qiita

Hosting a Gatsby site on S3 and CloudFront - Ximedes

トップの / にアクセスが来たときは、上記の CloudFront の Default Root Object 指定しているので、問題ないのですが、 /about//about/index.html に変換する仕組みが CloudFront にないので、 Lambda@Edge を使ってみます。

とはいえ、今回はそこまでここが必須ではないので、ちょいとした勉強がてらになります。

Lambda@Edgeを使っていく

Lambda@EdgeでCloudFrontへのアクセスをいい感じに振り分ける - ZOZO Technologies TECH BLOG

Amazon CloudFrontとAWS Lambda@EdgeでSPAのBasic認証をやってみる | DevelopersIO

こちらもすでに良い記事があるので詳細は割愛。

一点ハマったところは、なぜか Lambda@Edge を使うと 503 になってしまい、ログを確認しにいったら、バージニアのリージョンにログがなくしょんぼりしてたところ、

AWS Lambda@Edgeのログはどこ?AWS Lambda@Edgeのログ出力先について | DevelopersIO

どうやら アクセス元のリージョンに作成 されているようです。

なので、ぼくの場合は東京なので、 東京リージョン の CloudWatch Logs を見に行ったらありました。

結果的には単なる JavaScript エラーが原因でした。(よかったよかった)

Lambda@Edgeのバージョンアップ時に自動でトリガーがコピーされない

新しいバージョンの関数を発行する場合、
Lambda は以前のバージョンから新しいバージョンにトリガーを自動的にコピーしません。
新しいバージョン用のトリガーを再現する必要があります。

Lambda@Edge 用の Lambda 関数の編集 - Amazon CloudFront

なので、 Lambda 関数のコードを更新してバージョンアップしてぼーーっと待ってても、追加されません。

このあたりは AWS CLI や AWS CDK など、 Lambda@Edge を更新したら CloudFront のトリガーも自動的につけるようにプログラムを書く必要があります。

Lambda 関数と CloudFront トリガーをプログラムで作成する - Amazon CloudFront


microCMSをデータストアとして使う

https://microcms.io/

今回はのデータストアとして microCMS を使わせていただきました。

ヘッドレス CMS は海外とかで人気ですが、ちょっと触ったらいつのまにか触らなくなってしまい、向いてないかなーと思っていたら、日本にもヘッドレス CMS があることを思い出し、使い始めた感じです。

結果すごく使いやすかったのと、本家の使い方の記事がひじょうにわかりやすかったので良かったです。

Next.jsのgetStaticPropsでmicroCMSへfetchしてみる

今回は SSG をするのが目的なので、 SPA のようにブラウザにダウンロードされた JavaScript から Ajax するのではなく、 ビルド時 に Ajax するために以下のような fetch をしています。

また、仮に SPA として Ajax をすると今回の場合 microCMS の API-KEY がブラウザで見れてしまうので、それも問題ですね。

このあたりの JAMStack の記事は検索するとわんさかでてくるので、ここでは割愛します。

https://qiita.com/thesugar/items/47ec3d243d00ddd0b4ed

ぼくが使ったコードは以下のとおりです。
SDK に頼っていない分、少し構文は煩雑ですね。

export async function getStaticProps() {
  const key = {
    headers: {'X-API-KEY': process.env.API_KEY ?? ''},
  };
  const res = await fetch('https://hisasann.microcms.io/api/v1/cup-ramen-counter', key);
  const json = await res.json()

  return {
    props: {
      count: json.count,
    },
  }
}

ちょうどわからない点があって、こんなことをつぶやきましたが、

https://twitter.com/hisasann/status/1396405309963313154?s=20

本家の方がすぐにレスしてくだしました。感謝!

https://twitter.com/shibe97/status/1396406452101664770?s=20

microcms-js-sdkを使った場合のコード

https://zenn.dev/hiro08gh/articles/a953f46a839ee7

こちらの記事が参考になります。

import { createClient } from "microcms-js-sdk";

export const client = createClient({
  serviceDomain: 'service-domain',  // serviceDomainはXXXX.microcms.ioの場合、XXXXの部分になります。
  apiKey: 'api-key',
});

のちのちこの module を使った版にせねばっ!


MESHをIoTデバイスとして使う

MESHとは?

https://meshprj.com/jp/

上のほうでも書きましたが、 MESH という IoT デバイスを教えてもらい、とにかく簡単にボタンや人感センサー、明るさや動きなどなど、ほぼ網羅されているセンサーたちを使うことができ、ここからいろんなことができるだろうなーと思いまずは ボタン を買ってみました。

Slask 通知や Twitter 投稿などのはじめから使える連携に加え、自分で JavaScript を書くことでさらにいろんなことができるようになります。

自分で npm モジュールを入れたりはどうやらできないようですが、既存の SDK である程度賄ってくれています。

https://meshprj.com/sdk/doc/ja/

画像はいろんなアプリを使ってる様子。

PostmanからmicroCMSにGETしてみる

ここのコーナーは以下の記事にたいへん助けていただきました。

https://qiita.com/shmiki/items/16af9fecbb60d155a36e

Postman をインストールして microCMS に GET してみます。

このように 200 が返ってきたら OK ですね。

ぼくは count という値だけを microCMS で管理しているので戻り値に "count": 2 と値が入っています。

また右側に懐かしの jQuery の $.ajax のコードを表示していますが、これがのちのち MESH 用の JavaScript を書くときに活きてきます。

PostmanからmicroCMSにPATCHしてみる

microCMS で使われている更新用の http メソッド PATCH を Postman から叩いたのが上の図です。

まずはこれでデータの更新ができるところまでを確認しました。

ちなみにですが、 microCMS で PATCH 経由で更新するのは月 100 回までが無料で、それ以上は有料です。

MESHからカウンターをいじってみる

https://meshprj.com/sdk/mypage/#

上記のサイトから Create New Block をクリックして、 とりあえず Input ConnectorOutput Connector をプラスしておく。

これをしておかないと、前のブロックから値をもらったり、次につなげたりができない。

カウンターを加算するレシピを作る

microCMS から現在の値を取得し、その値に1加算し、その値で更新する。

この場合、それぞれの処理を別々のレシピにしてみてわかったんですが、 MESH SDK が用意してる ajax 関数はもちろん非同期ですが、完了してないのに次のレシピに行ってしまうのを防ぐ方法が提供されてました。

それが、

return {
  resultType : "pause"
};

で待たせておいて、コールバック関数で

callbackSuccess( {
  resultType : "continue"
});

を呼ぶことで Promise のようなことを実現している。

ajax という関数はグローバルに使えるので、こういった関数群は SDK が用意してくれています。

https://meshprj.com/sdk/doc/ja/

GETレシピ

runtimeValues というのがひとつのレシピ内で値を引き渡せるためのオブジェクトになります。

これを使って、 Execute -> Result へ値を渡しています。

Execute

ajax ({
  url: "https://hisasann.microcms.io/api/v1/cup-ramen-counter",
  method: "GET",
  headers: {
    "X-API-KEY": "xxxx",
    "cache-control": "no-cache"
  },
  timeout : 5000,
  success: function(contents){
    log(JSON.stringify(contents));
    runtimeValues.count = contents.count;

    callbackSuccess( {
      resultType : "continue",
      runtimeValues : runtimeValues
    });
  },
  error : function (request, errorMessage) {
    log("request:" + JSON.stringify(request));
    log("errorMessage:" + JSON.stringify(errorMessage));
    callbackSuccess({
      resultType : "continue",
      runtimeValues : runtimeValues
    });
  }
});

return {
  resultType : "pause"
};

Result

return {
  messageValues: {
    count: runtimeValues.count
  },
  resultType : "continue"
};

さらに messageValues が別のレシピに渡っていくオブジェクトなので、ここに count を渡すことで、次のレシピでいじれるイメージです。

PATCHレシピ

こちらも同じ ajax 関数で通信していきます。

ここで microCMS への更新は完了です。

let count = messageValues.count + 1;
ajax ({
  url: "https://hisasann.microcms.io/api/v1/cup-ramen-counter",
  method: "PATCH",
  headers: {
    "X-WRITE-API-KEY": "xxxxxxxxxxx",
    "content-type": "Application/json",
    "cache-control": "no-cache"
  },
  "data": JSON.stringify({
    "count": count
  }),
  timeout : 5000,
  success: function(contents){
    log(JSON.stringify(contents));
    callbackSuccess( {
      resultType : "continue",
      runtimeValues : runtimeValues
    });
  },
  error : function (request, errorMessage) {
    log("request:" + JSON.stringify(request));
    log("errorMessage:" + JSON.stringify(errorMessage));
    callbackSuccess({
      resultType : "continue",
      runtimeValues : runtimeValues
    });
  }
});

return {
  resultType : "pause"
};

ビルドするレシピを作る

Github Actions で使える workflow_dispatch というのを使い、 GHA を外部 API から実行できるようになっています。

curl すると以下のようになります。

curl \
  -XPOST \
  -H "Authorization: token xxxxxxxxxxxxxx" \
  -H "Accept: application/vnd.github.v3+json" \
  https://api.github.com/repos/hisasann/cup-ramen-counter/actions/workflows/xxxxxxx/dispatches \
  -d '{"ref":"main"}'

https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/

ここまでくれば http リクエストで Github Actions の workflow を実行できます。

https://qiita.com/shmiki/items/16af9fecbb60d155a36e

Buildレシピ

上記の curl の内容を ajax 関数で使いやすいように書いてみました。

ajax ({
  url: "https://api.github.com/repos/hisasann/cup-ramen-counter/actions/workflows/xxxxxx/dispatches",
  method: "POST",
  headers: {
    "Authorization": "token xxxxxxxxxxxxxxxxxxxxxxxxx",
    "Accept": "application/vnd.github.v3+json"
  },
  "data": JSON.stringify({
    "ref": "main"
  }),
  timeout : 5000,
  success: function(contents){
    log(JSON.stringify(contents));
    callbackSuccess( {
      resultType : "continue",
      runtimeValues : runtimeValues
    });
  },
  error : function (request, errorMessage) {
    log("request:" + JSON.stringify(request));
    log("errorMessage:" + JSON.stringify(errorMessage));
    callbackSuccess({
      resultType : "continue",
      runtimeValues : runtimeValues
    });
  }
});

return {
  resultType : "pause"
};

Github ActionsからNext.jsのSSGしてS3にrsyncする

https://docs.github.com/en/rest/reference/actions#create-a-workflow-dispatch-event

https://swfz.hatenablog.com/entry/2020/07/10/201136

https://swfz.hatenablog.com/entry/2020/01/23/080000

cup-ramen-counter/build-jamstack.yml at main · hisasann/cup-ramen-counter

今回はこんな感じになっております。

workflow_dispatch という、独自に実行できる Github Actions を使ってみています。

name: build-jamstack

on:
  push:
    branches:
      - main
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 60

    strategy:
      matrix:
        node-version: [12.16.1]

    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: ${{ matrix.node-version }}

      - name: Display version of Node.js, npm, Yarn
        run: |
          node -v
          npm -v
          yarn --version

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1

      - name: Get yarn cache
        id: yarn-cache
        run: echo "::set-output name=dir::$(yarn cache dir)"
      - uses: actions/cache@v2
        with:
          path: ${{ steps.yarn-cache.outputs.dir }}
          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-yarn-
            - uses: actions/cache@v2
              with:
                path: node_modules
                key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
      - run: yarn
      - run: yarn build
        env:
          API_KEY: ${{ secrets.API_KEY }}

      - name: Upload dist files to S3
        env:
          S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }}
        run: aws s3 sync ./out s3://$S3_BUCKET_NAME/ --quiet

cup-ramen-counter/build-jamstack.yml at main · hisasann/cup-ramen-counter

お名前.comからRoute53で管理しつつSSL化

実は今回、「あえてドメインを先にとってしまい、とってしまったからには何か作らねばドリブン」を採用しているので、ドメインを持った状態からスタートしています。

たまたま お名前.com でぼくが欲しいドメインが安く購入できたので、それを使いたいのですが、インフラは AWS なので Route53 のネームサーバーを使ったり、 ACMSSL化 したりゴニョゴニョしています。

ネームサーバーをお名前.comに記載する

以下のような Route53 で指定されたネームサーバーをお名前.comに指定します。

こちらの値は仮です。

自分の Route53 に記載されているドメインを使ってください。

ns-123.hoge-10.co.uk.
ns-321.foo-43.net.
ns-456.fuga-33.org.
ns-678.piyo-28.com.

AWS Certificate Manager(ACM)でSSL化

あとはお名前.com側で名前と CNAME と値を入れます。

Route 53 側に Aレコード で、 CloudFront の URL(xxxxxx.cloudfront.net) を忘れないように追加しておきましょう。

https://twitter.com/hisasann/status/1401177587876044807?s=20

ちょっとハマりました。

https://dev.classmethod.jp/articles/web-ssl-202009/

https://deep.tacoskingdom.com/blog/12

https://blog.apar.jp/web/6366/

ここまで来たらドメインを https で開いてくれるでしょう!

メモ:CloudFrontのキャッシュが効いちゃう問題

CDN はキャッシュしてくれるのがメリットですが、

microCMS の値を変える -> JAMStack ビルドする -> CloudFront が古い html をキャッシュしてるので、更新されない

という感じになってしまうので、いったん今はまだ開発途中で、キャッシュバージをするのも面倒なので TTL をいじってキャッシュしないようにしました。

https://aws.amazon.com/jp/premiumsupport/knowledge-center/prevent-cloudfront-from-caching-files/

https://dev.classmethod.jp/articles/aws-amazon-cloudfront-deleting-cache-by-invalidation/

https://dev.classmethod.jp/articles/cloudfront-ttl/

あとがき

ひとつのボタンから始まる壮大な世界ですが、まだやりたいことは多々あります。

https://tailwindcss.com/

で、シンプルにおしゃれにしたい。

データを日毎に DynamoDB に保存して、統計をとりグラフ化してみたい。

などなど。

まだできていないことが多いので、それらはこの記事では扱わず別の記事で書いていきます。

どうやらこの記事も長くなってしまったようです。

ではでは、この記事がどなたかの役に立てば!

https://github.com/hisasann/cup-ramen-counter

参考記事

Next.jsの静的サイト生成(SSG)について確認 - わくわくBank

Next.jsを使うべき5つの理由 + 実装Tips - Qiita

CloudFrontを使用してS3静的ウェブサイトを提供する手順 | DevelopersIO

お手軽IoTツール 「MESH」 ~始め方からREST APIを叩いてみるところまでやってみた~ - Qiita

この記事に贈られたバッジ