an app to share curated trails sidetrail.app
1

Configure Feed

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

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}