もともとHugoというブログシステムを使っていて、その上でCoderというテーマを使っています。以前のブログでDisqusというコメントシステムを導入しましたが、利用しなくなりました。Blueskyをコメントサービスとして導入しようと思ったのは、「Your data is yours」という思想に共感したからです。記録として実装内容を残します。

ずっと二の足を踏んでいた理由は、フロントエンドが全くわからないからです。また、Hugoのブログシステムの仕組みも全く理解していませんでした。正直、フロントエンドの知識はjQueryやGulpで止まっています。HTMLとCSSもお手上げです。また、Blueskyの仕組みもよくわかっていませんでした。

Blueskyのコメントシステムを参考にできるブログが2つあることと、HugoにJavaScriptを組み込むGitHubリポジトリがあったので、それを参考にしました。Claude Codeの力を借りてこのコメントサービスを作りました。Claude Codeは非常に強力です。たまに失敗もしますが、大きな恩恵を受けています。

Blueskyコメントシステムの仕組み 見出しへのリンク

参考にしたのは、次の2つのブログでした。Powering your Hugo blog with Bluesky CommentsBuilding Bluesky Comments for My Blog です。app.bsky.feed.getPostThread を見ればわかるのですが、このエンドポイントにリクエストを投げて、再帰的にコメントを読み込むだけです。認証とかは特に必要ないです。前者はhugoとライブラリも何も使わずに自前でやっていて、後者の方は Next とReact と mary-ext/atcute という Typescript のライブラリを使っています。

Hugo の js buildの仕組み 見出しへのリンク

Hugoには、evanw/esbuildを使ったjs.Buildを使ってビルドできます。
gohugoio/hugoTestProjectJSModImports というサイトがあり、簡単な like を react で実装されていました。 ただ、古くて5年前の実装でした。module が利用できていませんでした。

  <head>  
      <script  
        src="https://unpkg.com/react@18/umd/react.production.min.js"  
        crossorigin></script>  
      <script  
        src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"  
        crossorigin></script>  
  </head>  
  <body>  
        <div id="like_button_container"></div>  
    {{ $shims := dict }}  
    {{ $defines := dict }}  
      {{ $shims = dict "react" "js/shims/react.js"  "react-dom/client" "js/shims/react-dom.js" }}  
    {{ $js := resources.Get "js/like.jsx" | js.Build (dict "shims" $shims "defines" $defines ) }}  
    <script src="{{ $js.RelPermalink }}"></script>  
  </body>  

方針 見出しへのリンク

基本的にはライブラリは使いたくありませんでした。ただし、テンプレートエンジンと型システムは欲しかったです。looptype というサイトを別件で作っているのですが、Claudeに任せて生のJavaScriptと生のHTMLを使って大変だったので、この選択にしました。以下のような構成になりました。

  • ImportMapを使ってbundleをしないこと
  • TypeScriptをJavascriptにtranspileすること
  • Reactを使うこと

実装 見出しへのリンク

前提条件 見出しへのリンク

  • Hugoブログが既に動作していること
  • Coderテーマを使用していること
  • Blueskyアカウントを持っていること

ステップ1: Bluesky投稿の準備 見出しへのリンク

ブログ記事に対応するBluesky投稿を作成し、DIDとCIDを取得します。

ステップ2: TypeScript開発環境のセットアップ 見出しへのリンク

tsconfig.jsonの設定例:

{  
  "compilerOptions": {  
    "jsx": "react-jsx",  
    "target": "ES2020",  
    "module": "ESNext",  
    "lib": ["ES2020", "DOM", "DOM.Iterable"],  
    "moduleResolution": "bundler",  
    "strict": true,  
    "esModuleInterop": true,  
    "skipLibCheck": true,  
    "forceConsistentCasingInFileNames": true,  
    "types": ["react", "react-dom"] /* Include React type definitions */  
  },  
  "include": ["tsx/**/*"],  
  "exclude": ["node_modules"]  
}

ステップ3: Hugoパーシャルテンプレートの作成 見出しへのリンク

layouts/partials/posts/bluesky.html を作成します。

{{- if and (isset .Params "bluesky_did") (isset .Params "bluesky_cid") -}}  
<script type="importmap">  
    {  
        "imports": {  
            "react": "https://esm.sh/react@18",  
            "react-dom/client": "https://esm.sh/react-dom@18/client",  
            "react/jsx-runtime": "https://esm.sh/react@18/jsx-runtime",  
            "@atproto/api": "https://esm.sh/@atproto/api@0.18"  
        }  
    }  
</script>

<div id="bluesky_comment_container"   
    data-did="{{ .Params.bluesky_did }}"  
    data-cid="{{ .Params.bluesky_cid }}">  
</div>  
<script type="module" src="/js/blueskycomment.js"></script>  
{{- end -}}  

ステップ4: TypeScriptファイルの作成 見出しへのリンク

assets/ts/blueskycomment.tsx を作成します。

主な構成要素:

  • AtpAgentの初期化
  • スレッド取得関数
  • Reactコンポーネント(BlueskyComments)
  • renderReply関数(再帰的なコメント表示)

AtpAgentの初期化 見出しへのリンク

mary-ext/atcute は利用せずに、official の @atproto/api を利用した。

import { AtpAgent } from "@atproto/api";  
const agent = new AtpAgent({ service: "https://public.api.bsky.app" });  

スレッド取得の設定 見出しへのリンク

const uri = `at://${did}/app.bsky.feed.post/${postCid}`;  
await agent.getPostThread({  
   uri: uri,  
   depth: 5,        // コメントの深さ (最大10)  
   parentHeight: 0, // 親投稿の取得数  
});  

Reactコンポーネントの初期化 見出しへのリンク

const container = document.querySelector("#bluesky_comment_container");  
if (container && did && postCid) {  
  const root = ReactDOM.createRoot(container);  
  root.render(  
    <BlueskyComments did={did} postCid={postCid} skipFirst={true} />  
  );  
}  

Reply 見出しへのリンク

const renderReply = (reply: AppBskyFeedDefs.ThreadViewPost, depth: number = 0) => {  
  const { post } = reply;  
  const record = post.record as { text: string; createdAt: string };

  return (  
    <div key={post.cid}>  
      {/* ユーザー情報 */}  
      <div>  
        <img   
          src={post.author.avatar}   
          alt={post.author.handle}  
        />  
        <div>  
          <strong>{post.author.displayName || post.author.handle}</strong>  
          <div>  
            @{post.author.handle} · {new Date(record.createdAt).toLocaleDateString()}  
          </div>  
        </div>  
      </div>

      {/* コメント本文 */}  
      <div>  
        {record.text}  
      </div>

      {/* ネストした返信 */}  
      {reply.replies?.map((r) => {  
        if (r.$type === "app.bsky.feed.defs#threadViewPost") {  
          return renderReply(r as AppBskyFeedDefs.ThreadViewPost, depth + 1);  
        }  
        return null;  
      })}  
    </div>  
  );  
};  

ステップ5: TypeScriptのコンパイル 見出しへのリンク

# TypeScriptをJavaScriptにコンパイル  
npx tsc  

ステップ6: テーマテンプレートへの組み込み 見出しへのリンク

Coderテーマの投稿テンプレートを編集します。

``layouts/posts/single.html` に追加:

<article>  
  {{ .Content }}  
    
  <!-- Blueskyコメントの表示 -->  
  {{ partial "posts/bluesky.html" . }}  
</article>  

ステップ7: 記事のフロントマターに設定を追加 見出しへのリンク

コメントを表示したい記事の先頭に以下を追加:

---  
title: "記事タイトル"  
date: 2024-12-14  
bluesky_did: "did:plc:あなたのDID"  
bluesky_cid: "投稿のCID"  
---  

ハマったポイント・トラブルシューティング 見出しへのリンク

typescript の build 見出しへのリンク

TypeScriptのビルドではまりました。これは自分の理解不足が原因でもあります。Hugoのjs.Buildを使おうとしましたが、うまくいかなかったので、結局TypeScriptコンパイラ(tsc)でビルドすることにして、それを静的ファイルとして配置しました。TypeScriptコンパイラとesbuildでは正常に動作しましたが、なぜかHugoのjs.Buildから呼び出すとエラーになってしまったため、tscでのビルドに切り替えました。結局ローカルでビルドすることになったので、バンドルした方が良かったかもしれませんが、最初の設計のままにしました。

Tsxをトランスパイルしないやつ

esm.sh/tsxで、トランスパイルしないものもありましたが、HTMLに直接書かないと動かないようなので諦めました。下の実装は、公式サイトのままのやつです。


<!DOCTYPE html>  
<html>  
<head>  
  <script type="importmap">  
    {  
      "imports": {  
        "react": "https://esm.sh/react@19.2.0",  
        "react-dom/client": "https://esm.sh/react-dom@19.2.0/client"  
      }  
    }  
  </script>  
  <script type="module" src="https://esm.sh/tsx"></script>  
</head>  
<body>  
  <div id="root"></div>  
  <script type="text/babel">  
    import { createRoot } from "react-dom/client"  
    createRoot(root).render(<h1>Hello, World!</h1>)  
  </script>  
</body>  
</html>

html と CSS の実装 見出しへのリンク

HTMLとCSSは全くわからなかったので、Claudeに任せました。

感想 見出しへのリンク

今回の実装ではReactを採用しましたが、実装後に Useful patterns for building HTML tools「FastAPI + htmxが最強説」- AIエンジニアがモック作るならReactは不要、Streamlitも捨てよう といった記事を読み、考えさせられました。小さなプロジェクトは、シンプルさを保つことが大切だと改めて感じました。 Claude code もガンガン活用したほうがいい良いです。