Personal blog finxol.io
blog

feat: first draft at bsky replies

finxol.io c7f79e94 f74ce165

verified
+112 -6
+5 -4
.zed/settings.json
··· 2 2 "language_servers": ["deno", "..."], 3 3 "languages": { 4 4 "Vue.js": { 5 - "language_servers": ["deno", "..."] 5 + "formatter": { "language_server": { "name": "biome" } }, 6 + "language_servers": ["!deno", "biome", "..."] 6 7 }, 7 8 8 9 "JavaScript": { 9 10 "formatter": { "language_server": { "name": "biome" } }, 10 - "language_servers": ["deno", "..."] 11 + "language_servers": ["!deno", "..."] 11 12 }, 12 13 13 14 "TypeScript": { 14 15 "formatter": { "language_server": { "name": "biome" } }, 15 - "language_servers": ["deno", "..."] 16 + "language_servers": ["!deno", "..."] 16 17 }, 17 18 18 19 "TSX": { 19 20 "formatter": { "language_server": { "name": "biome" } }, 20 - "language_servers": ["deno", "..."] 21 + "language_servers": ["!deno", "..."] 21 22 }, 22 23 "JSON": { "formatter": { "language_server": { "name": "biome" } } }, 23 24 "JSONC": { "formatter": { "language_server": { "name": "biome" } } },
+52
app/components/BskyComments.vue
··· 1 + <script setup lang="ts"> 2 + import { getBskyReplies, type ReplyThread } from "~/util/atproto"; 3 + 4 + const props = defineProps({ 5 + cid: { 6 + type: String, 7 + required: true 8 + } 9 + }); 10 + const { cid } = toRefs(props); 11 + 12 + const data = ref(await getBskyReplies(cid.value)); 13 + const err = ref(""); 14 + const post = ref(); 15 + 16 + if (data.value.$type === "app.bsky.feed.defs#blockedPost") { 17 + err.value = "Post is blocked"; 18 + } 19 + 20 + if (data.value.$type === "app.bsky.feed.defs#notFoundPost") { 21 + err.value = "Post not found"; 22 + } 23 + 24 + if (data.value.$type === "app.bsky.feed.defs#threadViewPost") { 25 + post.value = data.value; 26 + } 27 + </script> 28 + 29 + <template> 30 + <h3>Join the conversation!</h3> 31 + 32 + <div v-if="err"> 33 + <div>{{ err }}</div> 34 + </div> 35 + 36 + <div v-if="post"> 37 + <div v-if="post.post.replyCount === 0"> 38 + <div>No replies yet!</div> 39 + </div> 40 + 41 + <div v-else> 42 + <p>{{post.post.replyCount}} replies</p> 43 + 44 + <div v-for="reply in post.replies"> 45 + <a :href="`https://bsky.app/profile/${reply.post.author.handle}`" class="text-blue-500 hover:underline"> 46 + {{ reply.post.author.displayName }} 47 + </a> 48 + <div>{{ reply.post.record.text }}</div> 49 + </div> 50 + </div> 51 + </div> 52 + </template>
+4
app/pages/posts/[...slug].vue
··· 91 91 92 92 <ShareActions :title="post.title" :description="post.description" :author="post.authors[0]?.name" /> 93 93 94 + <Suspense> 95 + <BskyComments v-if="post.bskyCid" :cid="post.bskyCid" /> 96 + </Suspense> 97 + 94 98 </article> 95 99 96 100 <div v-else class="flex items-center justify-center">
+39
app/util/atproto.ts
··· 1 + import { Client, simpleFetchHandler } from "@atcute/client"; 2 + import type { AppBskyFeedDefs } from "@atcute/bluesky"; 3 + import type { ResourceUri } from "@atcute/lexicons"; 4 + 5 + import config from "@/../blog.config"; 6 + 7 + const handler = simpleFetchHandler({ 8 + service: "https://public.api.bsky.app" 9 + }); 10 + const rpc = new Client({ handler }); 11 + 12 + export type ReplyThread = 13 + | AppBskyFeedDefs.ThreadViewPost 14 + | AppBskyFeedDefs.BlockedPost 15 + | AppBskyFeedDefs.NotFoundPost; 16 + 17 + export async function getBskyReplies(cid: string) { 18 + // uri should be in format: at://did:plc:xxx/app.bsky.feed.post/xxxxx 19 + const uri: ResourceUri = `at://${config.authorDid}/app.bsky.feed.post/${cid}`; 20 + 21 + const { ok, data } = await rpc.get("app.bsky.feed.getPostThread", { 22 + params: { 23 + uri, 24 + depth: 10 25 + } 26 + }); 27 + 28 + if (!ok) { 29 + console.error(data); 30 + console.error("Error fetching thread:", data.error); 31 + return { $type: "app.bsky.feed.defs#notFoundPost" }; 32 + } 33 + 34 + if (ok) { 35 + return data.thread; 36 + } 37 + 38 + return { $type: "app.bsky.feed.defs#notFoundPost" }; 39 + }
+1
blog.config.ts
··· 4 4 site: "https://finxol.io", 5 5 title: "finxol's blog", 6 6 author: "finxol", 7 + authorDid: "did:plc:hpmpe3pzpdtxbmvhlwrevhju", 7 8 meta: [ 8 9 { 9 10 name: "description",
+1 -1
content/posts/embracing-atproto-pt-2-tangled-knot.md
··· 10 10 - atproto 11 11 - self-hosting 12 12 published: true 13 - bskyCid: bafyreid4opjtllapzeyjgrsqcfrzyz2t6wjmxulmkhuh2wc6cyg5bre2su 13 + bskyCid: 3lyzhrumfu22n 14 14 --- 15 15 16 16 I recently set up my own atproto PDS, for use with Bluesky and all other atproto apps.
+2
globals.ts
··· 10 10 sharingProviders: SharingProvider[]; 11 11 title: string; 12 12 author: string; 13 + authorDid: `did:plc:${string}`; 13 14 meta: { 14 15 name: string; 15 16 content: string; ··· 36 37 : { bluesky: true, clipboard: true, native: true }, 37 38 title: config.title ?? "My Blog", 38 39 author: config.author ?? "finxol", 40 + authorDid: config.authorDid, 39 41 meta: config.meta ?? [ 40 42 { name: "description", content: "My blog description" } 41 43 ],
+1
package.json
··· 14 14 "dependencies": { 15 15 "@atcute/bluesky": "^3.2.10", 16 16 "@atcute/client": "^4.0.5", 17 + "@atcute/lexicons": "^1.2.4", 17 18 "@nuxt/content": "^3.8.0", 18 19 "@nuxt/icon": "1.11.0", 19 20 "@nuxtjs/tailwindcss": "^6.14.0",
+3
pnpm-lock.yaml
··· 14 14 '@atcute/client': 15 15 specifier: ^4.0.5 16 16 version: 4.0.5 17 + '@atcute/lexicons': 18 + specifier: ^1.2.4 19 + version: 1.2.4 17 20 '@nuxt/content': 18 21 specifier: ^3.8.0 19 22 version: 3.8.0(@libsql/client@0.15.15)(better-sqlite3@12.4.1)(magicast@0.5.1)
+4 -1
tsconfig.json
··· 1 1 { 2 2 // https://nuxt.com/docs/guide/concepts/typescript 3 - "extends": "./.nuxt/tsconfig.json" 3 + "extends": "./.nuxt/tsconfig.json", 4 + "compilerOptions": { 5 + "types": ["@atcute/bluesky"] 6 + } 4 7 }