[{"data":1,"prerenderedAt":238},["ShallowReactive",2],{"/blog/aws_like_button/":3,"related-技術系-/blog/aws_like_button":212},{"id":4,"title":5,"body":6,"category":194,"coverImage":195,"date":196,"description":197,"draft":198,"extension":199,"meta":200,"navigation":201,"path":202,"rawbody":203,"seo":204,"stem":205,"tags":206,"__hash__":211},"blog/blog/aws_like_button/index.md","Hugo + StackのブログにAWSでいいねボタン実装した",{"type":7,"value":8,"toc":180},"minimark",[9,13,24,31,34,40,56,61,72,86,103,110,113,116,120,123,126,132,135,138,141,148,159,162,165,168,171,174,177],[10,11,12],"h2",{"id":12},"概要",[14,15,16,23],"p",{},[17,18,22],"a",{"href":19,"rel":20},"https://blog.bokukoha.dev/p/hugo--stack%E3%81%AE%E3%83%96%E3%83%AD%E3%82%B0%E3%81%AB%E3%82%B7%E3%82%A7%E3%82%A2%E3%83%9C%E3%82%BF%E3%83%B3%E5%AE%9F%E8%A3%85%E3%81%97%E3%81%9F/",[21],"nofollow","以前実装したシェアボタン","に引き続き、いいね機能があってもいいかもな～となったのでそれを実装したメモとその苦労話的な感じのものです。6月頃に参加したAWS summitでAWSの簡単な使い方なんかを学習してきたので、いい機会だし早速使ってみようということで、今回はAWSの各種機能を活用していいねボタンを実装してみました。",[14,25,26],{},[27,28],"img",{"alt":29,"src":30},"成果物","https://images.bokukoha.dev/blog/aws_like_button/1.jpg",[10,32,33],{"id":33},"中身",[14,35,36],{},[37,38,39],"strong",{},"機能",[41,42,43,47,50,53],"ul",{},[44,45,46],"li",{},"ユーザーがいいねボタンを押す",[44,48,49],{},"いいねボタンが押されたらDBに記録する",[44,51,52],{},"DBに記録されたらdiscordに通知を飛ばす",[44,54,55],{},"いいねが押された回数をDBから取得して、ブログ側で表示する",[14,57,58],{},[37,59,60],{},"使ったもの",[41,62,63,66,69],{},[44,64,65],{},"Lambda",[44,67,68],{},"API Gateway",[44,70,71],{},"DynamoDB",[14,73,74,75,80,81,85],{},"とまあこのように、いたってシンプルな感じの機能です。",[17,76,79],{"href":77,"rel":78},"https://www.bokukoha.dev/blog/add-share-button/",[21],"以前のシェアボタン","の時同様、",[82,83,84],"code",{},"./layouts/partials","内にhtmlファイルを作り、適当にボタン部分とスタイルを作り、AIに書いてもらったAPIを叩くコードを張り付けてフロント側は完成です。",[87,88,89,93],"details",{},[90,91,92],"summary",{},"弊ブログで使っているコード例（JS）",[94,95,100],"pre",{"className":96,"code":98,"language":99},[97],"language-text","\u003Cscript>\n    document.addEventListener('DOMContentLoaded', () => {\n        const apiEndpoint = '{{ site.Params.apiEndpoint }}';\n        const apiKey = '{{ site.Params.apiKey }}'; // 設定ファイルからAPIキーを読み込む\n        const articleId = '{{ .RelPermalink }}';\n\n        const likeButton = document.getElementById('like-button');\n        const likeCountSpan = document.getElementById('like-count');\n\n        if (!likeButton) return;\n\n        // リクエストに含めるヘッダーを作成\n        const requestHeaders = {\n            'x-api-key': apiKey\n        };\n\n        // ページ読み込み時のいいね数取得（ヘッダー付きでfetch）\n        fetch(`${apiEndpoint}/${encodeURIComponent(articleId)}`, { headers: requestHeaders })\n            .then(res => {\n                if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);\n                return res.json();\n            })\n            .then(data => {\n                likeCountSpan.textContent = data.likes || 0;\n            })\n            .catch(err => console.error('いいね数取得失敗:', err));\n\n        // ボタンクリック時の処理\n        likeButton.addEventListener('click', () => {\n            likeCountSpan.textContent = parseInt(likeCountSpan.textContent) + 1;\n\n            // いいねを送信（ヘッダー付きでfetch）\n            fetch(`${apiEndpoint}/${encodeURIComponent(articleId)}`, {\n                method: 'POST',\n                headers: requestHeaders // ここにもヘッダーを追加\n            })\n            .then(res => {\n                if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);\n                return res.json();\n            })\n            .then(data => {\n                likeCountSpan.textContent = data.likes;\n            })\n            .catch(err => {\n                likeCountSpan.textContent = parseInt(likeCountSpan.textContent) - 1;\n                console.error('いいね送信失敗:', err);\n            });\n        });\n    });\n\u003C/script>\n","text",[82,101,98],{"__ignoreMap":102},"",[14,104,105,106,109],{},"上のコード内にもあるように、",[82,107,108],{},"{{ site.Params.hogehoge }}","を使ってブログのconfig.yamlに追加した要素を読み込んでくれるようにしておきました。コード内に直で書こうが、configから読むようにしようが、githubで公開している以上はエンドポイントもキーも見られちゃうわけなんですが、気持ち悪かったのでconfigから読む形式にしました。",[10,111,112],{"id":112},"苦労した点",[14,114,115],{},"前述のフロントエンドの方で苦労したことは一切と言っていいほどなかったんですが、今回のメインであるAWSの方は結構詰まった点が多かったのでそれを書いていきます。",[117,118,119],"h3",{"id":119},"与太話1",[14,121,122],{},"LambdaのコードなんかはAIにベースを書かせて、少しだけ自分で手直しをしてあげただけでテストで動いてくれたので良かったんですが、API Gatewayがかなりの曲者でした。いいねボタンを実装した当初は、設定が面倒くさかったこともあり、キーを使わずエンドポイントURLだけでapiを叩けるようにしていました。",[14,124,125],{},"機能が完成して開発サーバーでテストOK、いざ外部公開用の本番で公開してmisskey（SNS）にてその旨を告知したところ、知り合いの某エンジニアに、エンドポイントだけでレート制限かかってないのをいいことにAPIを一生叩き続けていいねスパムを送るbotを作られ、いいね数がとんでもないことになってしまいました。",[14,127,128],{},[27,129],{"alt":130,"src":131},"他の記事もこれでヤバかった","https://images.bokukoha.dev/blog/aws_like_button/2.jpg",[14,133,134],{},"流石にこれはマズイと思ったのでAPIキーを発行しレート制限をかけ解決したんですが、API gatewayの使用量プランとキーとAPI本体を紐づける作業がなかなか複雑だったのがつらかったです。今はもう理解したので詰まることはないんですが、初見だとAPIに対応するキーを発行して、使用量プランを作成して、プランとキーを紐づけて……という作業が難解でした。",[14,136,137],{},"API側でレート制限をかけるだけではなく、フロントのコード側でも10回いいねが押されたらそのブラウザからは押せないようにしたので多少のスパム対策にはなったのかなという感じです。",[117,139,140],{"id":140},"与太話2",[14,142,143,144,147],{},"つい先日記事を投稿した際、何故かいいねボタンが壊れていいねが取得できていないことに気が付きました。ブラウザの開発者モードでコンソールを見てみると、",[82,145,146],{},"No 'Access-Control-Allow-Origin' header","のエラー。これはAPI gatewayのCORSというアクセス制限をかける機能が原因なのではないか？と調べる中でわかったので、CORSを再設定しました。",[14,149,150,151,154,155,158],{},"これで治ったかと思うと今度はコンソールに",[82,152,153],{},"429 Too many request","のエラー。1回押すだけでエラーが出てしまうのでこれは異常だと思い、使用量プランをオフにしたり、そもそもプランを削除したりしましたがそれでもエラーが消えず頭を抱えてしまいました。最終的にAPIキーを再発行することで絵429は消えましたが、今度は",[82,156,157],{},"403 forbidden","のエラー。",[14,160,161],{},"結論から言うとこれは再発行したAPIキーをAPI本体に紐づけることを忘れていたことによる認証エラーだったのですが、これに気付くまでにそこそこ時間がかかりました。",[10,163,164],{"id":164},"おわりに",[14,166,167],{},"AWSの無料枠で実用的なものを作る経験はそんなになかったので勉強になりました。以前作ったS3へのマイクラデータのバックアッププラグインなんかはS3だけしか使っておらず、今回のようにいくつかの機能を連携させるのは初めてで楽しかったです。",[14,169,170],{},"エラーで頭を抱えたと書きましたが、この手の問題解決は結構楽しいので意外と好きだったりします。本業で使ってる方々から見ると結構しょぼい機能な気がしますが、自分的に満足のいくものを作れたので良かったです。",[14,172,173],{},"ここまで駄文を読んでくださりありがとうございました。良ければ下のいいねボタンを押していってください。僕が喜びます！",[117,175,176],{"id":176},"おまけ",[14,178,179],{},"今回のいいね機能はLambdaを活用したサーバーレスなコード実行という点でAWSを使うメリットが明確なものだったので勉強にちょうどよかったのが助かりました。実は以前EC2やVPCなんかを使ってマイクラサーバーを構築する計画があったんですが、EC2の無料枠がしょっぱ過ぎた（RAM1GBのVMしか使えない；；）ことが理由で諦めていました。やっぱり趣味で動かすゲームサーバーなんかはオンプレでやるのが一番楽なのかな～なんて思ったり。",{"title":102,"searchDepth":181,"depth":181,"links":182},4,[183,185,186,191],{"id":12,"depth":184,"text":12},2,{"id":33,"depth":184,"text":33},{"id":112,"depth":184,"text":112,"children":187},[188,190],{"id":119,"depth":189,"text":119},3,{"id":140,"depth":189,"text":140},{"id":164,"depth":184,"text":164,"children":192},[193],{"id":176,"depth":189,"text":176},"技術系","https://images.bokukoha.dev/blog/aws_like_button/main.jpg","2025-09-21","以前実装したシェアボタンに引き続き、いいね機能があってもいいかもな～となったのでそれを実装したメモとその苦労話的な感じのものです。",false,"md",{},true,"/blog/aws_like_button","---\ntitle: \"Hugo + StackのブログにAWSでいいねボタン実装した\"\ndate: \"2025-09-21\"\ncategory: \"技術系\"\ntags:\n  - \"AWS\"\n  - \"API\"\n  - \"Hugo\"\n  - \"Stack Theme\"\ncoverImage: \"https://images.bokukoha.dev/blog/aws_like_button/main.jpg\"\ndraft: false\ndescription: \"以前実装したシェアボタンに引き続き、いいね機能があってもいいかもな～となったのでそれを実装したメモとその苦労話的な感じのものです。\"\n---\n\n\n## 概要\n[以前実装したシェアボタン](https://blog.bokukoha.dev/p/hugo--stack%E3%81%AE%E3%83%96%E3%83%AD%E3%82%B0%E3%81%AB%E3%82%B7%E3%82%A7%E3%82%A2%E3%83%9C%E3%82%BF%E3%83%B3%E5%AE%9F%E8%A3%85%E3%81%97%E3%81%9F/)に引き続き、いいね機能があってもいいかもな～となったのでそれを実装したメモとその苦労話的な感じのものです。6月頃に参加したAWS summitでAWSの簡単な使い方なんかを学習してきたので、いい機会だし早速使ってみようということで、今回はAWSの各種機能を活用していいねボタンを実装してみました。\n\n![成果物](https://images.bokukoha.dev/blog/aws_like_button/1.jpg)\n\n## 中身\n\n\u003Cstrong>機能\u003C/strong>\n- ユーザーがいいねボタンを押す\n- いいねボタンが押されたらDBに記録する\n- DBに記録されたらdiscordに通知を飛ばす\n- いいねが押された回数をDBから取得して、ブログ側で表示する\n\n\u003Cstrong>使ったもの\u003C/strong>\n- Lambda\n- API Gateway\n- DynamoDB\n\nとまあこのように、いたってシンプルな感じの機能です。[以前のシェアボタン](https://www.bokukoha.dev/blog/add-share-button/)の時同様、`./layouts/partials`内にhtmlファイルを作り、適当にボタン部分とスタイルを作り、AIに書いてもらったAPIを叩くコードを張り付けてフロント側は完成です。\n\n\u003Cdetails>\n\u003Csummary>弊ブログで使っているコード例（JS）\u003C/summary>\n\n```\n\u003Cscript>\n    document.addEventListener('DOMContentLoaded', () => {\n        const apiEndpoint = '{{ site.Params.apiEndpoint }}';\n        const apiKey = '{{ site.Params.apiKey }}'; // 設定ファイルからAPIキーを読み込む\n        const articleId = '{{ .RelPermalink }}';\n\n        const likeButton = document.getElementById('like-button');\n        const likeCountSpan = document.getElementById('like-count');\n\n        if (!likeButton) return;\n\n        // リクエストに含めるヘッダーを作成\n        const requestHeaders = {\n            'x-api-key': apiKey\n        };\n\n        // ページ読み込み時のいいね数取得（ヘッダー付きでfetch）\n        fetch(`${apiEndpoint}/${encodeURIComponent(articleId)}`, { headers: requestHeaders })\n            .then(res => {\n                if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);\n                return res.json();\n            })\n            .then(data => {\n                likeCountSpan.textContent = data.likes || 0;\n            })\n            .catch(err => console.error('いいね数取得失敗:', err));\n\n        // ボタンクリック時の処理\n        likeButton.addEventListener('click', () => {\n            likeCountSpan.textContent = parseInt(likeCountSpan.textContent) + 1;\n\n            // いいねを送信（ヘッダー付きでfetch）\n            fetch(`${apiEndpoint}/${encodeURIComponent(articleId)}`, {\n                method: 'POST',\n                headers: requestHeaders // ここにもヘッダーを追加\n            })\n            .then(res => {\n                if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);\n                return res.json();\n            })\n            .then(data => {\n                likeCountSpan.textContent = data.likes;\n            })\n            .catch(err => {\n                likeCountSpan.textContent = parseInt(likeCountSpan.textContent) - 1;\n                console.error('いいね送信失敗:', err);\n            });\n        });\n    });\n\u003C/script>\n```\n\n\u003C/details>\n\n上のコード内にもあるように、`{{ site.Params.hogehoge }}`を使ってブログのconfig.yamlに追加した要素を読み込んでくれるようにしておきました。コード内に直で書こうが、configから読むようにしようが、githubで公開している以上はエンドポイントもキーも見られちゃうわけなんですが、気持ち悪かったのでconfigから読む形式にしました。\n\n## 苦労した点\n\n前述のフロントエンドの方で苦労したことは一切と言っていいほどなかったんですが、今回のメインであるAWSの方は結構詰まった点が多かったのでそれを書いていきます。\n\n### 与太話1\nLambdaのコードなんかはAIにベースを書かせて、少しだけ自分で手直しをしてあげただけでテストで動いてくれたので良かったんですが、API Gatewayがかなりの曲者でした。いいねボタンを実装した当初は、設定が面倒くさかったこともあり、キーを使わずエンドポイントURLだけでapiを叩けるようにしていました。\n\n機能が完成して開発サーバーでテストOK、いざ外部公開用の本番で公開してmisskey（SNS）にてその旨を告知したところ、知り合いの某エンジニアに、エンドポイントだけでレート制限かかってないのをいいことにAPIを一生叩き続けていいねスパムを送るbotを作られ、いいね数がとんでもないことになってしまいました。\n\n![他の記事もこれでヤバかった](https://images.bokukoha.dev/blog/aws_like_button/2.jpg)\n\n流石にこれはマズイと思ったのでAPIキーを発行しレート制限をかけ解決したんですが、API gatewayの使用量プランとキーとAPI本体を紐づける作業がなかなか複雑だったのがつらかったです。今はもう理解したので詰まることはないんですが、初見だとAPIに対応するキーを発行して、使用量プランを作成して、プランとキーを紐づけて……という作業が難解でした。\n\nAPI側でレート制限をかけるだけではなく、フロントのコード側でも10回いいねが押されたらそのブラウザからは押せないようにしたので多少のスパム対策にはなったのかなという感じです。\n\n### 与太話2\n\nつい先日記事を投稿した際、何故かいいねボタンが壊れていいねが取得できていないことに気が付きました。ブラウザの開発者モードでコンソールを見てみると、`No 'Access-Control-Allow-Origin' header`のエラー。これはAPI gatewayのCORSというアクセス制限をかける機能が原因なのではないか？と調べる中でわかったので、CORSを再設定しました。\n\nこれで治ったかと思うと今度はコンソールに`429 Too many request`のエラー。1回押すだけでエラーが出てしまうのでこれは異常だと思い、使用量プランをオフにしたり、そもそもプランを削除したりしましたがそれでもエラーが消えず頭を抱えてしまいました。最終的にAPIキーを再発行することで絵429は消えましたが、今度は`403 forbidden`のエラー。\n\n結論から言うとこれは再発行したAPIキーをAPI本体に紐づけることを忘れていたことによる認証エラーだったのですが、これに気付くまでにそこそこ時間がかかりました。\n\n## おわりに\nAWSの無料枠で実用的なものを作る経験はそんなになかったので勉強になりました。以前作ったS3へのマイクラデータのバックアッププラグインなんかはS3だけしか使っておらず、今回のようにいくつかの機能を連携させるのは初めてで楽しかったです。\n\nエラーで頭を抱えたと書きましたが、この手の問題解決は結構楽しいので意外と好きだったりします。本業で使ってる方々から見ると結構しょぼい機能な気がしますが、自分的に満足のいくものを作れたので良かったです。\n\nここまで駄文を読んでくださりありがとうございました。良ければ下のいいねボタンを押していってください。僕が喜びます！\n\n### おまけ\n\n今回のいいね機能はLambdaを活用したサーバーレスなコード実行という点でAWSを使うメリットが明確なものだったので勉強にちょうどよかったのが助かりました。実は以前EC2やVPCなんかを使ってマイクラサーバーを構築する計画があったんですが、EC2の無料枠がしょっぱ過ぎた（RAM1GBのVMしか使えない；；）ことが理由で諦めていました。やっぱり趣味で動かすゲームサーバーなんかはオンプレでやるのが一番楽なのかな～なんて思ったり。\n\n",{"title":5,"description":197},"blog/aws_like_button/index",[207,208,209,210],"AWS","API","Hugo","Stack Theme","1B7N4Xhceu7HQxNPm83n5yfWktcXr5I6KUuqvU08YDE",[213,218,223,228,233],{"title":214,"path":215,"date":216,"category":194,"coverImage":217},"ClaudePro契約してCMS作ったぞ！！！","/blog/cms-dev","2026-04-09","https://images.bokukoha.dev/blog/cms-dev/PXL_20260327_034729621.jpg",{"title":219,"path":220,"date":221,"category":194,"coverImage":222},"MisskeyのDBバックアップとオブジェクトストレージの設定方法","/blog/misskey-setting","2025-11-23","https://images.bokukoha.dev/blog/misskey-setting/main.jpg",{"title":224,"path":225,"date":226,"category":194,"coverImage":227},"Proxmoxでコンテナ on コンテナして自鯖落とした話","/blog/docker-on-lxc","2025-11-21","https://images.bokukoha.dev/blog/docker-on-lxc/main.png",{"title":229,"path":230,"date":231,"category":194,"coverImage":232},"Nuxt Content3のコンポーネント呼び出し機能で遊んでみよう","/blog/md_components","2025-11-17","https://images.bokukoha.dev/blog/md_components/6.jpg",{"title":234,"path":235,"date":236,"category":194,"coverImage":237},"自宅で動かすMinecraftサーバー","/blog/mc_server","2025-09-30","https://images.bokukoha.dev/blog/mc_server/main.jpg",1777182800240]