From e704e527613c9105a806d7411606a4cb012f0060 Mon Sep 17 00:00:00 2001 From: syui Date: Fri, 16 Jan 2026 16:48:55 +0900 Subject: [PATCH] add @user --- .../ai.syui.log.post/3mchqlshygs2s.json | 12 +- .../favicons/atproto.com.png | Bin 0 -> 1482 bytes .../favicons/frontpage.fyi.png | Bin 0 -> 578 bytes .../favicons/leaflet.pub.png | Bin 0 -> 938 bytes .../favicons/linkat.blue.png | Bin 0 -> 349 bytes .../favicons/pinksea.art.png | Bin 0 -> 218 bytes .../favicons/syu.is.png | Bin 0 -> 29204 bytes .../favicons/tangled.sh.png | Bin 0 -> 726 bytes .../favicons/whtwnd.com.png | Bin 0 -> 538 bytes .../profile.json | 2 +- .../.well-known/lexicon/ai.syui.log.post.json | 36 ++++- .../.well-known/lexicon/ai/syui/log/post.json | 68 ++++++++++ public/_redirects | 1 - scripts/generate.ts | 50 +++++-- src/components/atbrowser.ts | 23 +++- src/components/browser.ts | 5 +- src/components/posts.ts | 6 +- src/lib/api.ts | 27 ++++ src/lib/router.ts | 16 ++- src/main.ts | 123 ++++++++++++++++-- 20 files changed, 328 insertions(+), 41 deletions(-) create mode 100644 content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/atproto.com.png create mode 100644 content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/frontpage.fyi.png create mode 100644 content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/leaflet.pub.png create mode 100644 content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/linkat.blue.png create mode 100644 content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/pinksea.art.png create mode 100644 content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/syu.is.png create mode 100644 content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/tangled.sh.png create mode 100644 content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/whtwnd.com.png create mode 100644 public/.well-known/lexicon/ai/syui/log/post.json delete mode 100644 public/_redirects 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 0000000000000000000000000000000000000000..4c04c29d6bf4ac3e48066dceac2cb8202fac7c01 GIT binary patch literal 1482 zcmV;*1vUDKP)b_3z=Fea<>(MjgA*_q4yu`o7<{_S$Q$ z4gAl-u?EgtZ70QXaUm672Y6Eeoo#?gA_D-B^t*`fC~l(F#zP3c-4*fZKL(f`7N_{e zMu2W5Y3d#ez#Mm+q!`faDVtu=7xDK~2AKQ8PwhN<`dUNYeKN=q8PxzXDB+20Px_TH z1I+0yUg9Z!pX3z)8k&`~S03LpvG!QvH_{AB_Cf-eP+TBrM&nFX%2?U;m)hXLbrFjv z1eoYyFpUQR|KqO~A}h6I)og57fhkyx8>#>PyAJwS5uGqXNtf z6||asj^tFQ_>Z8sq`$lE`x6yzmab6#F*EoG;Xh3>o!Bk(bhbOaVpM=klz)}P(mF9d zw{hE|Tx1TON^w^@!~8rMZKy$5zzUt&DCh^AW!w>#uB;Td6;A2kaD2hd|D-qoY>k2~ zqVA{}3_znOQEM}h=^6T%hBqd9OfJr?YTwYDaNcS=DIP8@q0uZ!7ZNxCVP|Zx`A8h> z9Pbx10N{!H-Pe|hhCU*>B+<*ztZm)t?S7I}aj{lhLN3?cRQ#RWyv*)(D{Z;3${2b8 zK(f|`4`70q@1xiS09N)j01qX@t@T6Ci)#QLcrSqM1_0vW;v%;ei5HtY&+J~ehCmNl zDL$GImEzTB>-ut0>kdi#YX+$csICE0i|ckN9k*?2C?4hK6L{3M3EB?eRS7Rk+BFJK zc@$f1R!LrURj3JmRdc`Bof}6kyk)Lph2_~W{d}T#h4P0@{&kY~)`IBnhY56Q4;`*n zl+G~#An9}`?k9IpS12FQP4y($mJNgF-I%_JWz!NYlazNkn*pfdKV02|oltoNz(mJ> z#0@I=#q&w}^)|GzIWYiid7Os;e&c|S0021Hpc;-(_JQ$k%?8S{NjGE?4vhv-m3>KF zN^!H(LgN~BdpH3&SU2LV#(JrJCh=492rRA)R4QF4TnFHTBr+cdFOD;X2SA)tO+MKJ-h)(WSgddALj9Rs z-r^OOclNp;N79u45|GT7;fHmX1;u;Z;IAVdwib6v`m-TR(``c1prB1SiAZ`Q+uizQ z4IsNJ*h^`1#ce?1){frNbazlT3O0L&Qzh@+>IJ3o>IH2@yP45{`6r7kv_?{?8B$U# z>CJ%&!6PGj$VB-L8uTYg1#C;j?F;&vu22Bb8Rjo2`@E=@q>Nv3QTvZ>%*;?h$}e7R ztfmPH3~;#Yx4*q@g*o8HHB4e>l>ZP!eb=-4G^OB{(IEaUX}CPbw(Wih`)YKCHavt}Pn-)Q*+o_BW4lCG(6OuTo)Qwmbd7iQdhj-`_XGD%p1mVVKlUVE}J$Njg;MrNSDCw;Ar(IvUx_9%y3wXC}rr7W)> z3-p+OPG^Th=XsvF-pF1f*qMNt>>ksS21I;M;RnfVId*p2`t3_O{I3Cwkayr^b{8j0 k@oD3N_FbPjXUD0?U)6qUaf3D^^8f$<07*qoM6N<$f*Y*JZU6uP literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..759fabdcc59fb36b6e4155883cc43ad980407d7f GIT binary patch literal 578 zcmV-I0=@l-P)iJ()&Z3{xkB8(I0J*oa^A`Zv5dh1cn$fzs%B-us+SdF=GVF0_%j4th zN=AYJ07S^d`~U#(BLL$;G0xf7@qc@)002LrpRU5b`>?C&tEkQZ08B6?n0R)X#m2`J z79s!ujYL0%Pf%LCy4$$6&xC`K+S}a1zwZD5FL82q001aEJVpQjR9RYI001_^ueh85 z00AyZL_t(IjlIy@QiCuM2H-4bgg_Bb2vW6HvBd-0qF|x4_Whs9P3$;BK<;!eAT$3r zo81ud-{l_-#>Xel=}>;A1;)ki@p%|&4$tFpPkecmMB252r^(Iqp_|{{CE@)H57J!M z(;o1cgiq}M?3#MUbMK(tA&DeHW-qp@zp{H4OAtU9DWu4A^KHV$cn3=WRirWCmTZg< zuvA#C1P~ywO+1ckN&yaqAuG*MNa^YliD^w?t%zGxfq}q8AUDm1cW+Z!+ zjM_u`My`-p&k$I6zw!yt2ev5!0M7$Z1yJNRf^5ft%3|<{09*vqngiA9u2I-7ZkW>V z4x1GbtDNH}-dTKxbtm3e+yDpSEB=%WBQgv;G7R=@#0@*3XNMM$2jjn}FZ8t;Uv>7$ QKmY&$07*qoM6N<$g2m5X7>1u&T|0>04INH(6p7pA6!&-j0kOHBVy_D{cUw{JN`X4WKLB$*QIZmxq$AyC zC0u85e!zDH9-j?(1KCYp$&xiQp7)(^-udRU;*}sG4d7ScK`C|fT0Drz`;;sr7Lh9{ z;#NeeKbyEAB42VOUqmDjkw+1c<#H+0=~NbrMZW(kea=Os_M_yFA|i{$B2OYBBBRkr zj*gC`Ua!l^$%&NUQABF$1>{xW7N|v0M5ogMAPhqs$6+>`;ro6zzEY`R+je1&cDs$^ zH~=4ic|ia^0JZsio{KB1}90vY&I*j&1N$rpt@HCQnTFxc%H|6KF|6r z%OZ~BXXmgiizteyR4M?!z1v3s@Co4J;-Wwf6v(yKr4w{I9Yg-0l)C<|01;^!vx&$p z&`gqq!Cxj;CUXsUJu}AYXnlKe}YxW9LFJwBKrM)f!y;v#^W(hPfz&1zY{sEjA^hn0wVGc z@b~(WGO#Smtbk6Zvy=JEcaG!aeehS=G5j4?W?P5v`*gcqdcEG({yenSc%Fyry4bdD zNDcl~N`2lCkfz8+k|cS&f)evk8Zb{Z(0J^z*s3Y1lZ<*CQu--EQ>G< zGsEq=*I^juJ@Zj1RaHv;VaUq_H1Yy$+a`)40D>UcIRAU#+qTAS9(WIMe}9i{+k|0A z9LEGfP;l>aux%UHb#YzSG>vx^(12BD>-YQ5l54H$cDo!OAFrD%k3W@C58FC6j-iN% zd5_~5&-1>!@?26`$5&Q{`>d3@EBn4wK$0Xo@401J*@;m~?RCKwOqj1ydcEGxhO;co zynfw>d_w?f?w?F1ne(56xdT!R_Z5(ZOGKK$RgxsxzHhtL(n12QHIC!t$J{8Tn)?dK zU@FoOXR%4)fFd`s_+7eS@uE#Ta^UTd9C!}*&2A$?$#SniZk*Doc{U{-&6 zdb+;);SW!~19?+qRma!sbvZmdl;v`11k_%e{~geR)x`nex~|D+H)YxDXA7vo|Cdj$ z02#hEIRGLuG=DhL=`?fpn~f?cC4Cj#lN12s=&2U>dhkXA0Fd-S9iqgkpKVy M07*qoM6N<$f+g?BZ2$lO literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4e6e984d3eccba11e21690bf5251a44577fc1cec GIT binary patch literal 349 zcmV-j0iyniP)KXDjDdxQdw_zow6$$= zb-KH|z`?;}X>G{J$X#P+(bCY{+SpZFU*qE8OHx)YH$Fj0PBJ?{hcTd80002ENkl<3vZrS!)IST{6hH00000NkvXXu0mjf{;QWl literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2423469ef53c98b7a8c2363b5f31774bb4cf0ca9 GIT binary patch literal 218 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyasfUeuI>ds|DU|Tg&6!@LxC!} zN`m}?8UD`*KVaZ`sYqw8`j=He(L_%d#}JF&xBcE?4GJ8to&7KWna3!yESI=`#C*4% zsNR9{Och3_w}Fq#TGb{n?aQ)N^SygY%@@cNEl_-^iA;yXL+ivLxkLo>)cx rUY%*KiHeDet*ys&Yc_BAVak44lKG&)wV4_~%NRUe{an^LB{Ts5>mgda literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f8f45fb6ea3d351d962359774c45911853552d6d GIT binary patch literal 29204 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 IM6N<$f|IpWF#rGn literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d7e99b9347da980ecaa45fe63177013262967b58 GIT binary patch literal 538 zcmV+#0_FXQP)ln$)p-Q3*7!M~i9 zkj%=+)zi|xy0^Epu&t=1ySKE+#lz9h&V~HMI{*Lygh@m}R49>cQ`@41AP7uBSuL%$ z`~Lrj9>8kbI=oa)z|0_3bw(2+AyW2r^Z1_yf<3_XXw}6BbU)cE@WlG$qECsUJX3 zJTeZ4lxT6|{0-OybQU;7?zaQDE~OQ*sB}idmi!Qe4XtUVqH-8U5T|U`+H4(G(Lh=_ zeG`Uh8ph}w^}3NxKouL1z`4)1^~-RzUUVQ6G1!mDHmk?soKqGZ>J@ve#GF%ZmV);o zYWbp5K+c7(E7Pn2fO=xL>Zr0fH6l_^pjtrb+GS^@W { 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`) } } } @@ -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) { 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/components/browser.ts b/src/components/browser.ts index 26ed08b..bf51378 100644 --- a/src/components/browser.ts +++ b/src/components/browser.ts @@ -13,6 +13,9 @@ export function renderHeader(currentHandle: string, isLoggedIn: boolean, userHan ` + // Use logged-in user's handle for input if available + const inputHandle = isLoggedIn && userHandle ? userHandle : currentHandle + return `
@@ -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}" > ${loginBtn} diff --git a/src/components/posts.ts b/src/components/posts.ts index 230c4c5..15e4f83 100644 --- a/src/components/posts.ts +++ b/src/components/posts.ts @@ -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 = '

No posts yet

' 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 `
  • - + ${escapeHtml(post.title)} 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' } diff --git a/src/lib/router.ts b/src/lib/router.ts index 40c6d08..4412dcb 100644 --- a/src/lib/router.ts +++ b/src/lib/router.ts @@ -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': diff --git a/src/main.ts b/src/main.ts index 65ed7ff..4dc70f3 100644 --- a/src/main.ts +++ b/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 = ` Blog - + ` 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 { 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 { } 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 = `

    Failed to load: ${err}

    ` + } + 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 = '

    Post not found

    ' + } + } catch (err) { + console.error(err) + contentEl.innerHTML = `

    Failed to load: ${err}

    ` + } + 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 = `

    Failed to load: ${err}

    `