Skip to content

Commit d4a9466

Browse files
feat: add blog post composables
These composables will be used in this PR #257. Co-authored-by: Brandon Hurrington <brandon.o.hurrington@gmail.com>
1 parent ea25ecb commit d4a9466

File tree

3 files changed

+174
-0
lines changed

3 files changed

+174
-0
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { Author, ResolvedAuthor } from '#shared/schemas/blog'
2+
3+
/**
4+
* @public
5+
*/
6+
export function useAuthorProfiles(authors: Author[]) {
7+
const authorsJson = JSON.stringify(authors)
8+
9+
const { data } = useFetch('/api/atproto/author-profiles', {
10+
query: {
11+
authors: authorsJson,
12+
},
13+
})
14+
15+
const resolvedAuthors = computed<ResolvedAuthor[]>(
16+
() =>
17+
data.value?.authors ??
18+
authors.map(author => ({
19+
...author,
20+
avatar: null,
21+
profileUrl: author.blueskyHandle
22+
? `https://bsky.app/profile/${author.blueskyHandle}`
23+
: null,
24+
})),
25+
)
26+
27+
return {
28+
resolvedAuthors,
29+
}
30+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { Constellation } from '#shared/utils/constellation'
2+
import { NPMX_SITE } from '#shared/utils/constants'
3+
4+
const BLOG_BACKLINK_TTL_IN_SECONDS = 60 * 5
5+
6+
// TODO: Remove did when going live
7+
const TESTING_ROE_DID = 'did:plc:jbeaa5kdaladzwq3r7f5xgwe'
8+
// const TESTING_BACKLINK_URL = 'https://roe.dev/blog/the-golden-thread'
9+
// const NPMX_DID = 'did:plc:u5zp7npt5kpueado77kuihyz'
10+
11+
/**
12+
* @public
13+
*/
14+
export interface BlogPostBlueskyLink {
15+
did: string
16+
rkey: string
17+
postUri: string
18+
}
19+
20+
/**
21+
* @public
22+
*/
23+
export function useBlogPostBlueskyLink(slug: MaybeRefOrGetter<string | null | undefined>) {
24+
const cachedFetch = useCachedFetch()
25+
26+
const blogUrl = computed(() => {
27+
const s = toValue(slug)
28+
if (!s) return null
29+
return `${NPMX_SITE}/blog/${s}`
30+
// return TESTING_BACKLINK_URL
31+
})
32+
33+
return useAsyncData<BlogPostBlueskyLink | null>(
34+
() => (blogUrl.value ? `blog-bsky-link:${blogUrl.value}` : 'blog-bsky-link:none'),
35+
async () => {
36+
const url = blogUrl.value
37+
if (!url) return null
38+
39+
const constellation = new Constellation(cachedFetch)
40+
41+
try {
42+
// Try embed.external.uri first (link card embeds)
43+
const { data: embedBacklinks } = await constellation.getBackLinks(
44+
url,
45+
'app.bsky.feed.post',
46+
'embed.external.uri',
47+
1,
48+
undefined,
49+
true,
50+
[[TESTING_ROE_DID]],
51+
BLOG_BACKLINK_TTL_IN_SECONDS,
52+
)
53+
54+
const embedRecord = embedBacklinks.records[0]
55+
if (embedRecord) {
56+
return {
57+
did: embedRecord.did,
58+
rkey: embedRecord.rkey,
59+
postUri: `at://${embedRecord.did}/app.bsky.feed.post/${embedRecord.rkey}`,
60+
}
61+
}
62+
63+
// Try facets.features.uri (URLs in post text)
64+
const { data: facetBacklinks } = await constellation.getBackLinks(
65+
url,
66+
'app.bsky.feed.post',
67+
'facets[].features[app.bsky.richtext.facet#link].uri',
68+
1,
69+
undefined,
70+
true,
71+
[[TESTING_ROE_DID]],
72+
BLOG_BACKLINK_TTL_IN_SECONDS,
73+
)
74+
75+
const facetRecord = facetBacklinks.records[0]
76+
if (facetRecord) {
77+
return {
78+
did: facetRecord.did,
79+
rkey: facetRecord.rkey,
80+
postUri: `at://${facetRecord.did}/app.bsky.feed.post/${facetRecord.rkey}`,
81+
}
82+
}
83+
} catch (error: unknown) {
84+
// Constellation unavailable or error - fail silently
85+
// But during dev we will get an error
86+
if (import.meta.dev) console.error('[Bluesky] Constellation error:', error)
87+
}
88+
89+
return null
90+
},
91+
)
92+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { Comment } from '#shared/types/blog-post'
2+
import { BLUESKY_COMMENTS_REQUEST } from '#shared/utils/constants'
3+
4+
/**
5+
* @public
6+
*/
7+
export type BlueskyCommentsState = {
8+
thread: Comment | null
9+
likes: Array<{
10+
actor: {
11+
did: string
12+
handle: string
13+
displayName?: string
14+
avatar?: string
15+
}
16+
}>
17+
totalLikes: number
18+
postUrl: string | null
19+
_empty?: boolean
20+
_error?: boolean
21+
}
22+
23+
// Handles both server-side caching and client-side hydration
24+
/**
25+
* @public
26+
*/
27+
export function useBlueskyComments(postUri: MaybeRefOrGetter<string>) {
28+
const uri = toRef(postUri)
29+
30+
const { data, pending, error, refresh } = useFetch(BLUESKY_COMMENTS_REQUEST, {
31+
query: { uri },
32+
key: () => `bsky-comments-${uri.value}`,
33+
default: (): BlueskyCommentsState => ({
34+
thread: null,
35+
likes: [],
36+
totalLikes: 0,
37+
postUrl: null,
38+
}),
39+
})
40+
41+
// Hydrate with fresh data on client side
42+
onMounted(() => {
43+
refresh()
44+
})
45+
46+
return {
47+
data,
48+
pending,
49+
error,
50+
refresh,
51+
}
52+
}

0 commit comments

Comments
 (0)