an app to share curated trails sidetrail.app
atproto nextjs react rsc
50
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 104 lines 3.1 kB view raw
1"use server"; 2 3import "server-only"; 4import { cacheLife, cacheTag } from "next/cache"; 5import { Client, type l } from "@atproto/lex"; 6import * as getPostThread from "@/lib/lexicons/app/bsky/feed/getPostThread"; 7import * as feedDefs from "@/lib/lexicons/app/bsky/feed/defs"; 8import { loadTrailCardByUri } from "@/data/queries"; 9import { TrailCard } from "./TrailCard"; 10import { BlueskyPostEmbed } from "./at/(trail)/[handle]/trail/[rkey]/embeds/BlueskyPostEmbed"; 11import { LinkPreview } from "./at/(trail)/[handle]/trail/[rkey]/LinkPreview"; 12import { blueskyUrlToAtUri } from "./at/(trail)/[handle]/trail/[rkey]/utils/bluesky"; 13 14type BlueskyPost = feedDefs.ThreadViewPost; 15 16type LinkMetadata = { 17 uri: string; 18 title: string; 19 description: string; 20 thumb?: string; 21}; 22 23function isTrailUri(uri: string): boolean { 24 return uri.includes("/app.sidetrail.trail/"); 25} 26 27function isBlueskyPostUri(uri: string): boolean { 28 return ( 29 uri.includes("/app.bsky.feed.post/") || (uri.includes("bsky.app") && uri.includes("/post/")) 30 ); 31} 32 33// Cached fetch for Bluesky posts - throws on failure 34async function fetchBlueskyPost(atUri: string): Promise<BlueskyPost> { 35 "use cache: redis"; 36 cacheTag(`embed:bsky:${atUri}`); 37 cacheLife("days"); 38 39 const atprotoClient = new Client("https://public.api.bsky.app"); 40 const result = await atprotoClient.call(getPostThread.main, { 41 uri: atUri as l.AtUri, 42 depth: 0, 43 parentHeight: 0, 44 }); 45 if (feedDefs.threadViewPost.$check(result.thread)) { 46 // Serialize to strip non-plain objects (like CID) 47 return JSON.parse(JSON.stringify(result.thread)) as BlueskyPost; 48 } 49 throw new Error(`Invalid thread response for ${atUri}`); 50} 51 52// Cached fetch for link metadata - throws on failure 53async function fetchLinkMetadata(url: string): Promise<LinkMetadata> { 54 "use cache: redis"; 55 cacheTag(`embed:link:${url}`); 56 cacheLife("days"); 57 58 const response = await fetch(`https://cardyb.bsky.app/v1/extract?url=${encodeURIComponent(url)}`); 59 if (!response.ok) { 60 throw new Error(`Failed to fetch link metadata: ${response.status}`); 61 } 62 63 const data = await response.json(); 64 return { 65 uri: url, 66 title: data.title || url, 67 description: data.description || "", 68 thumb: data.image || undefined, 69 }; 70} 71 72export async function loadEmbed(uri: string): Promise<React.ReactElement> { 73 if (isTrailUri(uri)) { 74 const trail = await loadTrailCardByUri(uri); 75 if (!trail) throw new Error(`Trail not found: ${uri}`); 76 return <TrailCard {...trail} />; 77 } 78 79 if (isBlueskyPostUri(uri)) { 80 const atUri = uri.startsWith("at://") 81 ? uri 82 : uri.startsWith("http") 83 ? blueskyUrlToAtUri(uri) 84 : null; 85 if (!atUri) throw new Error(`Invalid Bluesky URI: ${uri}`); 86 87 const post = await fetchBlueskyPost(atUri); 88 return <BlueskyPostEmbed post={post} />; 89 } 90 91 // Regular link 92 const metadata = await fetchLinkMetadata(uri); 93 return ( 94 <LinkPreview 95 external={{ 96 uri: metadata.uri, 97 title: metadata.title, 98 description: metadata.description, 99 thumb: metadata.thumb, 100 }} 101 fallbackToUrl 102 /> 103 ); 104}