8.7 KiB
8.7 KiB
ailog
概要
atprotoベースの静的ブログジェネレーター。markdownファイルをatproto recordとして保存し、それを元に静的サイトを生成する。
Collection Schema
ai.syui.log.post (ブログ記事)
{
"lexicon": 1,
"id": "ai.syui.log.post",
"defs": {
"main": {
"type": "record",
"description": "Record containing a blog post.",
"key": "tid",
"record": {
"type": "object",
"required": ["title", "content", "createdAt"],
"properties": {
"title": {
"type": "string",
"maxLength": 3000,
"maxGraphemes": 300,
"description": "The title of the post."
},
"content": {
"type": "string",
"maxLength": 1000000,
"maxGraphemes": 100000,
"description": "The content of the post."
},
"createdAt": {
"type": "string",
"format": "datetime",
"description": "Client-declared timestamp when this post was originally created."
}
}
}
}
}
}
ai.syui.log.comment (コメント)
{
"lexicon": 1,
"id": "ai.syui.log.comment",
"defs": {
"main": {
"type": "record",
"description": "Record containing a comment.",
"key": "tid",
"record": {
"type": "object",
"required": ["content", "createdAt", "post"],
"properties": {
"content": {
"type": "string",
"maxLength": 100000,
"maxGraphemes": 10000,
"description": "The content of the comment."
},
"createdAt": {
"type": "string",
"format": "datetime",
"description": "Client-declared timestamp when this comment was originally created."
},
"parent": {
"type": "ref",
"ref": "com.atproto.repo.strongRef"
},
"post": {
"type": "ref",
"ref": "com.atproto.repo.strongRef"
}
}
}
}
}
}
CLIコマンド仕様
login - ログイン
ailog login ${handle} -p ${password} -s ${pds}
- handleからDID, PDSを解決
- 認証情報を
~/.config/syui/ai/log/config.jsonに保存
post - 記事をatprotoに投稿
ailog post
./content/post/*.mdを読み込む- frontmatterからtitle, dateなどを抽出
- markdown本文をcontentに設定
ai.syui.log.postとしてputRecord- 既存recordがあれば更新、なければ新規作成
build - 静的サイト生成
ailog build
- atprotoから
ai.syui.log.postのrecordを取得 - markdownをHTMLに変換
- 静的ファイルとして
./publicに出力
URL構造
記事一覧
localhost/at://did:plc:ragtjsm2j2vknwkz3zp4oxrd/ai.syui.log.post/
- atprotoから全recordを取得
- 一覧表示
個別記事
localhost/.../3mbnbdt4bas2a
- atproto URIからrecordを取得
- markdownをレンダリングして表示
データフロー
記事投稿
./content/post/*.md
↓ (frontmatter + markdown解析)
ailog post
↓ (putRecord)
atproto PDS (ai.syui.log.post)
静的サイト生成
atproto PDS (ai.syui.log.post)
↓ (listRecords)
ailog build
↓ (markdown → HTML)
./public/*.html
at browser統合
- atproto URIでの記事アクセス
- handle解決 → DID → PDS URLの取得
- NetworkConfig (pdsApi, bskyApi, plcApi) の割り当て
match pds {
"bsky.social" | "bsky.app" => NetworkConfig {
pds_api: format!("https://{}", pds),
plc_api: "https://plc.directory".to_string(),
bsky_api: "https://public.api.bsky.app".to_string(),
web_url: "https://bsky.app".to_string(),
},
"syu.is" => NetworkConfig {
pds_api: "https://syu.is".to_string(),
plc_api: "https://plc.syu.is".to_string(),
bsky_api: "https://bsky.syu.is".to_string(),
web_url: "https://web.syu.is".to_string(),
},
_ => NetworkConfig {
pds_api: format!("https://{}", pds),
plc_api: "https://plc.directory".to_string(),
bsky_api: "https://public.api.bsky.app".to_string(),
web_url: "https://bsky.app".to_string(),
}
}
参考実装
./repos/log- 既存のailog実装 (Rust)./repos/frontpage- frontpage (fyi.unravel.frontpage)./repos/pfrazee.com- Paul FrazeeのLeaflet実装 (TypeScript)
Paul Frazeeの仕組み
pfrazee.comは以下の流れでLeafletをblogに統合:
-
lexicon schema取得
lex install pub.leaflet.document lex build --out ./util -
データ取得
// handle → DID → PDS const did = await resolver.handle.resolve('pfrazee.com') const pds = await resolver.did.resolveAtprotoData(did).pds // listRecords const result = await client.list(leaflet.document, { repo: did, limit: 50 }) -
画像取得
for (const blobRef of enumBlobRefs(leaflet.value)) { const blobRes = await client.getBlob(did, blobRef.ref) await fsp.writeFile(imagePath, blobRes.payload.body) } -
静的サイト生成 (Next.js)
- JSONからHTMLレンダリング
- 型安全なblock rendering
既存のai.syui.log形式
現在のchat形式:
{
"uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log/2025-06-14-blog",
"value": {
"url": "https://syui.ai/posts/2025-06-14-blog",
"post": {
"url": "https://syui.ai/posts/2025-06-14-blog",
"date": "",
"slug": "",
"tags": [],
"title": "syui.ai",
"language": "ja"
},
"text": "test",
"type": "comment",
"$type": "ai.syui.log",
"author": {
"did": "did:plc:6qyecktefllvenje24fcxnie",
"avatar": "https://...",
"handle": "ai.syui.ai",
"displayName": "ai"
},
"createdAt": "2025-06-17T06:24:37.386Z"
}
}
設定ファイル
~/.config/syui/ai/log/config.json
{
"pds": "syu.is",
"handle": "ai.syui.ai",
"did": "did:plc:6qyecktefllvenje24fcxnie",
"access_jwt": "...",
"refresh_jwt": "..."
}
~/.config/syui/ai/log/mapping.json
ファイル名とatproto recordの紐付け(自動生成):
{
"2026-01-08-test.md": {
"rkey": "3mbnbdt4bas2a",
"uri": "at://did:plc:xxx/ai.syui.log.post/3mbnbdt4bas2a",
"cid": "bafyrei..."
}
}
動作:
- すべてのAPI呼び出し前に自動でrefreshSessionを実行してJWTトークンを更新
ailog post- 初回投稿時にTID形式のrkeyが自動生成され、mappingに保存ailog post- 2回目以降は既存rkeyで更新(putRecord)ailog delete- 全削除時にmappingもクリア
ディレクトリ構造
ailog/
├── content/
│ └── post/
│ ├── 2025-01-08-example.md
│ └── ...
├── public/
│ ├── index.html
│ ├── posts/
│ │ └── 2025-01-08-example.html
│ └── at/
│ └── ...
├── lexicons/
│ └── ai/
│ └── syui/
│ └── log/
│ ├── post.json
│ └── comment.json
└── templates/
├── index.html
├── post.html
└── layout.html
開発優先順位
-
CLIコマンド実装 ✅
- collection schema定義
ailog login- handle → DID解決 + JWT保存ailog post- ./content/post/*.md → putRecordailog build- listRecords → 静的HTML生成
-
at browser統合 (進行中)
- React/TypeScript環境セットアップ
- atproto client library (repos/log/pdsベース)
- at browser components
- 静的サイトへの統合
- ai.syui.log.post 表示スタイル
-
スタイリング
- Tailwind CSS統合
- repos/log風デザイン
実装済み
Rust CLI (src/)
src/main.rs- CLIエントリーポイントsrc/config.rs- ~/.config/syui/ai/log/config.json管理src/login.rs- atproto認証 (describeRepo + createSession)src/refresh.rs- セッションリフレッシュ (refreshSession)src/post.rs- markdown投稿 (createRecord/putRecord)src/build.rs- 静的サイト生成 (listRecords + markdown→HTML)src/delete.rs- record削除 (deleteRecord)
at browser (browser/)
browser/package.json- React + TypeScript + Tailwind- 実装予定: repos/log/pdsの実装をベースに作成
使用方法
# ログイン
ailog l ai.syui.ai -p PASSWORD -s syu.is
# 記事投稿(初回: 新規作成、2回目以降: 更新)
ailog p
# サイト生成
ailog b
# 全削除
ailog d
エイリアス:
l=loginp=postb=buildd=delete