add @user
@@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 578 B |
|
After Width: | Height: | Size: 938 B |
|
After Width: | Height: | Size: 349 B |
|
After Width: | Height: | Size: 218 B |
BIN
content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/syu.is.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/tangled.sh.png
Normal file
|
After Width: | Height: | Size: 726 B |
BIN
content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/whtwnd.com.png
Normal file
|
After Width: | Height: | Size: 538 B |
@@ -17,5 +17,5 @@
|
||||
"indexedAt": "2025-09-19T06:17:42.000Z",
|
||||
"followersCount": 1,
|
||||
"followsCount": 1,
|
||||
"postsCount": 74
|
||||
"postsCount": 77
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
68
public/.well-known/lexicon/ai/syui/log/post.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
/* /index.html 200
|
||||
@@ -199,12 +199,14 @@ function getFaviconDir(did: string): string {
|
||||
|
||||
async function downloadFavicon(url: string, filepath: string): Promise<boolean> {
|
||||
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<string>()
|
||||
const domains = new Set<string>(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<void> {
|
||||
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<string, string> = {
|
||||
'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`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -801,12 +817,20 @@ async function generate() {
|
||||
console.log('Generated: /app.html')
|
||||
|
||||
// Generate _redirects for Cloudflare Pages (SPA routes)
|
||||
const redirects = `/app / 301
|
||||
/oauth/* /app.html 200
|
||||
// Static files (index.html, post/*/index.html) are served automatically
|
||||
// Dynamic routes are rewritten to app.html
|
||||
const redirects = `/oauth/* /app.html 200
|
||||
/at/* /app.html 200
|
||||
/new /app.html 200
|
||||
/app /app.html 200
|
||||
`
|
||||
fs.writeFileSync(path.join(distDir, '_redirects'), redirects)
|
||||
console.log('Generated: /_redirects')
|
||||
|
||||
// Generate 404.html as SPA fallback for unmatched routes (like /@handle)
|
||||
fs.writeFileSync(path.join(distDir, '404.html'), spaHtml)
|
||||
console.log('Generated: /404.html')
|
||||
|
||||
// Copy static files
|
||||
const filesToCopy = ['favicon.png', 'favicon.svg', 'config.json', 'networks.json', 'client-metadata.json', 'links.json']
|
||||
for (const file of filesToCopy) {
|
||||
|
||||
@@ -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<Networks> {
|
||||
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<string> {
|
||||
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 = `
|
||||
<div class="server-info">
|
||||
<h3>Server</h3>
|
||||
|
||||
@@ -13,6 +13,9 @@ export function renderHeader(currentHandle: string, isLoggedIn: boolean, userHan
|
||||
</svg>
|
||||
</button>`
|
||||
|
||||
// Use logged-in user's handle for input if available
|
||||
const inputHandle = isLoggedIn && userHandle ? userHandle : currentHandle
|
||||
|
||||
return `
|
||||
<div class="header">
|
||||
<form class="header-form" id="header-form">
|
||||
@@ -21,7 +24,7 @@ export function renderHeader(currentHandle: string, isLoggedIn: boolean, userHan
|
||||
class="header-input"
|
||||
id="header-input"
|
||||
placeholder="handle (e.g., syui.ai)"
|
||||
value="${currentHandle}"
|
||||
value="${inputHandle}"
|
||||
>
|
||||
<button type="submit" class="header-btn at-btn" title="Browse">@</button>
|
||||
${loginBtn}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { renderMarkdown } from '../lib/markdown.js'
|
||||
import { escapeHtml, formatDate } from '../lib/utils.js'
|
||||
import { renderDiscussionLink, loadDiscussionPosts, getAppUrl } from './discussion.js'
|
||||
|
||||
export function mountPostList(container: HTMLElement, posts: BlogPost[]): void {
|
||||
export function mountPostList(container: HTMLElement, posts: BlogPost[], userHandle?: string): void {
|
||||
if (posts.length === 0) {
|
||||
container.innerHTML = '<p class="no-posts">No posts yet</p>'
|
||||
return
|
||||
@@ -12,9 +12,11 @@ export function mountPostList(container: HTMLElement, posts: BlogPost[]): void {
|
||||
|
||||
const html = posts.map(post => {
|
||||
const rkey = post.uri.split('/').pop()
|
||||
// Use /@handle/post/rkey for user pages, /post/rkey for own blog
|
||||
const postUrl = userHandle ? `/@${userHandle}/post/${rkey}` : `/post/${rkey}`
|
||||
return `
|
||||
<li class="post-item">
|
||||
<a href="/post/${rkey}" class="post-link">
|
||||
<a href="${postUrl}" class="post-link">
|
||||
<span class="post-title">${escapeHtml(post.title)}</span>
|
||||
<span class="post-date">${formatDate(post.createdAt)}</span>
|
||||
</a>
|
||||
|
||||
@@ -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, { plc: string; bsky: string; web?: string }>): 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'
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface Route {
|
||||
type: 'blog' | 'post' | 'browser-services' | 'browser-collections' | 'browser-record' | 'new'
|
||||
type: 'blog' | 'post' | 'browser-services' | 'browser-collections' | 'browser-record' | 'new' | 'user-blog' | 'user-post'
|
||||
handle?: string
|
||||
collection?: string
|
||||
rkey?: string
|
||||
@@ -33,6 +33,16 @@ export function parseRoute(pathname: string): Route {
|
||||
return { type: 'new' }
|
||||
}
|
||||
|
||||
// /@${handle} - User blog (any user's posts)
|
||||
// /@${handle}/post/${rkey} - User post detail
|
||||
if (parts[0].startsWith('@')) {
|
||||
const handle = parts[0].slice(1) // Remove @ prefix
|
||||
if (parts[1] === 'post' && parts[2]) {
|
||||
return { type: 'user-post', handle, rkey: parts[2] }
|
||||
}
|
||||
return { type: 'user-blog', handle }
|
||||
}
|
||||
|
||||
// /at/${handle} - Browser services
|
||||
// /at/${handle}/${service-or-collection} - Browser collections or records
|
||||
// /at/${handle}/${collection}/${rkey} - Browser record detail
|
||||
@@ -75,6 +85,10 @@ export function buildPath(route: Route): string {
|
||||
return '/new'
|
||||
case 'post':
|
||||
return `/post/${route.rkey}`
|
||||
case 'user-blog':
|
||||
return `/@${route.handle}`
|
||||
case 'user-post':
|
||||
return `/@${route.handle}/post/${route.rkey}`
|
||||
case 'browser-services':
|
||||
return `/at/${route.handle}`
|
||||
case 'browser-collections':
|
||||
|
||||
123
src/main.ts
@@ -94,6 +94,40 @@ function renderFooter(handle: string): string {
|
||||
`
|
||||
}
|
||||
|
||||
// Detect network from handle domain
|
||||
// e.g., syui.ai → bsky.social, syui.syui.ai → syu.is
|
||||
function detectNetworkFromHandle(handle: string): string {
|
||||
const parts = handle.split('.')
|
||||
if (parts.length >= 2) {
|
||||
// Get domain part (last 2 parts for most cases)
|
||||
const domain = parts.slice(-2).join('.')
|
||||
// Check if domain matches any network key
|
||||
if (networks[domain]) {
|
||||
return domain
|
||||
}
|
||||
// Check if it's a subdomain of a known network
|
||||
for (const networkKey of Object.keys(networks)) {
|
||||
if (handle.endsWith(`.${networkKey}`) || handle.endsWith(networkKey)) {
|
||||
return networkKey
|
||||
}
|
||||
}
|
||||
}
|
||||
// Default to bsky.social
|
||||
return 'bsky.social'
|
||||
}
|
||||
|
||||
function switchNetwork(newNetwork: string): void {
|
||||
if (newNetwork === browserNetwork) return
|
||||
browserNetwork = newNetwork
|
||||
localStorage.setItem('browserNetwork', newNetwork)
|
||||
const networkConfig = networks[newNetwork]
|
||||
if (networkConfig) {
|
||||
setNetworkConfig(networkConfig)
|
||||
setAuthNetworkConfig(networkConfig)
|
||||
}
|
||||
updatePdsSelector()
|
||||
}
|
||||
|
||||
function renderPdsSelector(): string {
|
||||
const networkKeys = Object.keys(networks)
|
||||
const options = networkKeys.map(key => {
|
||||
@@ -220,10 +254,11 @@ function applyTitleTranslations(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function renderTabs(activeTab: 'blog' | 'browser' | 'new', isLoggedIn: boolean): string {
|
||||
function renderTabs(activeTab: 'blog' | 'browser' | 'new', isLoggedIn: boolean, handle?: string): string {
|
||||
const browserHandle = handle || config.handle
|
||||
let tabs = `
|
||||
<a href="/" class="tab ${activeTab === 'blog' ? 'active' : ''}" id="blog-tab">Blog</a>
|
||||
<button type="button" class="tab ${activeTab === 'browser' ? 'active' : ''}" id="browser-tab" data-handle="${config.handle}">Browser</button>
|
||||
<button type="button" class="tab ${activeTab === 'browser' ? 'active' : ''}" id="browser-tab" data-handle="${browserHandle}">Browser</button>
|
||||
`
|
||||
|
||||
if (isLoggedIn) {
|
||||
@@ -242,6 +277,10 @@ function openBrowser(handle: string, service: string | null = null, collection:
|
||||
|
||||
if (!contentEl || !tabsEl) return
|
||||
|
||||
// Auto-detect and switch network based on handle
|
||||
const detectedNetwork = detectNetworkFromHandle(handle)
|
||||
switchNetwork(detectedNetwork)
|
||||
|
||||
// Save current content if not already in browser mode
|
||||
if (!browserMode) {
|
||||
savedContent = {
|
||||
@@ -678,8 +717,10 @@ async function render(): Promise<void> {
|
||||
const handle = route.handle || config.handle
|
||||
|
||||
// Skip re-rendering for static blog/post pages (but still mount header for login)
|
||||
// Exception: if logged in on blog page, re-render to show user's blog
|
||||
const isStaticRoute = route.type === 'blog' || route.type === 'post'
|
||||
if (isStatic && isStaticRoute) {
|
||||
const shouldUseStatic = isStatic && isStaticRoute && !(isLoggedIn && route.type === 'blog')
|
||||
if (shouldUseStatic) {
|
||||
// Only mount header for login functionality (pass isStatic=true to skip unnecessary re-render)
|
||||
mountHeader(headerEl, handle, isLoggedIn, authSession?.handle, {
|
||||
onBrowse: (newHandle) => {
|
||||
@@ -826,21 +867,83 @@ async function render(): Promise<void> {
|
||||
}
|
||||
break
|
||||
|
||||
case 'blog':
|
||||
default:
|
||||
case 'user-blog':
|
||||
// /@{handle} - Any user's blog
|
||||
try {
|
||||
const profile = await getProfile(config.handle)
|
||||
const webUrl = networks[config.network]?.web
|
||||
profileEl.innerHTML = renderTabs('blog', isLoggedIn)
|
||||
const userHandle = route.handle!
|
||||
// Auto-detect and switch network based on handle
|
||||
const detectedNetwork = detectNetworkFromHandle(userHandle)
|
||||
switchNetwork(detectedNetwork)
|
||||
const profile = await getProfile(userHandle)
|
||||
const webUrl = networks[browserNetwork]?.web
|
||||
profileEl.innerHTML = renderTabs('blog', isLoggedIn, userHandle)
|
||||
const profileContentEl = document.createElement('div')
|
||||
profileEl.appendChild(profileContentEl)
|
||||
mountProfile(profileContentEl, profile, webUrl)
|
||||
|
||||
const servicesHtml = await renderServices(config.handle)
|
||||
const servicesHtml = await renderServices(userHandle)
|
||||
profileContentEl.insertAdjacentHTML('beforeend', servicesHtml)
|
||||
|
||||
const posts = await listRecords(profile.did, config.collection)
|
||||
mountPostList(contentEl, posts)
|
||||
mountPostList(contentEl, posts, userHandle)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
contentEl.innerHTML = `<p class="error">Failed to load: ${err}</p>`
|
||||
}
|
||||
break
|
||||
|
||||
case 'user-post':
|
||||
// /@{handle}/post/{rkey} - Any user's post detail
|
||||
try {
|
||||
const userHandle = route.handle!
|
||||
// Auto-detect and switch network based on handle
|
||||
const detectedNetwork = detectNetworkFromHandle(userHandle)
|
||||
switchNetwork(detectedNetwork)
|
||||
const profile = await getProfile(userHandle)
|
||||
const webUrl = networks[browserNetwork]?.web
|
||||
profileEl.innerHTML = renderTabs('blog', isLoggedIn, userHandle)
|
||||
const profileContentEl = document.createElement('div')
|
||||
profileEl.appendChild(profileContentEl)
|
||||
mountProfile(profileContentEl, profile, webUrl)
|
||||
|
||||
const servicesHtml = await renderServices(userHandle)
|
||||
profileContentEl.insertAdjacentHTML('beforeend', servicesHtml)
|
||||
|
||||
const post = await getRecord(profile.did, config.collection, route.rkey!)
|
||||
if (post) {
|
||||
const canEdit = isLoggedIn && authSession?.did === profile.did
|
||||
mountPostDetail(contentEl, post, userHandle, config.collection, canEdit, undefined, browserNetwork)
|
||||
} else {
|
||||
contentEl.innerHTML = '<p>Post not found</p>'
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
contentEl.innerHTML = `<p class="error">Failed to load: ${err}</p>`
|
||||
}
|
||||
break
|
||||
|
||||
case 'blog':
|
||||
default:
|
||||
try {
|
||||
// If logged in, show logged-in user's blog instead of site owner's
|
||||
const blogHandle = isLoggedIn ? authSession!.handle : config.handle
|
||||
const detectedNetwork = isLoggedIn ? detectNetworkFromHandle(blogHandle) : config.network
|
||||
if (isLoggedIn) {
|
||||
switchNetwork(detectedNetwork)
|
||||
}
|
||||
const profile = await getProfile(blogHandle)
|
||||
const webUrl = networks[detectedNetwork]?.web
|
||||
profileEl.innerHTML = renderTabs('blog', isLoggedIn, blogHandle)
|
||||
const profileContentEl = document.createElement('div')
|
||||
profileEl.appendChild(profileContentEl)
|
||||
mountProfile(profileContentEl, profile, webUrl)
|
||||
|
||||
const servicesHtml = await renderServices(blogHandle)
|
||||
profileContentEl.insertAdjacentHTML('beforeend', servicesHtml)
|
||||
|
||||
const posts = await listRecords(profile.did, config.collection)
|
||||
// Use handle for post links if logged in user
|
||||
mountPostList(contentEl, posts, isLoggedIn ? blogHandle : undefined)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
contentEl.innerHTML = `<p class="error">Failed to load: ${err}</p>`
|
||||
|
||||