Zettelkasten を公開できるようにした
Scrapbox ではあまり stock と flow を意識して分けてなかったのだが、自作の diary とそれの公開面である jgs.me とを運用していく中でいわゆる stock 的なるものと flow 的なるものがあった方がいいなという気持ちになっていった
stock は Article という名前を付けていたのだけど、flow をどうしたもんかな、とおもい、エージェントと相談した結果 Zettelkasten となった
略して zet/zets
併せて、最近気持ちが AT Protocol になってきていたので zets をうま〜く atproto に載せたいな〜とおもい、ここもエージェントとアレコレやった
AT Protocol ざっくり
登場人物は3種類
PDS (Personal Data Server)
ユーザーのデータを持つサーバー
投稿も画像もここにある
relay
世界中の PDS から更新を集めて、1本の firehose として流す
AppView
firehose を読んで索引を作り、画面として見せる
Bluesky のアプリなど
書く場所と見せる場所が分かれていて、PDS に書いたものを AppView が勝手に拾っていく、という分業になっている
データの置き方
ユーザーごとに repo があって、その中に collection と rkey で record が並ぶ
record は at://did/collection/rkey の形で、これを at-uri と呼ぶ
zet なら at://did:web:ds.jgs.me/me.jgs.diary.zet/rkey になる
ユーザーの ID
Bluesky で普通にアカウントを作ると did:plc になる
did:web はドメインに紐付く方式で、ここでは did:web:ds.jgs.me を使っている
ds.jgs.me については後述
record の型定義。ドメインを逆にした NSID という名前で型を決める
自分のドメインを持っていれば誰でも自分の NSID を作れる。zet は me.jgs.diary.zet
blob
画像などのバイナリは record とは別に PDS に上げて、record からは CID で参照する
投稿用の API を公開していないアプリでも、そのアプリの lexicon に沿った record を自分の PDS に書いておけば、relay 経由で AppView に拾われる
全体像
流れとしては4段になっている
書く
diary で zet を公開すると、AT Protocol の record にして PDS に書き込む
置く
dirig という名前で cloudflare スタックの PDS サーバを作ってもらった
これが ds.jgs.me
見せる
jgs.me が PDS から record を取り込んで、/zets やトップに出す
流す
jgs.me から ActivityPub、Webmention、Bluesky、Threads へ
大事にしているのは、原本が PDS の record で、jgs.me 側はそれをレンダリングしているだけ〜という構成にしているというあたり

書く: diary
diary は自分用の日記アプリで、entry に kind を持たせて zet と article に分けている
article は今まで通り jgs.me に article として出す
zet は PDS に record として置いてから、jgs.me に知らせる
record
中身は text (Scrapbox 記法の本文)、title 、date、createdAt、images
rkey は初回の公開時だけ TID で採番して、以後は同じ rkey に put し直す
順番
at-uri を DB に保存してから micropub を送る
micropub には本文を入れず、at-uri だけを送る。中身は jgs.me 側が PDS に取りに行く
置く: dirig
dirig はエージェントに作ってもらった PDS で、Cloudflare Workers の上で1アカウントだけを動かしている
なぜ自前の PDS か
Bluesky で普通にアカウントを作ると、データは Bluesky が運営する PDS に置かれる
PDS を自分のドメインで立てて did:web:ds.jgs.me にすれば、データの置き場も持ち主の ID も自分のドメインになる
構成
Workers
XRPC のエンドポイント
D1
repo (record と commit) と firehose のイベント
R2
blob
Durable Object
subscribeRepos の WebSocket。relay はここに繋いで更新を受け取る
cron
毎分動いて、firehose を起こしたり、7日より古いイベントを消したりしている
relay との付き合い
relay 側で、いつの間にか ds.jgs.me が offline 扱いにされていることがあった
offline の間は PDS に書いても relay の firehose に流れず、Grain のような AppView に届かない
なので dirig の cron で 10分ごとに relay (bsky.network) の getHostStatus を見て、offline か idle なら requestCrawl を送るようにした
見せる: jgs.me
jgs.me 側は micropub のエンドポイントで zet を受ける
受け取り
diary から来るのは category=zet と at-uri だけ
at-uri が at://did:web:ds.jgs.me/me.jgs.diary.zet/ で始まっていて、rkey が TID の形かを確かめる
保存
D1 に page / zet / object / copy を、1回の batch で入れる
画像
PDS の getBlob の URL のままだと、Image Transformations も srcset も効かず、毎回原寸が PDS から返ってくる
なので取り込み時に blob を R2 に写して、本文の URL を差し替える
表示
/zets にタイムラインで並べて、トップにも zets のタイルを出している
流す: クロスポスト
jgs.me に取り込まれた zet は、記事や clip と同じ仕組みで外に流れる
zet は Note として配送する
本文中の外部リンクに Webmention を送る
bsky.social にある別アカウントから、リンクカード付きで投稿する
本文と /p/<n> の URL をテキスト投稿として流す
Grain はなんか往年の Instagram っぽくていいなとなった
2026-09-26 by @ds.jgs.me on Grain1 photo by @ds.jgs.me on Grain.grain.social画像だけの zet は Grain にも載せている
Grain は写真用の AppView で、relay 経由で Grain が拾う〜という流れは同じ
ハマりどころ
relay の offline
relay の getHostStatus を見たら、ds.jgs.me が offline になっていた
PDS の firehose 自体は正常で、relay 側の記録だけが古い seq で止まっていた
dirig は自分から requestCrawl を送っていなかったので、一度外れると戻ってこない
手で requestCrawl を叩いたら active に戻って最新まで追いついた
Grain の photo
social.grain.photo は photo の blob が 1MB までで、aspectRatio が必須
1MB を超える画像か、寸法が読めない画像が1枚でもある zet は、Grain には出さない
今後
今は Semble ベースの news aggregator を試作している
n年ぶりm回目
これも AT Protocol の上に載せようとしているので形になってきたらまた書くゾイ