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 になる

DID

ユーザーの ID

Bluesky で普通にアカウントを作ると did:plc になる

did:web はドメインに紐付く方式で、ここでは did:web:ds.jgs.me を使っている

ds.jgs.me については後述

lexicon

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 と同じ仕組みで外に流れる

ActivityPub

zet は Note として配送する

Webmention

本文中の外部リンクに Webmention を送る

Bluesky

bsky.social にある別アカウントから、リンクカード付きで投稿する

Threads

本文と /p/<n> の URL をテキスト投稿として流す

Grain

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 の上に載せようとしているので形になってきたらまた書くゾイ

似てるかもしれんページ