From 28eb463b741e4299178c6e4c98686a2f88eca989 Mon Sep 17 00:00:00 2001 From: syui Date: Sun, 18 Jan 2026 11:43:58 +0900 Subject: [PATCH] init --- .github/workflows/cf-pages.yml | 39 +++ .gitignore | 8 + Cargo.toml | 5 + config.json | 8 + .../ai.syui.log.post/3mchqlshygs2s.json | 18 ++ .../app.bsky.actor.profile/self.json | 18 ++ .../describe.json | 39 +++ lexicon/ai.syui.log.post.json | 68 ++++++ network.json | 12 + package.json | 4 + readme.md | 224 ++++++++++++++++++ 11 files changed, 443 insertions(+) create mode 100644 .github/workflows/cf-pages.yml create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 config.json create mode 100644 content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/3mchqlshygs2s.json create mode 100644 content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/app.bsky.actor.profile/self.json create mode 100644 content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/describe.json create mode 100644 lexicon/ai.syui.log.post.json create mode 100644 network.json create mode 100644 package.json create mode 100644 readme.md diff --git a/.github/workflows/cf-pages.yml b/.github/workflows/cf-pages.yml new file mode 100644 index 0000000..2e2661c --- /dev/null +++ b/.github/workflows/cf-pages.yml @@ -0,0 +1,39 @@ +name: Deploy to Cloudflare Pages + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + deployments: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + + - name: Install dependencies + run: npm install + + - name: Fetch content from ATProto + run: npm run fetch + + - name: Generate static site + run: npm run generate + + - name: Deploy to Cloudflare Pages + uses: cloudflare/pages-action@v1 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }} + directory: dist + gitHubToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..908a233 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +dist +repos +node_modules +package-lock.json +CLAUDE.md +.claude +.env +target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5254d46 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,5 @@ +[package] +name = "ailog" +version = "0.2.0" +edition = "2021" + diff --git a/config.json b/config.json new file mode 100644 index 0000000..a5358af --- /dev/null +++ b/config.json @@ -0,0 +1,8 @@ +{ + "title": "syui.ai", + "handle": "syui.syui.ai", + "collection": "ai.syui.log.post", + "network": "syu.is", + "color": "#EF454A", + "siteUrl": "https://syui.ai" +} diff --git a/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/3mchqlshygs2s.json b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/3mchqlshygs2s.json new file mode 100644 index 0000000..60ce66d --- /dev/null +++ b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/3mchqlshygs2s.json @@ -0,0 +1,18 @@ +{ + "uri": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/3mchqlshygs2s", + "cid": "bafyreielgn743kg5xotfj5x53edl25vkbbd2d6v7s3tydyyjsvczcluyme", + "value": { + "cid": "bafyreidymanu2xk4ftmvfdna3j7ixyijc37s6h3aytstuqgzatgjl4tp7e", + "uri": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/3mchqlshygs2s", + "$type": "ai.syui.log.post", + "title": "ailogを作り直した", + "content": "## ailogとは\n\natprotoと連携するサイトジェネレータ。\n\n## ailogの使い方\n\n```sh\n$ git clone https://git.syui.ai/ai/log\n$ cd log\n$ cat public/config.json\n{\n \"title\": \"syui.ai\",\n \"handle\": \"syui.syui.ai\",\n \"collection\": \"ai.syui.log.post\",\n \"network\": \"syu.is\",\n \"color\": \"#0066cc\",\n \"siteUrl\": \"https://syui.ai\"\n}\n---\n$ npm run dev\n```\n\n## ailogのコンセプト\n\n1. at-browserを基本にする\n2. atproto oauthでログインする\n3. ログインしたアカウントで記事をポストする\n\n## ailogの追加機能\n\n1. atproto recordからjsonをdownloadすると表示速度が上がる(ただし更新はlocalから)\n2. コメントはurlの言及を検索して表示\n\n```sh\n$ npm run fetch\n$ npm run generate\n```", + "createdAt": "2026-01-15T13:59:52.367Z", + "translations": { + "en": { + "title": "recreated ailog", + "content": "## What is ailog?\n\nA site generator that integrates with the atproto framework.\n\n## How to Use ailog\n\n```sh\n$ git clone https://git.syui.ai/ai/log\n$ cd log\n$ cat public/config.json\n{\n \"title\": \"syui.ai\",\n \"handle\": \"syui.syui.ai\",\n \"collection\": \"ai.syui.log.post\",\n \"network\": \"syu.is\",\n \"color\": \"#0066cc\",\n \"siteUrl\": \"https://syui.ai\"\n}\n---\n$ npm run dev\n```\n\n## ailog's Concept\n\n1. Based on at-browser as its foundation\n2. Authentication via atproto oAuth\n3. Post articles using the logged-in account\n\n## Additional Features of ailog\n\n1. Downloading JSON from atproto record improves display speed (though updates still come from local storage)\n2. Comments are displayed by searching for URL mentions\n\n```sh\n$ npm run fetch\n$ npm run generate\n```" + } + } + } +} diff --git a/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/app.bsky.actor.profile/self.json b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/app.bsky.actor.profile/self.json new file mode 100644 index 0000000..8a3ebf5 --- /dev/null +++ b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/app.bsky.actor.profile/self.json @@ -0,0 +1,18 @@ +{ + "uri": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/app.bsky.actor.profile/self", + "cid": "bafyreihlch2vdee6wpydo2bwap7nyzszjz6focbtxikz7zljcejxz27npy", + "value": { + "$type": "app.bsky.actor.profile", + "avatar": { + "$type": "blob", + "ref": { + "$link": "bafkreigta4pf5h7uvx6jpfcm3d6aeq4g3qpsiqjdoeytnutwp6vwc2yo7u" + }, + "mimeType": "image/jpeg", + "size": 166370 + }, + "createdAt": "2025-09-19T06:17:42Z", + "description": "", + "displayName": "syui" + } +} diff --git a/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/describe.json b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/describe.json new file mode 100644 index 0000000..2e46361 --- /dev/null +++ b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/describe.json @@ -0,0 +1,39 @@ +{ + "handle": "syui.syui.ai", + "did": "did:plc:vzsvtbtbnwn22xjqhcu3vd6y", + "didDoc": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/multikey/v1", + "https://w3id.org/security/suites/secp256k1-2019/v1" + ], + "id": "did:plc:vzsvtbtbnwn22xjqhcu3vd6y", + "alsoKnownAs": [ + "at://syui.syui.ai" + ], + "verificationMethod": [ + { + "id": "did:plc:vzsvtbtbnwn22xjqhcu3vd6y#atproto", + "type": "Multikey", + "controller": "did:plc:vzsvtbtbnwn22xjqhcu3vd6y", + "publicKeyMultibase": "zQ3shZj81oA4A9CmUQgYUv97nFdd7m5qNaRMyG16XZixytTmQ" + } + ], + "service": [ + { + "id": "#atproto_pds", + "type": "AtprotoPersonalDataServer", + "serviceEndpoint": "https://syu.is" + } + ] + }, + "collections": [ + "ai.syui.log.post", + "app.bsky.actor.profile", + "app.bsky.feed.post", + "app.bsky.feed.repost", + "app.bsky.graph.follow", + "chat.bsky.actor.declaration" + ], + "handleIsCorrect": true +} diff --git a/lexicon/ai.syui.log.post.json b/lexicon/ai.syui.log.post.json new file mode 100644 index 0000000..ad2f85b --- /dev/null +++ b/lexicon/ai.syui.log.post.json @@ -0,0 +1,68 @@ +{ + "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 (markdown)." + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "Client-declared timestamp when this post was originally created." + }, + "lang": { + "type": "string", + "maxLength": 10, + "description": "Language code of the original content (e.g., 'ja', 'en')." + }, + "translations": { + "type": "ref", + "ref": "#translationMap", + "description": "Translations of the post in other languages." + } + } + } + }, + "translationMap": { + "type": "object", + "description": "Map of language codes to translations.", + "properties": { + "en": { "type": "ref", "ref": "#translation" }, + "ja": { "type": "ref", "ref": "#translation" } + } + }, + "translation": { + "type": "object", + "description": "A translation of a post.", + "properties": { + "title": { + "type": "string", + "maxLength": 3000, + "maxGraphemes": 300 + }, + "content": { + "type": "string", + "maxLength": 1000000, + "maxGraphemes": 100000 + } + } + } + } +} diff --git a/network.json b/network.json new file mode 100644 index 0000000..62de6a3 --- /dev/null +++ b/network.json @@ -0,0 +1,12 @@ +{ + "bsky.social": { + "plc": "https://plc.directory", + "bsky": "https://public.api.bsky.app", + "web": "https://bsky.app" + }, + "syu.is": { + "plc": "https://plc.syu.is", + "bsky": "https://bsky.syu.is", + "web": "https://syu.is" + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2bda70c --- /dev/null +++ b/package.json @@ -0,0 +1,4 @@ +{ + "name": "ailog", + "version": "0.2.0" +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..bb09443 --- /dev/null +++ b/readme.md @@ -0,0 +1,224 @@ +# ailog + +ATProto-based blog platform built on at-browser. + +## Concept + +**Data lives in ATProto, not on this server.** + +This is not a traditional blog generator. It's a **viewer (client)** for ATProto records. + +``` +Traditional blog: + Server DB ← article data ← user + +ATProto blog: + User's PDS ← article data (ai.syui.log.post) + ↓ + at-browser (this site) → displays records +``` + +## Architecture + +``` +┌─────────────────────────────────────────┐ +│ at-browser │ +│ (ATProto record viewer/editor) │ +├─────────────────────────────────────────┤ +│ │ +│ / → admin (config.json) │ +│ /@alice → user page │ +│ /@bob.bsky → user page │ +│ │ +└─────────────────────────────────────────┘ +``` + +## Roles + +| Role | Path | Data Source | +|------|------|-------------| +| **admin** | `/` (root) | local + remote | +| **user** | `/@handle` | remote only | + +### Admin (Site Owner) + +- Defined in `config.json` +- Has root (`/`) access +- Can reference **local files** (static assets, custom styles) +- Can reference **remote** (ATProto records) + +### User (Any ATProto User) + +- Accessed via `/@handle` path +- **Remote only** (ATProto records from their PDS) +- No registration required +- Anyone with an ATProto account can be displayed + +## Features + +### 1. at-browser (Core) + +- Search by handle/DID +- Browse PDS collections +- Navigate ATProto records + +### 2. ai.syui.log.post View + +- Markdown rendering +- Syntax highlighting +- Blog-style display + +### 3. OAuth + +- Login with ATProto +- Post to ai.syui.log.post collection + +## Use Cases + +### Personal Blog + +```json +// config.json +{ + "did": "did:plc:xxxxx", + "handle": "syui.syui.ai" +} +``` + +- Deploy to `syui.ai` +- Root shows your profile + posts +- You are the admin (local + remote) +- Others can view via `/@handle` + +### Blog Service + +```json +// config.json +{ + "admin": "service.example.com", + "handle": null +} +``` + +- Deploy to `blog.example.com` +- Root shows landing/search +- All users via `/@handle` (remote only) +- Platform for any ATProto user + +## Data Flow + +``` +┌──────────────┐ ┌──────────────┐ +│ User's PDS │────→│ at-browser │ +│ (ATProto) │←────│ (this site) │ +└──────────────┘ └──────────────┘ + ↑ │ + │ ↓ + ai.syui.log.post ┌──────────┐ + collection │ Display │ + │ - Profile│ + │ - Posts │ + └──────────┘ +``` + +## Local = Remote (Same Format) + +**Critical design principle: local files use the exact same format as ATProto API responses.** + +This allows the same code to handle both data sources. + +### Remote (ATProto API) + +```bash +curl "https://syu.is/xrpc/com.atproto.repo.listRecords?repo=did:plc:xxx&collection=ai.syui.log.post" +``` + +```json +{ + "records": [ + { + "uri": "at://did:plc:xxx/ai.syui.log.post/3xxx", + "cid": "bafyrei...", + "value": { + "title": "Hello World", + "content": "# Hello\n\nThis is my post.", + "createdAt": "2025-01-01T00:00:00Z" + } + } + ] +} +``` + +### Local (Static File) + +``` +public/records/ai.syui.log.post/3xxx.json +``` + +```json +{ + "uri": "at://did:plc:xxx/ai.syui.log.post/3xxx", + "cid": "local", + "value": { + "title": "Hello World", + "content": "# Hello\n\nThis is my post.", + "createdAt": "2025-01-01T00:00:00Z" + } +} +``` + +### Resolution Strategy + +``` +at-browser + │ + ├── admin (config.json user) + │ ├── 1. Check local: /records/{collection}/{rkey}.json + │ └── 2. Fallback to remote: PDS API + │ + └── user (/@handle) + └── remote only: PDS API +``` + +### Why Same Format? + +- **One codebase**: No branching logic for local vs remote +- **Easy testing**: Copy API response to local file +- **Offline support**: Admin can work with local files +- **Migration**: Local → Remote (just POST to PDS) + +## Config + +### config.json + +```json +{ + "did": "did:plc:xxxxx", + "handle": "syui.syui.ai", + "pds": "syu.is", + "collection": "ai.syui.log.post" +} +``` + +## Tech Stack + +- **Frontend**: Vite + TypeScript +- **ATProto**: @atproto/api +- **OAuth**: @atproto/oauth-client-browser +- **Markdown**: marked + highlight.js + +## Collection Schema + +### ai.syui.log.post + +```json +{ + "title": "Post Title", + "content": "Markdown content...", + "createdAt": "2025-01-01T00:00:00Z" +} +``` + +## License + +MIT