Blogの記事を何本か公開しました。
ただ、どれくらい読んでもらえているのかは、まだちゃんと把握できていません。
Google Analyticsは入っているものの、数字もほとんど見ていないままでした。
閲覧数はまた別で確認するとして、記事を読んだ人が「読んだよ」くらいの小さな反応を残せる場所があってもいいなと思いました。
そこで、Blogにかなりシンプルな「いいね」を付けてみることにしました。
押したら増えて、もう一度押したら戻るくらいでいい
最初に考えたUIはかなり単純です。
未Likeなら、
♡ 0
押したら、
♥ 1
もう一度押したら、
♡ 0
これだけです。
大きなボタンにしたり、「いいねしてください」と書いたりはしませんでした。
記事を読み終えたところに小さく置いて、反応したい人だけ押せるくらいで十分かなと。
あと、自分は間違えて押したものを戻せないUIがちょっと気になります。
なので今回は、 同じボタンをもう一度押せば取り消せること を最初から条件にしました。
本家の実装を見て、「取り消せる」も欲しくなった
このBlogは、 おとよさん が開発されている astro-notion-blog をフォークして使っています。
Likeについても、本家側に実装例があります。
本家の記事では、Notion DBにLikeの数値を持っておき、ボタンを押したら現在のLike数へ1を足して更新する形です。
さらに、swimuさんの記事も参考にしました。
astro-notion-blogの改修をして、ハートがいい感じに送れるようにした
こちらは、astro-notion-blog側のキャッシュに古いLike値が残ることで、連続して押したときに数がうまく増えないことがある点を調整されています。
Notionから最新のLike値を取り直して、その値を基準に増やす形です。
どちらも「押したら1つ増える」というシンプルなLikeで、少なくとも記事で紹介されている実装には、Like済みのブラウザを覚えてもう一度押したら戻す処理は入っていませんでした。
自分はそこだけ少し条件が違いました。
押したLikeを戻したい。
それと、同じブラウザから何度押しても1票にしたい。
この2つを入れるなら、単純に「Likeの合計値」だけを持つより、1票ずつ持った方が扱いやすそうでした。
保存先はNotionの数値ではなくD1へ
今回使ったのは Cloudflare Pages Functions + D1 です。
Blogの記事そのものは、これまで通り静的生成のまま。
Likeの取得と更新だけ、Pages FunctionsのAPIへ送ります。
Cloudflare Pages Functionsは functions ディレクトリの構成からrouteが作られるので、今回は functions/api/likes.js を /api/likes として使っています。
Cloudflare Pages Functions - Routing
D1側はかなり小さなテーブルです。
CREATE TABLE IF NOT EXISTS article_likes (
slug TEXT NOT NULL,
voter_id TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (slug, voter_id)
); 1記事 × 1つのtokenを1行として持つ形です。
Likeすると行を追加して、取り消すとその行を削除します。
同じ slug と voter_id は複合主キーで重複できないので、同じtokenから何度Like処理が飛んでも1票だけです。
今回は「取り消す」「同じブラウザでは1票」という条件も入れたかったので、それに合わせて保存の形も変えることにしました。
ログインなしで、1記事1票だけ覚える
Blogにログイン機能を増やすつもりはなかったので、ブラウザ側では記事ごとにランダムなUUIDを作って、localStorageへ保存することにしました。
記事Aと記事Bでは別tokenです。
D1に保存するのは、
- slug
- ランダムなvoter_id
- 作成日時
だけです。
このtokenは「このブラウザでは、この記事にLike済みか」を覚えるためだけに使います。
なので、ユーザー登録やメールアドレスのような情報は必要ありません。
localStorageを消すと以前のtokenも消えるので、そのブラウザから古いLikeを取り消せなくなる制約は残ります。
ここを完全に解決しようとすると、今度はアカウントや別の識別方法が必要になってきます。
個人Blogの小さなリアクションとしては、そこまでしなくていいかなと思っています。
更新した直後の数がズレないように
Likeは、
INSERT OR IGNORE
取り消しは、
DELETE
です。
そのあと同じリクエスト内で、現在のLike数と自分のliked状態を読み直して返します。
D1ではSessions APIの first-primary を使いました。
Cloudflareのドキュメントでは、 first-primary を使うと最初のqueryをprimaryへ送り、そのsession内の後続queryはsequential consistencyが保たれるとされています。
ここまでやるのは少し丁寧すぎる気もします。
でも、Likeを押した直後だけ表示が古い、みたいな動きは地味に気になりそうなので。。
更新したあとに返す数は、その更新が反映された状態になるようにしておきました。
PreviewのはずがProductionに出てしまった。。
コード側の確認が終わって、次はCloudflare上でPreviewを試すことにしました。
D1に tsukurun-blog-likes を作成して、 article_likes テーブルを作成。
Preview環境には、
LIKES_DB → tsukurun-blog-likes
というD1 Bindingを追加しました。
ここまではよかったのですが、Branch Controlの設定を触っている途中で、 feature branchをProduction branchとして設定してしまいました。。
その状態でデプロイを再試行したので、
feat/reversible-blog-likes
がProduction扱いでデプロイされる状態に。
幸いProduction側には LIKES_DB をまだ設定しておらず、Like APIの初回取得に失敗した場合はUI自体を非表示にする作りにしていたので、Blog本文を巻き込む形にはなりませんでした。
一度 main の直前のProduction deploymentへロールバックして、
- Production:
main - Preview:
feat/reversible-blog-likes - D1 BindingはPreviewだけ
に設定し直しました。
そのあと空commitを1つpushして、ようやく意図したPreview deploymentを作れました。
こういうところ、画面を見ながら設定していると普通に間違えますね。。
別のブラウザでも、1票ずつ分かれるか
Previewの記事でまず、
♡ 0 → ♥ 1 → ♡ 0 → ♥ 1
まで確認しました。
ページを更新しても ♥ 1 の状態が戻ることも確認。
さらに別のブラウザから同じ記事を開いてLikeすると、表示は2件になりました。
D1 Studioで確認すると、同じ記事のslugに対して異なる voter_id が2行。
想定していた形で保存されていました。
テストが終わったあとは、その2件を削除して0件へ戻しています。
ここまで確認できたので、シンプル版としてはだいぶ形になってきました。
まずは、この小さいハートから
最後にProduction側にもD1 Bindingを追加して、Pull Requestをmainへmergeしました。
本番サイトでも ♡ 0 → ♥ 1 → ♡ 0 と、Likeと取り消しが動くところまで確認できました。
Previewの設定で一度Productionへ出してしまったこともあったので、実際の本番でちゃんと動いたところまで見られて、ちょっと安心しました。。
ただ、このLikeだけで「Blogがどれくらい読まれているか」が分かるわけではありません。
閲覧数そのものはGoogle Analyticsなどで別に確認する必要がありますし、Likeを押さずに読んでくれる人の方が多いと思います。
それでも、これまでは記事を公開したあとに残る反応がほとんど見えていませんでした。
なので、まずは小さなハートが1つ増えるくらいでも、ちょっと嬉しいかもしれません。
このあとBlog全体の構成も見直したいと思っていて、そのときには「よく読まれている記事」と「Likeが多い記事」を少し違う見せ方にするのも面白そうだなと考えています。
まずはこの小さいハートを置いた状態で、しばらく使ってみます。
