#Blueskyでブログにコメント機能を追加するライブラリ「blue-comet」を作った
##Table of Contents
##はじめに
このブログは Next.js + MDX で運用していますが、長いことコメント機能を持っていませんでした。
個人ブログのコメント機能と聞くと Disqus が定番ですが、広告が出たり、別アカウントが必要だったり、外部サービスへ依存することへの抵抗感があり、なかなか導入に踏み切れずにいました。
そこで思いついたのが、Bluesky の投稿をそのままコメント欄として使う という発想です。
Bluesky では、ある投稿に対する返信は単なる別の投稿として保存され、API 経由で誰でも取得できます。
記事ごとに 1 件だけ Bluesky 投稿を作っておけば、その投稿への返信=記事へのコメント、として扱えます。
読者も自分が普段使っている Bluesky クライアントから返信できるので、ブログ独自のコメントフォームを作る必要もありません。
実はこのアイデア自体は新しくはなく、既に czue/bluesky-comments という有名な React ライブラリが存在しています。
ただ、自分のブログに合う形に組み込もうとすると、既存ライブラリではいくつか痒いところに手が届かない部分がありました。
そこで、自分のユースケースに合わせて新しく 「blue-comet」 という React ライブラリを作り、npm に公開しました。
Read-only Bluesky comments for blogs, with a CLI that links articles to Bluesky posts.
##blue-comet とは
blue-comet は、Bluesky の投稿をコメントスレッドとしてブログに埋め込む ための React ライブラリです。
import { BlueCometComments } from 'blue-comet';
import 'blue-comet/styles.css';
<BlueCometComments postUri="at://did:plc:.../app.bsky.feed.post/3kxyz..." />;
このコンポーネントを記事ページに置くだけで、指定した Bluesky 投稿への返信が ネスト構造 で表示されます。
コメントを書きたい読者は「Reply on Bluesky」ボタンを押すと Bluesky 本体に飛び、そこで返信を書けば自動的にコメント欄に反映されます。
実際にこのブログのコメント欄が blue-comet で動いています。
##主な特徴
###1. 4.4 KB gzipped の超軽量バンドル
ランタイム側は @atproto/api を一切インポートせず、fetch() で公開 AppView (https://public.api.bsky.app) を直接叩く 設計にしています。
@atproto/api は便利なライブラリですが、minified で 700 KB ほどあるので、コメント欄を表示するためにそれを丸ごと載せるのはオーバースペックでした。
表示に必要な最小限の API レスポンス正規化を自前で書いたことで、React エントリは gzip 後 4.4 KB に収まっています。
@atproto/api を使うのは CLI 側だけです。dist エントリを分離しているので、ブラウザバンドルにはまったく入りません。
###2. CLI で記事と Bluesky 投稿をリンク
このライブラリの一番の特徴が同梱の CLI です。
# 認証情報を保存(一度だけ)
bluecomet login
# 記事ごとに Bluesky 投稿を作成して frontmatter に URI を書き戻す
bluecomet link posts/*.mdx
このコマンドを実行すると、各記事について以下を自動で行います。
- MDX の frontmatter を読み取る
- テンプレートに沿って投稿本文を組み立てる(記事タイトル、概要、URL を埋め込む)
- Bluesky にポスト
- 返ってきた
at://URI を frontmatter のbluesky:キーに書き戻す
その結果、記事の frontmatter にこんな行が増えます。
---
title: '...'
description: '...'
bluesky: 'at://did:plc:.../app.bsky.feed.post/3xyz...'
---
これだけで <BlueCometComments postUri={post.bluesky} /> がコメント欄を表示するようになります。
URL のコピペや手作業でのリンク管理が一切不要になるのが地味に大きいポイントです。
なお既存ライブラリの bluesky-comments でも同じような動作はできるのですが、Bluesky 投稿の作成・URI のコピー・frontmatter への貼り付けはすべて手作業でした。
記事を書くたびに毎回それをやるのは面倒なので、CLI 化したのが blue-comet の差別化ポイントの 1 つです。
###3. テーマに馴染ませやすい classNames スロット
このブログはターミナル風の独自デザインで統一しているため、コメント欄の見た目もそれに合わせる必要がありました。
そこで blue-comet では、内部の各要素に対して classNames を個別に注入できるように設計しています。
<BlueCometComments
postUri={post.bluesky}
classNames={{
root: 'flex flex-col gap-4',
item: 'rounded border border-white/5 bg-black/20 p-3',
displayName: 'text-hack-primary font-bold',
handle: 'text-hack-text/60',
replies: 'mt-3 border-l-2 border-hack-secondary/30 pl-4',
replyButton: 'rounded-full border border-hack-secondary/40 px-3 py-1',
// ... 全 17 スロット
}}
/>
これで Tailwind や CSS Modules、Mantine など、利用者側のデザインシステムに合わせて自由にスタイリングできます。
opt-in のデフォルトスタイル blue-comet/styles.css も用意しているので、ゼロコンフィグでも使えます。
###4. リッチな投稿表示
CLI で投稿を作るとき、URL を含むテキストを送るだけでは Bluesky 上では「ただのテキスト」として表示されてしまい、リンクにもならず OGP カードも出ません。
そこで以下を自動で行うようにしています。
RichText.detectFacets()で URL / @メンション / #ハッシュタグを検出してリッチテキスト化- 最初の URL に対して cardyb.bsky.app で OG メタデータを取得
- og:image を blob としてアップロードし、
app.bsky.embed.externalを投稿レコードに添付
これにより、Bluesky 公式クライアントから記事を投稿したときと変わらない見た目(クリッカブルな URL + OGP プレビューカード)になります。
##アーキテクチャ
ざっくりと、このような分担になっています。
your-site (browser bundle)
│
│ import { BlueCometComments } from 'blue-comet'
│
└──> blue-comet React entry (4.4 KB gzipped)
│
│ fetch()
▼
https://public.api.bsky.app
/xrpc/app.bsky.feed.getPostThread
author CLI (Node only)
│
│ bluecomet link posts/*.mdx
│
└──> blue-comet CLI entry
│
├──> MDX frontmatter を読む / 書き戻す
├──> @atproto/api で投稿
└──> cardyb で OGP メタデータ取得
ランタイム(ブラウザ)と CLI(Node)でエントリポイントを分けることで、ブラウザバンドルに @atproto/api が混入しないようにしています。
##このブログへの組み込み
最終的にこのブログ(hacking-frontline)への組み込みは、たった 3 ステップで終わりました。
###1. PostMetadata に bluesky フィールドを追加
// src/lib/blog.ts
export interface PostMetadata {
id: string;
title: string;
date: string;
description: string;
tags?: string[];
isPublished?: boolean;
bluesky?: string; // ← 追加
}
###2. 記事ページに <BlueCometComments> を差し込む
// src/app/posts/[id]/page.tsx
{
post.bluesky ? (
<BlueCometComments postUri={post.bluesky} classNames={...} />
) : (
<p>Comments not yet linked. Reply on Bluesky.</p>
);
}
bluesky が無い記事は fallback で「Reply on Bluesky」リンクを出すだけにしています。
###3. bluecomet link で投稿を作成
bluecomet login
bluecomet link posts/*.mdx
これで各記事の frontmatter に Bluesky URI が書き込まれ、デプロイすると即コメント欄が動き出します。
過去の記事をすべて一気にリンクするか、新しい記事から順次リンクしていくかは運用次第です。
私は最新記事から少しずつ試しながら反映していこうと思っています。
##設計上の判断
開発しながら考えていた設計の判断についても少し書いておきます。
###なぜ「読み取り専用」にしたか
ブログ内で Bluesky にログインして直接返信する UI も技術的には作れます(OAuth で @atproto/oauth-client-browser を使う)。
しかしこれを実装するには、
- OAuth クライアントメタデータを公開 URL でホスティング
- DPoP トークン署名処理
- IndexedDB でのセッション保存
など、コメント欄一個のためにやるには重すぎる工程が発生します。
「読者は普段使っているクライアントから返信したいはず」と割り切って、ライブラリ側はあくまで読み取り専用に徹し、書き込みは Bluesky 本体に飛ばす設計にしました。
結果としてライブラリの実装も簡潔になり、認証情報も一切扱わないので安全です。
###なぜ frontmatter ベースのリンクにしたか
「searchPosts API で記事タイトルをキーに自動検索する」という方法も最初は検討しました。
しかし Bluesky の searchPosts はインデックス遅延があり、カーソルページネーションも一部壊れているという既知の問題があります。
実装してもすぐ壊れる API に頼るのは避けたかったので、明示的に frontmatter で URI を持たせる方針にしました。
書き戻しの自動化を CLI でやることで、利用者側の手間も最小限に抑えられています。
##今後の予定
1.0.0-alpha.0 として公開していますが、しばらく実利用して問題なければ stable な 1.0.0 に上げていく予定です。
特に以下は実装する余地を残しています。
- i18n: 現状はデフォルト UI のテキストが英語固定です。
emptyContentなどの slot 差し替えで個別にローカライズできますが、messagesprop で一括差し替えできる仕組みを足すかもしれません。 fetchThread経由の server-side レンダリング: 直接 fetch する API はすでに export しているので、RSC や SSG で server 側に取得を寄せたいユーザ向けのレシピを README に追記する余地があります。
OAuth ベースの in-blog 投稿は、設計判断のセクションで述べた通り、意図的にスコープから外しています。
##まとめ
blue-comet は、Bluesky を使ったブログコメント機能を最小コストで実現するための React ライブラリです。
表示は無認証 + 軽量バンドル、書き込みは Bluesky 本体に誘導、リンク管理は CLI で半自動化、という方針で、個人ブログにも組み込みやすい設計を目指しました。
オープンソースとして公開していますので、Bluesky アカウントを持っていてブログにコメント欄を付けたい方は、ぜひ試してみてください。
フィードバックやバグ報告も歓迎です(このブログのコメント欄経由でも、GitHub の Issues でも)。
Read-only Bluesky comments for blogs, with a CLI that links articles to Bluesky posts.