diff --git a/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/3mchqlshygs2s.json b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/3mchqlshygs2s.json index f675bd5..a3c91cf 100644 --- a/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/3mchqlshygs2s.json +++ b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/3mchqlshygs2s.json @@ -1,13 +1,13 @@ { - "cid": "bafyreidymanu2xk4ftmvfdna3j7ixyijc37s6h3aytstuqgzatgjl4tp7e", + "uri": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/3mchqlshygs2s", + "cid": "bafyreielgn743kg5xotfj5x53edl25vkbbd2d6v7s3tydyyjsvczcluyme", + "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", - "title": "ailogを作り直した", "translations": { "en": { - "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```", - "title": "recreated ailog" + "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```" } - }, - "uri": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/3mchqlshygs2s" + } } \ No newline at end of file diff --git a/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/atproto.com.png b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/atproto.com.png new file mode 100644 index 0000000..4c04c29 Binary files /dev/null and b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/atproto.com.png differ diff --git a/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/frontpage.fyi.png b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/frontpage.fyi.png new file mode 100644 index 0000000..759fabd Binary files /dev/null and b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/frontpage.fyi.png differ diff --git a/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/leaflet.pub.png b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/leaflet.pub.png new file mode 100644 index 0000000..0aab0d3 Binary files /dev/null and b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/leaflet.pub.png differ diff --git a/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/linkat.blue.png b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/linkat.blue.png new file mode 100644 index 0000000..4e6e984 Binary files /dev/null and b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/linkat.blue.png differ diff --git a/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/pinksea.art.png b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/pinksea.art.png new file mode 100644 index 0000000..2423469 Binary files /dev/null and b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/pinksea.art.png differ diff --git a/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/syu.is.png b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/syu.is.png new file mode 100644 index 0000000..f8f45fb Binary files /dev/null and b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/syu.is.png differ diff --git a/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/tangled.sh.png b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/tangled.sh.png new file mode 100644 index 0000000..1e660b7 Binary files /dev/null and b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/tangled.sh.png differ diff --git a/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/whtwnd.com.png b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/whtwnd.com.png new file mode 100644 index 0000000..d7e99b9 Binary files /dev/null and b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/whtwnd.com.png differ diff --git a/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/profile.json b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/profile.json index c7441a2..4b4ad7c 100644 --- a/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/profile.json +++ b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/profile.json @@ -17,5 +17,5 @@ "indexedAt": "2025-09-19T06:17:42.000Z", "followersCount": 1, "followsCount": 1, - "postsCount": 74 + "postsCount": 77 } \ No newline at end of file diff --git a/public/.well-known/lexicon/ai.syui.log.post.json b/public/.well-known/lexicon/ai.syui.log.post.json index bb7f93b..ad2f85b 100644 --- a/public/.well-known/lexicon/ai.syui.log.post.json +++ b/public/.well-known/lexicon/ai.syui.log.post.json @@ -20,15 +20,49 @@ "type": "string", "maxLength": 1000000, "maxGraphemes": 100000, - "description": "The content of the post." + "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/public/.well-known/lexicon/ai/syui/log/post.json b/public/.well-known/lexicon/ai/syui/log/post.json new file mode 100644 index 0000000..ad2f85b --- /dev/null +++ b/public/.well-known/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/scripts/generate.ts b/scripts/generate.ts index 82c9b5a..e858a40 100644 --- a/scripts/generate.ts +++ b/scripts/generate.ts @@ -199,12 +199,14 @@ function getFaviconDir(did: string): string { async function downloadFavicon(url: string, filepath: string): Promise { try { - const res = await fetch(url) + const res = await fetch(url, { redirect: 'follow' }) if (!res.ok) return false const buffer = await res.arrayBuffer() + if (buffer.byteLength === 0) return false fs.writeFileSync(filepath, Buffer.from(buffer)) return true - } catch { + } catch (err) { + console.error(`Failed to download ${url}:`, err) return false } } @@ -226,8 +228,21 @@ function getServiceDomain(collection: string): string | null { return null } +// Common service domains to always download favicons for +const COMMON_SERVICE_DOMAINS = [ + 'bsky.app', + 'syui.ai', + 'atproto.com', + 'whtwnd.com', + 'frontpage.fyi', + 'pinksea.art', + 'linkat.blue', + 'tangled.sh', + 'leaflet.pub', +] + function getServiceDomains(collections: string[]): string[] { - const domains = new Set() + const domains = new Set(COMMON_SERVICE_DOMAINS) for (const col of collections) { const domain = getServiceDomain(col) if (domain) domains.add(domain) @@ -241,21 +256,22 @@ async function downloadFavicons(did: string, domains: string[]): Promise { fs.mkdirSync(faviconDir, { recursive: true }) } + // Known favicon URLs (prefer official sources over Google) + // Others will use Google's favicon API as fallback const faviconUrls: Record = { 'bsky.app': 'https://bsky.app/static/favicon-32x32.png', 'syui.ai': 'https://syui.ai/favicon.png', } for (const domain of domains) { - const url = faviconUrls[domain] - if (!url) continue - const filepath = path.join(faviconDir, `${domain}.png`) - if (!fs.existsSync(filepath)) { - const ok = await downloadFavicon(url, filepath) - if (ok) { - console.log(`Downloaded: ${domain}.png`) - } + if (fs.existsSync(filepath)) continue + + // Try known URL first, then fallback to Google's favicon API + const url = faviconUrls[domain] || `https://www.google.com/s2/favicons?domain=${domain}&sz=32` + const ok = await downloadFavicon(url, filepath) + if (ok) { + console.log(`Downloaded: ${domain}.png`) } } } diff --git a/src/components/atbrowser.ts b/src/components/atbrowser.ts index 7a27eeb..5a08dd6 100644 --- a/src/components/atbrowser.ts +++ b/src/components/atbrowser.ts @@ -1,6 +1,17 @@ -import { describeRepo, listRecordsRaw, getRecordRaw, fetchLexicon, resolveHandle, getServiceInfo, resolvePds, getPlc } from '../lib/api.js' +import { describeRepo, listRecordsRaw, getRecordRaw, fetchLexicon, resolveHandle, getServiceInfo, resolvePds, getPlcForPds } from '../lib/api.js' import { deleteRecord } from '../lib/auth.js' import { escapeHtml } from '../lib/utils.js' +import type { Networks } from '../types.js' + +// Cache networks config +let networksConfig: Networks | null = null + +async function loadNetworks(): Promise { + if (networksConfig) return networksConfig + const res = await fetch('/networks.json') + networksConfig = await res.json() + return networksConfig! +} function extractRkey(uri: string): string { const parts = uri.split('/') @@ -8,13 +19,15 @@ function extractRkey(uri: string): string { } async function renderServices(did: string, handle: string): Promise { - const [collections, pds] = await Promise.all([ + const [collections, pds, networks] = await Promise.all([ describeRepo(did), - resolvePds(did) + resolvePds(did), + loadNetworks() ]) - // Server info section - const plcUrl = `${getPlc()}/${did}/log` + // Server info section - use PLC based on PDS + const plc = getPlcForPds(pds, networks) + const plcUrl = `${plc}/${did}/log` const serverHtml = `

Server

diff --git a/src/lib/api.ts b/src/lib/api.ts index 809e1f5..0eefe4f 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -14,6 +14,33 @@ export function getPlc(): string { return networkConfig?.plc || 'https://plc.directory' } +// Get PLC URL based on PDS endpoint +export function getPlcForPds(pds: string, networks: Record): string { + // Check if PDS matches any network + for (const [_key, config] of Object.entries(networks)) { + // Match by domain (e.g., "https://syu.is" or "https://bsky.syu.is") + try { + const pdsHost = new URL(pds).hostname + const bskyHost = new URL(config.bsky).hostname + // Check if PDS host matches network's bsky host + if (pdsHost === bskyHost || pdsHost.endsWith('.' + bskyHost)) { + return config.plc + } + // Also check web host if available + if (config.web) { + const webHost = new URL(config.web).hostname + if (pdsHost === webHost || pdsHost.endsWith('.' + webHost)) { + return config.plc + } + } + } catch { + continue + } + } + // Default to plc.directory + return 'https://plc.directory' +} + function getBsky(): string { return networkConfig?.bsky || 'https://public.api.bsky.app' }