Next.jsとmicroCMSでカップラーメン食べた回数カウンターを作る
まえがき
コロナの影響で食生活がおうち中心になり、どうしても カップラーメン 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
CloudFront と S3 をつなぐ部分はここでは端折ります。
すでに良い記事があるので、そちらを貼っておきます。
とはいえ、ここはちょっと面倒な部分ではあるので、少しメモ。
S3バケットを作成する
以下の2つを作りました。
- CloudFrontアクセスログ用バケット
- コンテンツ用バケット
CloudFront Distributionを作成する
Default Root Object
で index.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をデータストアとして使う
今回はのデータストアとして microCMS を使わせていただきました。
ヘッドレス CMS は海外とかで人気ですが、ちょっと触ったらいつのまにか触らなくなってしまい、向いてないかなーと思っていたら、日本にもヘッドレス CMS があることを思い出し、使い始めた感じです。
結果すごく使いやすかったのと、本家の使い方の記事がひじょうにわかりやすかったので良かったです。
Next.jsのgetStaticPropsでmicroCMSへfetchしてみる
今回は SSG をするのが目的なので、 SPA のようにブラウザにダウンロードされた JavaScript から Ajax するのではなく、 ビルド時 に Ajax するために以下のような fetch をしています。
また、仮に SPA として Ajax をすると今回の場合 microCMS の API-KEY がブラウザで見れてしまうので、それも問題ですね。
このあたりの JAMStack の記事は検索するとわんさかでてくるので、ここでは割愛します。
ぼくが使ったコードは以下のとおりです。
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,
},
}
}
ちょうどわからない点があって、こんなことをつぶやきましたが、
本家の方がすぐにレスしてくだしました。感謝!
microcms-js-sdkを使った場合のコード
こちらの記事が参考になります。
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とは?
上のほうでも書きましたが、 MESH という IoT デバイスを教えてもらい、とにかく簡単にボタンや人感センサー、明るさや動きなどなど、ほぼ網羅されているセンサーたちを使うことができ、ここからいろんなことができるだろうなーと思いまずは ボタン を買ってみました。
Slask 通知や Twitter 投稿などのはじめから使える連携に加え、自分で JavaScript を書くことでさらにいろんなことができるようになります。
自分で npm モジュールを入れたりはどうやらできないようですが、既存の SDK である程度賄ってくれています。
画像はいろんなアプリを使ってる様子。
PostmanからmicroCMSにGETしてみる
ここのコーナーは以下の記事にたいへん助けていただきました。
Postman をインストールして microCMS に GET してみます。
このように 200 が返ってきたら OK ですね。
ぼくは count という値だけを microCMS で管理しているので戻り値に "count": 2 と値が入っています。
また右側に懐かしの jQuery の $.ajax のコードを表示していますが、これがのちのち MESH 用の JavaScript を書くときに活きてきます。
PostmanからmicroCMSにPATCHしてみる
microCMS で使われている更新用の http メソッド PATCH を Postman から叩いたのが上の図です。
まずはこれでデータの更新ができるところまでを確認しました。
ちなみにですが、 microCMS で PATCH 経由で更新するのは月 100 回までが無料で、それ以上は有料です。
MESHからカウンターをいじってみる
上記のサイトから Create New Block
をクリックして、 とりあえず Input Connector と Output Connector をプラスしておく。
これをしておかないと、前のブロックから値をもらったり、次につなげたりができない。
カウンターを加算するレシピを作る
microCMS から現在の値を取得し、その値に1加算し、その値で更新する。
この場合、それぞれの処理を別々のレシピにしてみてわかったんですが、 MESH SDK が用意してる ajax 関数はもちろん非同期ですが、完了してないのに次のレシピに行ってしまうのを防ぐ方法が提供されてました。
それが、
return {
resultType : "pause"
};
で待たせておいて、コールバック関数で
callbackSuccess( {
resultType : "continue"
});
を呼ぶことで Promise のようなことを実現している。
ajax という関数はグローバルに使えるので、こういった関数群は SDK が用意してくれています。
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"}'
ここまでくれば http リクエストで Github Actions の workflow を実行できます。
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する
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 のネームサーバーを使ったり、 ACM で SSL化 したりゴニョゴニョしています。
ネームサーバーをお名前.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 で開いてくれるでしょう!
メモ:CloudFrontのキャッシュが効いちゃう問題
CDN はキャッシュしてくれるのがメリットですが、
microCMS の値を変える -> JAMStack ビルドする -> CloudFront が古い html をキャッシュしてるので、更新されない
という感じになってしまうので、いったん今はまだ開発途中で、キャッシュバージをするのも面倒なので TTL をいじってキャッシュしないようにしました。
あとがき
ひとつのボタンから始まる壮大な世界ですが、まだやりたいことは多々あります。
で、シンプルにおしゃれにしたい。
データを日毎に DynamoDB に保存して、統計をとりグラフ化してみたい。
などなど。
まだできていないことが多いので、それらはこの記事では扱わず別の記事で書いていきます。
どうやらこの記事も長くなってしまったようです。
ではでは、この記事がどなたかの役に立てば!
参考記事
Next.jsの静的サイト生成(SSG)について確認 - わくわくBank
Next.jsを使うべき5つの理由 + 実装Tips - Qiita
Discussion
参考になりました!ありがとうございます😊
良かったです!