astroで作った静的ブログにコメント機能をつけたい!!!!!!!
当ブログはAstroで作成した静的サイトである。githubのリポジトリにあるmd資産を元に静的なhtmlを生成しているのでサイトの動きはサクサクでとてもよい。 が、静的サイトなので当然コメント機能などの動的機能は存在しない。実装するには外部のコメントサービスを利用するか、自分で実装するしかない。
「Astro コメント機能」などと検索すればいくつか参考になりそうなサービスは出てくるものの、githubのDiscussion機能を利用したGiscus(コメントにgithubログインが必要で一般的ではない)や広告の表示されるものなどがメイン。実装が簡単であるのは大きいが、コメントを残すためにgithubログインが必要だとただでさえ低いコメントされる可能性が更に低くなることが容易に想像できたのでGiscusは無し。広告も表示したくない。
また、コメントが投稿されるたびにデプロイを行いサイトをビルドし直してコメントを静的なサイトとして取り込む案も検討したが、どうせならリアルタイムでコメント更新されてほしいのでこの案も見送る。
ということでつい最近提供されはじめたらしいCloudflare D1をDBとして用いて自前のコメント機能を実装していくことに。
果たして実装完了まで漕ぎ着ける事はできるのか……。 実装完了しました。
構成
資産:Githubリポジトリで管理
デプロイ:Cloudflare Pages
DB:Cloudflare D1
フレームワーク:Astro
Astroテンプレート:huwari
手順
すでにAstroブログの初期設定は完了している前提です。
Cloudeflare Adapteのインストール
AstroでSSRを利用するためには各デプロイ先サービスのアダプターが必要なため、今回はCloudeflare Adapteをインストール。
pnpm astro add cloudflare
yでインストール
pnpm astro add cloudflare✔ Resolving packages...
Astro will run the following command: If you skip this step, you can always run it yourself later
╭───────────────────────────────────────╮ │ pnpm add @astrojs/cloudflare@^12.6.0 │ ╰───────────────────────────────────────╯
? Continue? » (Y/n)
インストール完了するとastro.config.mjs
に下記のように追記される。
adapter: cloudflare(),
Wranglerのインストール
Cloudflareを操作するために利用するCLIツールのWranglerのインストールを行う。Cloudflareの開発サーバの起動(Astro previewは使えなくなるので開発環境でビルドしてその内容をpreviewの代用として見る必要がある)やデータベースの作成、デプロイ等もranglerコマンドから可能(Github管理なので自動でデプロイされる為今回は使用しないが)。
pnpm install wrangler
インストールできたらバージョン確認コマンドを打ってみる。
pnpm wrangler --version
⛅️ wrangler 4.22.0───────────────────
Drizzle ORMのインストール
データベースを操作するためにDrizzle ORMに関係するパッケージのインストールを行う。
pnpm install drizzle-orm better-sqlite3
下記でインストールするdrizzle-kitは定義したスキーマファイルを利用してテーブルを作成/更新するために必要なマイグレーションファイルを作成するためのツール。
pnpm install -D drizzle-kit
Cloudflare D1のデータベース設定
下記コマンドでデータベースを作成。
pnpm wrangler d1 create tarailife-post-comments
{ "d1_databases": [ { "binding": "DB", "database_name": "tarailife-post-comments", "database_id": "2df89c19-da66-4200-96a5-2ed03756185e" } ]}
初実行時はブラウザが起動し認証画面が表示される。
認証を終えるとデータベースが作成されていることが確認できる。
wrangler.tomlをプロジェクトルートに作成
[[d1_databases]]binding = "DB" # i.e. available in your Worker on env.DBdatabase_name = "tarailife-post-comments"database_id = "2df89c19-da66-4200-96a5-2ed03756185e"
Drizzleによる設定
データベースのテーブルを作成する際に利用するsql文を作成するためにsrcフォルダの中にdbフォルダを作成しテーブルのスキーマ情報を記述したschema.tsファイルを作成。
今回は下記のような必要最低限のテーブルを取り敢えず作成。
import { text, integer, sqliteTable } from "drizzle-orm/sqlite-core";
export const comments = sqliteTable("comments", { id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), author: text("author"), content: text("content"), post_slug: text("post_slug"), post_date: text("post_date"),});
プロジェクトルートにdrizzle.config.tsの作成
import { defineConfig } from "drizzle-kit";
export default defineConfig({ schema: "./src/db/schema.ts", out: "./drizzle", driver: "d1-http", dialect: "sqlite",});
下記コマンドでマイグレーションファイルを生成。
pnpm drizzle-kit generate
コマンドを実行するとプロジェクトルートにdrizzle
フォルダが作成される。
テーブルの生成(.sqlファイルのファイル名は適宜上の手順で生成されたものに変更)。
pnpm wrangler d1 execute tarailife-post-comments --file=./drizzle/0000_useful_skreet.sql
.wrangler\state\v3\d1\miniflare-D1DatabaseObject\
にファイルが3つ作成されているので確認。
drizzle.config.ts
を更新(dbCredentials
のurl
を先程生成された.sqlite
ファイルのパスに変更)
import { defineConfig } from "drizzle-kit";
export default defineConfig({ schema: "./src/db/schema.ts", out: "./drizzle", driver: "d1-http", dialect: "sqlite", dbCredentials: { url: ".wrangler\state\v3\d1\miniflare-D1DatabaseObject\a520e4684331132e883183950c13a4f9c830d371f45a1f10dfdf654b438f8857.sqlite", },});
本番環境へテーブル作成(.sqlファイル名は生成されたものに変更)。
pnpm wrangler d1 execute tarailife-post-comments --remote --file=./drizzle/0000_useful_skreet.sql
⛅️ wrangler 4.22.0───────────────────? ⚠️ This process may take some time, during which your D1 database will be unavailable to serve queries. Ok to proceed? » (Y/n)
DB の Binding 設定
Pages にアップしたファイルからデータベースへのアクセスを行うためにD1 database bindingsの設定行う。Workers&Pagesのプロジェクト設定画面を開きバインディング
の追加
を選択しD1データベースの情報を追加する。
コメント表示用のコンポーネントを作成
AstroではアイランドアーキテクチャでAstroファイル内にReactやSvelteなどの他フレームワークのコンポーネントを使用することができる他ページの一部を動的生成したり出来る。 今回はSSRを行うコメント表示用のコンポーネントをSvelteで作成し、それをページ生成用のAstroコンポーネントで読み込むことで各投稿ページにコメント機能を実装する。 表示している記事に投稿されているコメントだけ拾ってきたいのでリクエストパラメータでslugを渡しAPIを呼び出す。
src\components\widget\
にCommentBox.svelte
を新規作成。
<script>export let slug;let author = "";let content = "";let comments = [];
const loadComments = async () => { const res = await fetch(`/api/v1/comments/?slug=${slug}`, { method: "GET", headers: { "Content-Type": "application/json" }, }); comments = await res.json();};
const submitComment = async () => { const res = await fetch("/api/v1/comments/", { method: "POST", body: JSON.stringify({ post_slug: slug, author: author, content: content }), headers: { "Content-Type": "application/json" }, }); author = content = ""; await loadComments();};
import { onMount } from "svelte";onMount(loadComments);</script>
<div class="grid grid-cols-1 gap-8"> <h3>コメント</h3> {#each comments?.body?.comments as comment, i} <div class="row-span-2 grid grid-rows-subgrid grid-rows-2 grid-cols-1 gap-2"> <div> <strong>{`${i + 1}: ${comment.author || "no name"}`}</strong> <small>{new Date(comment.post_date).toLocaleString()}</small> </div> <p>{comment.content}</p> </div> {/each} </div><div class="grid grid-cols-1 gap-4 mt-8"> <h3>コメントを書く</h3> <form on:submit|preventDefault={submitComment} class="row-span-4 grid grid-rows-subgrid grid-rows-2 grid-cols-1 gap-4 md:w-4/4"> <input bind:value={author} placeholder="名前" class="bg-[var(--license-block-bg)] rounded-md"> <textarea bind:value={content} placeholder="コメント内容" required class="h-24 bg-[var(--license-block-bg)] rounded-md field-sizing-content"></textarea> <button type="submit" class="btn-regular h-8 text-sm px-3 rounded-lg">投稿</button> </form></div>
作成したコンポーネントをコメント機能を追加したいページのコンポーネントに設置
当ブログではAstroのブログテンプレートであるFuwari
を使用しているためブログの各ポストを生成しているファイルはsrc\pages\posts\[...slug].astro
になる。ここは各自でコメント機能を追加したいファイルに置き換えてコンポーネントを設置してほしい。
import CommentBox from "src/components/widget/CommentBox.svelte"; //先ほど作成したコメント表示コンポーネントをインポート
//コンポーネントを好きな箇所(といってもおそらくページ下部だと思うが)に設置 <div id="comments-container" class:list={["card-base z-10 px-6 md:px-9 pt-6 pb-4 relative w-full ",{}]}> <CommentBox slug={entry.slug} client:load /> </div>
コンポーネントのpropsentry.slug
で渡された値でどの記事に投稿されたコメントなのか判別している。
API用のページを作成
今回はAstroのAPIエンドポイントとして、src\pages\api\v1\comments.ts
のような配置でディレクトリを作成しAPIエンドポイント用のAstroファイルを作成している。
export const prerender = false;import { drizzle } from "drizzle-orm/d1";import { like } from "drizzle-orm"import { comments } from "../../../db/schema";import type { Runtime } from "@astrojs/cloudflare";import type { D1Database } from "@cloudflare/workers-types";
export async function GET(context: { request: Request; locals: Runtime }) { const { request, locals } = context; const envDB = locals.runtime.env.DB as D1Database; const db = drizzle(envDB); const url = new URL(request.url); const slug = url.searchParams.get("slug");
let comment = await db.select().from(comments).where(like(comments.post_slug, `${slug}`));
return new Response( JSON.stringify({ body: { comments: comment, request: request, }, }), { status: 200 }, );}
export async function POST(context: { request: Request; locals: Runtime }) { const { request, locals } = context;
if (!locals.runtime.env.DB) { throw new Error("Database connection is missing"); } const envDB = locals.runtime.env.DB as D1Database; const db = drizzle(envDB);
const body = await request.json(); const comment = { author: body.author, content: body.content, post_slug: body.post_slug, post_date: new Date().toISOString(), };
await db.insert(comments).values(comment);
return new Response( JSON.stringify({ body: { request: request }, }), { status: 200 }, );}
これで手順は終了。 下記は構築にあたって参考にさせていただいたサイトになります。
コメント欄実装完了!
これでシンプルだがコメント欄を実装できた。
コメントの返信機能だったりスパム対策のreCAPTCHA導入とか色々と直したいところもあるので今後も少しずつ改善していきたい。
コメント
コメントを書く