Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions app/composables/useAuthorProfiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Author, ResolvedAuthor } from '#shared/schemas/blog'

/**
* @public
*/
export function useAuthorProfiles(authors: Author[]) {
const authorsJson = JSON.stringify(authors)

const { data } = useFetch('/api/atproto/author-profiles', {
query: {
authors: authorsJson,
},
})

const resolvedAuthors = computed<ResolvedAuthor[]>(
() =>
data.value?.authors ??
authors.map(author => ({
...author,
avatar: null,
profileUrl: author.blueskyHandle
? `https://bsky.app/profile/${author.blueskyHandle}`
: null,
})),
)

return {
resolvedAuthors,
}
}
92 changes: 92 additions & 0 deletions app/composables/useBlogPostBlueskyLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Constellation } from '#shared/utils/constellation'
import { NPMX_SITE } from '#shared/utils/constants'

const BLOG_BACKLINK_TTL_IN_SECONDS = 60 * 5

// TODO: Remove did when going live
const TESTING_ROE_DID = 'did:plc:jbeaa5kdaladzwq3r7f5xgwe'
// const TESTING_BACKLINK_URL = 'https://roe.dev/blog/the-golden-thread'
// const NPMX_DID = 'did:plc:u5zp7npt5kpueado77kuihyz'

/**
* @public
*/
export interface BlogPostBlueskyLink {
did: string
rkey: string
postUri: string
}

/**
* @public
*/
export function useBlogPostBlueskyLink(slug: MaybeRefOrGetter<string | null | undefined>) {
const cachedFetch = useCachedFetch()

const blogUrl = computed(() => {
const s = toValue(slug)
if (!s) return null
return `${NPMX_SITE}/blog/${s}`
// return TESTING_BACKLINK_URL
})

return useAsyncData<BlogPostBlueskyLink | null>(
() => (blogUrl.value ? `blog-bsky-link:${blogUrl.value}` : 'blog-bsky-link:none'),
async () => {
const url = blogUrl.value
if (!url) return null

const constellation = new Constellation(cachedFetch)

try {
// Try embed.external.uri first (link card embeds)
const { data: embedBacklinks } = await constellation.getBackLinks(
url,
'app.bsky.feed.post',
'embed.external.uri',
1,
undefined,
true,
[[TESTING_ROE_DID]],
BLOG_BACKLINK_TTL_IN_SECONDS,
)

const embedRecord = embedBacklinks.records[0]
if (embedRecord) {
return {
did: embedRecord.did,
rkey: embedRecord.rkey,
postUri: `at://${embedRecord.did}/app.bsky.feed.post/${embedRecord.rkey}`,
}
}

// Try facets.features.uri (URLs in post text)
const { data: facetBacklinks } = await constellation.getBackLinks(
url,
'app.bsky.feed.post',
'facets[].features[app.bsky.richtext.facet#link].uri',
1,
undefined,
true,
[[TESTING_ROE_DID]],
BLOG_BACKLINK_TTL_IN_SECONDS,
)

const facetRecord = facetBacklinks.records[0]
if (facetRecord) {
return {
did: facetRecord.did,
rkey: facetRecord.rkey,
postUri: `at://${facetRecord.did}/app.bsky.feed.post/${facetRecord.rkey}`,
}
}
} catch (error: unknown) {
// Constellation unavailable or error - fail silently
// But during dev we will get an error
if (import.meta.dev) console.error('[Bluesky] Constellation error:', error)
}

return null
},
)
}
52 changes: 52 additions & 0 deletions app/composables/useBlueskyComments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { Comment } from '#shared/types/blog-post'
import { BLUESKY_COMMENTS_REQUEST } from '#shared/utils/constants'

/**
* @public
*/
export type BlueskyCommentsState = {
thread: Comment | null
likes: Array<{
actor: {
did: string
handle: string
displayName?: string
avatar?: string
}
}>
totalLikes: number
postUrl: string | null
_empty?: boolean
_error?: boolean
}

// Handles both server-side caching and client-side hydration
/**
* @public
*/
export function useBlueskyComments(postUri: MaybeRefOrGetter<string>) {
const uri = toRef(postUri)

const { data, pending, error, refresh } = useFetch(BLUESKY_COMMENTS_REQUEST, {
query: { uri },
key: () => `bsky-comments-${uri.value}`,
default: (): BlueskyCommentsState => ({
thread: null,
likes: [],
totalLikes: 0,
postUrl: null,
}),
})

// Hydrate with fresh data on client side
onMounted(() => {
refresh()
})

return {
data,
pending,
error,
refresh,
}
}
Loading