{
  "$type": "com.whtwnd.blog.entry",
  "blobs": [
    {
      "name": "atprofile_joey.png",
      "blobref": {
        "$type": "blob",
        "ref": {
          "$link": "bafkreicox6cohjioyy2y654bmvrquqtj74bjauzbcn3fo3iuht6a7ql46e"
        },
        "mimeType": "image/png",
        "size": 552146
      },
      "encoding": "image/png"
    },
    {
      "name": "settings.png",
      "blobref": {
        "$type": "blob",
        "ref": {
          "$link": "bafkreibywdo5md6odo5uaabht6rfb642v5q6akqihclorfgs5tuu3ktwkm"
        },
        "mimeType": "image/png",
        "size": 128997
      },
      "encoding": "image/png"
    },
    {
      "name": "editor.png",
      "blobref": {
        "$type": "blob",
        "ref": {
          "$link": "bafkreifkhafqbqjput377inqnuajfxn6tlzntsio2kqre34ydd6x7omtta"
        },
        "mimeType": "image/png",
        "size": 266222
      },
      "encoding": "image/png"
    }
  ],
  "theme": "github-light",
  "title": "AT Profile勝手チュートリアル",
  "content": "今やatprotoアプリケーションは数多ありますが、その中でもatprotoオタクとしてお気に入りのサービスに[AT Profile](https://atprofile.com/)があります。[^favapp]\n\n[^favapp]: 同じくらい好きなサービスとして、atprotoの使い方が上手い[Tangled](https://tangled.org/)と、 atprotoサービスの先駆者である[WhiteWind](https://whtwnd.com/)があります。あくまでオタク基準ですが。\n\nかなりatproto風味の強いサービスなのですが、ドキュメントが全然無いので面白い部分が全然見えません。\\\nというわけで、関係者でもなんでもないですが、実装から読み解いた機能と使い方を紹介します。\n\n以下、[atproto公式ガイド](https://atproto.com/ja/guides/data-repos)で説明されている程度のatproto知識を前提とします。\n\n## AT Profileとは\n\nその名の通り、プロフィールページを作れるサービスです。\\\nデフォルトでは[Bluesky情報を使った素朴なもの](https://atprofile.com/atprofile.com)ですが、ウェブアプリ開発で使われる[Vue](https://ja.vuejs.org/)フレームワークが利用できるため、自由度が非常に高いです。開発者のJoey氏は[MySpace風プロフィール](https://atprofile.com/joey.codes)を作ったりしています。\n\n![AT Profileによるプロフィールページ例](https://lionsmane.us-east.host.bsky.network/xrpc/com.atproto.sync.getBlob?did=did%3Aplc%3Aqi6xg6zplzivyu7zrylxuugk&cid=bafkreicox6cohjioyy2y654bmvrquqtj74bjauzbcn3fo3iuht6a7ql46e)\n\nまた、atprotoデータを取り込みやすくなっているのも特徴です。Blueskyに限らず、全atprotoサービスで所有する公開データ(record)を簡単に参照できます。\\\nデータは元サービスから随時取得されており、例えばデフォルトで表示されるアイコンや紹介文はBlueskyで更新されたらAT Profileでも追従します。\n\n## 基本的な使い方\n\nAT Profileでは基本的にvueファイルをベタ書きします。乱暴な言い方をするなら少し特殊なHTMLファイルです。\\\nこれからAT Profile独自機能を含めたvueファイルの簡単な説明をしますが、正直Vueがあんまり分かってないので、話半分に聞いてください。\n\nログインして編集画面を開くと、`style`, `div`, `script`の3要素があるのが見えます。プロフィールページでは、この内容が概ねそのまま`body`にぶち込まれます。[^iframe]\n\n[^iframe]: `body`といってもiframe内の`body`で、プロフィールページのヘッダ/フッタ(AT Profileロゴ等が表示されている部分)には干渉できません。iframe内はatprfl.comという別ドメインになっています。\n\n![編集画面スクリーンショット](https://lionsmane.us-east.host.bsky.network/xrpc/com.atproto.sync.getBlob?did=did%3Aplc%3Aqi6xg6zplzivyu7zrylxuugk&cid=bafkreifkhafqbqjput377inqnuajfxn6tlzntsio2kqre34ydd6x7omtta)\n\n普通のHTMLと違うところとして、変数や制御構造(テンプレート構文)を使うことができます。\\\n例えばデフォルトプロフィールの以下の部分。\n\n```html\n<h1 class=\"title\">\n  {{ profile.displayName || profile.handle}}\n  <template v-if=\"status.length\">- {{status[0].value.status}}</template>\n</h1>\n```\n\n二重波括弧で囲まれた部分はテキスト展開と呼ばれるもので、好きなJavaScript式を書くことができます。AT Profileの場合、`profile`変数にアカウントデータが入っており、ここでは`displayName`があればそれに、なければ`handle`に展開する式になっています。\n\n`v-if`属性付きのHTML要素は属性値の評価結果がfalsyであれば描写されません。ここでは[Statusphere](https://atproto.com/guides/applications)データ`status`が存在するならその内容を表示するという処理になっています。\\\nただの文字列に`v-if`を適用するために`template`タグを使っていますが、`h1`や`p`のような普通のHTMLタグにも同じことができます。\n\nここで使う変数はどこで定義されているかというと、`script`タグ内にあります。\n\n```js\ncreateApp({\n  setup() {\n    const cursor = useTemplateRef('cursor')\n    const cursorTop = ref(0)\n    const cursorLeft = ref(0)\n\n    const setPosition = (event) => {\n      const radius = cursor.value?.offsetHeight / 2\n\n      cursorTop.value = event.clientY - radius\n      cursorLeft.value = event.clientX - radius\n    }\n\n    onMounted(() => {\n      document.addEventListener('mousemove', setPosition)\n    })\n\n    onUnmounted(() => {\n      document.removeEventListener('mousemove', setPosition)\n    })\n\n    return {\n      ...window.context,\n      cursorTop,\n      cursorLeft,\n    }\n  },\n}).mount('#app')\n```\n\nこの`setup`の返り値のプロパティが変数に使えると思ってください。`profile`や`status`が無いのが気になるかと思いますが、それらは`window.context`に含まれています。詳細は後ほど。\n\n`setPosition`がやっているように、Vueでは値を動的に更新することもできます。細かいVueの使い方はいくらでも資料があると思うので各自調べてください。\n\n### atproto的な使い方\n\nさて、ここからが本題です。`profile`から一部のBluesky情報にアクセスできるのは分かりましたが、他には何をサポートしているのでしょう?\n\n`window.context`には、以下が含まれています。[^context]\n\n[^context]: [コード](https://github.com/Narkoleptika/atprfl.com)から読み取っただけなので、仕様として安定はしていないかもしれません。\n\n| 名前 | 内容 |\n| - | - |\n| `profile` | 作成者アカウントに[`getProfile`](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/actor/getProfile.json)した結果 |\n| `pds` | 作成者PDSエンドポイント |\n| `contextItems` | 後述 |\n| `newlinesToLinebreaks` | HTML内の改行を表示時にも改行として扱うか(`preserve-breaks`) |\n| `publicAgent` | public.api.bsky.appへの[`Agent`](https://github.com/bluesky-social/atproto/blob/591de19524639341a7dd64ee75c482c645c186fd/packages/api/src/agent.ts#L78) |\n| `agent` | `pds`への`Agent` |\n| `getRecord` | 作成者アカウントへの`getRecord` |\n| `getRecords` | 作成者アカウントへの`listRecord` |\n| `atprotoApi` | @atproto/apiパッケージへの参照 |\n\nまあ実際に使うのはほぼ`profile`だけかと思いますが、時々`publicAgent`で`getProfile`したり、`agent`で`getBlob`したりはできそうです。\n\nところで、察しの良い方は`status`が無いことにお気づきかもしれません。\\\n実はこれ、常にあるとは限りません。設定によってあったりなかったりするからです。[^empty]\n\n[^empty]: なお、作成者がStatusphere使ってない場合でも、設定されている場合は`status`は空リストとして参照できます。\n\n編集画面の歯車マークを押すと設定が出てきます。\n\n![設定画面](https://lionsmane.us-east.host.bsky.network/xrpc/com.atproto.sync.getBlob?did=did%3Aplc%3Aqi6xg6zplzivyu7zrylxuugk&cid=bafkreibywdo5md6odo5uaabht6rfb642v5q6akqihclorfgs5tuu3ktwkm)\n\nContext設定で、`xyz.statusphere.status`コレクションから取得したrecordを最大1個の配列として`status`変数に入れるように指定されています。\\\n同様に、`app.bsky.feed.post`を指定すればBlueskyの自分の投稿が、`com.whtwnd.entry`を指定すればWhiteWindの記事が取得できるというわけです。\\\n最新50件しか取得できませんが、rkeyを指定すれば決まったrecordを引っ張ってくることもできます。さらに多くのrecordを取得したい場合は`getRecords`を呼ぶのもいいでしょう。\n\nつまり、自分のrecordデータを挿入する程度であればスクリプトを全然書かなくてもうまいことやってくれるわけですね。ここが個人的に刺さったポイントです。\n\nちなみに、実は`contextItems`にはここで指定したContext設定がそのまま入っています。つまり、先ほどの設定画面で指定したcollectionやlimitのことです。\n\nついでに`newlinesToLinebreaks`が設定画面で指定できることもスクリーンショットから分かりますね。\n\nもちろんこの設定やカスタマイズしたプロフィールページもrecordとしてPDSに保存されるので、その他のatprotoサービスからも参照できます。使い道があるかはともかく。\n\n## サンプル\n\n具体的にどんなものが作れるか、少しだけ見てみましょう。サンプルなので見栄えは最低限です。\n\nどんな表示になるか見たい方は[山貂のプロフィールページ](https://atprofile.com/yamarten.bsky.social)を参照してください。\n\n### WhiteWind記事リンク\n\nWhiteWindの記事は`com.whtwnd.blog.entry`に入っています。これで記事リンク一覧を作ってみましょう。\n\nContext設定でcollectionを指定して、`entries`という名前で5件取得するよう設定してみます。`whtwnd`のrkeyはTIDなので、普通は最新5件が取れるはずです。\n\n![設定例](https://lionsmane.us-east.host.bsky.network/xrpc/com.atproto.sync.getBlob?did=did%3Aplc%3Aqi6xg6zplzivyu7zrylxuugk&cid=bafkreiclkitymtzuolw5xnn2avexhytv7h6jcz4bqqqn3krqazcbcnmmya)\n\nあとは`entries`をVueから参照するだけ。簡単ですね。\n\n```html\n<ul><li v-for=\"e in entries\">\n    <a :href=\"`https://whtwnd.com/${profile.did}/${e.uri.slice(-13)}`\">{{e.value.title}}</a>\n</li></ul>\n```\n[`listRecords`](https://github.com/bluesky-social/atproto/blob/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3/lexicons/com/atproto/repo/listRecords.json)のレスポンスがそのまま入っているので、配列の各要素は`uri`, `cid`, `value`(record値)を持ちます。\\\nwhtwnd.comへのリンクはrecordには入っていないので自分で構成します。rkeyはat-uri末尾から切り出すことで取得。\n\nrecordには記事本文も丸ごと入っているので、直接表示することもできます。Markdownレンダリングをどうするかは考えないといけませんが。\n\n### likeされたBluesky投稿\n\n続いて、recordに入ってない情報を使いたい場合も考えてみます。\\\nWhiteWindなら記事についたコメントはrecordにはありませんし、Blueskyならフォロワー一覧、like数など。購読フィードのような非公開データは難しいですが、公開情報、特にBlueskyのものなら`publicAgent`を使えば簡単です。\n\nここでは自分のBluesky投稿から、1件以上likeがついているものを最新3件表示してみます。\n\n`getAuthorFeed`でlikeカウント付きの投稿一覧を取得し、likeされているものを抽出します。\\\n非同期処理もあるため`script`タグ内で処理を書く必要があります。また、HTML側で参照するために`likedPosts`という変数を用意し、そこに結果を入れることにしました。\n\n```js\nconst likedPosts = ref(\"\")\n\nconst fetchLiked = async () => {\n    const { data } = await context.publicAgent.app.bsky.feed.getAuthorFeed({\n        actor: context.profile.did,\n    })\n\n    likedPosts.value = data.feed.filter((p)=>!p.reason && p.post.likeCount > 0).slice(0,3)\n}\n```\n\n`actor`のアカウント指定はIDベタ書きもできますが、`profile`から持ってくると再利用がききます。\\\nスクリプト側では`window.context`のフィールドとしてアクセスする点にだけ注意してください。(例では`window`は省略していますが、対象は同じです)\n\n追加したスクリプトを使うために、`setup`の中で`fetchLiked`を呼び出した上で、返り値に`likedPosts`を加えることでHTMLから参照できるようにします。\\\nDOM触らないので即時`fetchLiked`呼んでますが、Vueのお作法的には`onMounted(fetchLiked)`の方が安全なんでしょうか。\n\n```js\ncreateApp({\n    setup() {\n        // 中略\n        fetchLiked()\n\n        return {\n            ...window.context,\n            cursorTop,\n            cursorLeft,\n            likedPosts,\n        }\n    },\n}).mount('#app')\n```\n\nここまできたらあとはWhiteWindの例と同じです。シンプルに本文だけ出力してみました。\n\n```html\n<ul><li v-for=\"p in likedPosts\">\n    <q>{{p.post.record.text}}</q>\n</li></ul>\n```\n\n`publicAgent`だけでも用途はそれなりに広がると思います。`getPosts`を使えば「likeした投稿一覧」とかも作れますね。\\\nただし、`searchPosts`はDOS対策でpublicエンドポイントからは使えません。\n\n他のエンドポイントを使いたい場合は`atprotoApi`から新しい`Agent`を作ったり単に`fetch`したりできますが、認証はできないものと思ってください。\\\nうっかりスクリプトにapp passwordを埋め込めば世界中に晒される羽目になります。\n\n## 雑感\n\natprotoサービスにはBlueskyのプロフィール情報を引っ張ってくるものが多いですが、独自のプロフィールを持つもの(例: [Tangled](https://pdsls.dev/at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.actor.profile/self))もあります。\\\nlexicon.communityでは過去にプロフィール情報を一つのcollectionにまとめようという[提案](https://github.com/orgs/lexicon-community/discussions/9)もありました。\\\n大体のサービスでは必要なものですが、そこにどんな情報を載せるかはサービスの性質によって様々です。\n\nAT Profileのシンプルな使い方の一つは、様々なサービスのプロフィールrecordを並べて、サービス横断のプロフィールページを作ることです。更新は勝手に追従してくれますし、一緒に投稿を見せたりもできます。\\\nテンプレート化されているので、一度誰かが作ってしまえばコピペするだけで共有もできるでしょう。\n\n様々な形態のサービスにまたがった単一のアカウントを運用できるのはatprotoの代表的な特徴ですが、AT Profileはそれを活かした数少ないサービスだと思います。\\\n[PDSls](https://pdsls.dev/)や[microcosm](https://www.microcosm.blue/)のようなインフラではなく、サービスとしてこういうものが出てくるというのは琴線に触れるものがありました。\n\n個人的には、こういう仕組みをモジュール化したような埋め込みビュー定義が、かつてatprotoに期待したものでした。Blueskyのプロトコル開発者達も似たような構想を時折口にするものの[^integ]、今でもそこを目指しているか定かではありません。\\\nAT Profileのような試みが、そんな未来に繋がる道になることを願います。\n\n[^integ]: 例えば、外部サービスの情報をカード形式で埋め込むというアイディアを検討していることが[開発者の発言](https://bsky.app/profile/pfrazee.com/post/3ju2fbwunf52e)で示されています。2023年に日本で行われたBluesky meetupでも、おそらくこれに関連して、[Bluesky Socialを様々なatprotoサービスへの導線にしたいという旨のコメント](https://youtu.be/dOAyiuOGAmY?si=vhsBb008xapjmorH&t=573)がありました。\n",
  "createdAt": "2025-10-04T12:47:38.119Z",
  "visibility": "public"
}