diff --git a/quartz.layout.ts b/quartz.layout.ts
index 13a1e191f..ca99c0ccc 100644
--- a/quartz.layout.ts
+++ b/quartz.layout.ts
@@ -5,9 +5,7 @@ import * as Component from "./quartz/components"
export const sharedPageComponents: SharedLayout = {
head: Component.Head(),
header: [],
- afterBody: [
- Component.BlueskyComments(),
- ],
+ afterBody: [Component.BlueskyComments()],
footer: Component.Footer({
links: {
GitHub: "https://github.com/jackyzha0/quartz",
diff --git a/quartz/components/BlueskyComments.tsx b/quartz/components/BlueskyComments.tsx
index ce6a3b989..1b2f64756 100644
--- a/quartz/components/BlueskyComments.tsx
+++ b/quartz/components/BlueskyComments.tsx
@@ -1,4 +1,3 @@
-
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { classNames } from "../util/lang"
// @ts-ignore
@@ -6,22 +5,30 @@ import script from "./scripts/bluesky.inline"
import style from "./styles/bluesky.scss"
const BlueskyComments: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => {
- const blueskyUrl = fileData.frontmatter?.blueskyUrl as string | undefined
- if (!blueskyUrl) {
- return null
- }
+ const blueskyUrl = fileData.frontmatter?.blueskyUrl as string | undefined
+ if (!blueskyUrl) {
+ return null
+ }
- return (
-
- )
+ return (
+
+ )
}
BlueskyComments.afterDOMLoaded = script
diff --git a/quartz/components/scripts/bluesky.inline.ts b/quartz/components/scripts/bluesky.inline.ts
index e3f1218e9..e96dd87f7 100644
--- a/quartz/components/scripts/bluesky.inline.ts
+++ b/quartz/components/scripts/bluesky.inline.ts
@@ -1,199 +1,196 @@
-
document.addEventListener("nav", async () => {
- const container = document.getElementById("bluesky-comments")
- if (!container) return
+ const container = document.getElementById("bluesky-comments")
+ if (!container) return
- const url = container.getAttribute("data-url")
- if (!url) return
+ const url = container.getAttribute("data-url")
+ if (!url) return
- try {
- const { handle, postId } = parseBlueskyUrl(url)
- const did = await resolveHandle(handle)
- const replies = await fetchThread(did, postId)
- renderComments(replies)
- } catch (e) {
- console.error("Error loading Bluesky comments:", e)
- const list = document.getElementById("bluesky-comments-list")
- if (list) {
- list.textContent = "Error loading comments."
- }
+ try {
+ const { handle, postId } = parseBlueskyUrl(url)
+ const did = await resolveHandle(handle)
+ const replies = await fetchThread(did, postId)
+ renderComments(replies)
+ } catch (e) {
+ console.error("Error loading Bluesky comments:", e)
+ const list = document.getElementById("bluesky-comments-list")
+ if (list) {
+ list.textContent = "Error loading comments."
}
+ }
})
function parseBlueskyUrl(url: string) {
- const urlParts = new URL(url).pathname.split("/")
- const handle = urlParts[2]
- const postId = urlParts[4]
+ const urlParts = new URL(url).pathname.split("/")
+ const handle = urlParts[2]
+ const postId = urlParts[4]
- if (!handle || !postId) throw new Error("Invalid Bluesky URL")
- return { handle, postId }
+ if (!handle || !postId) throw new Error("Invalid Bluesky URL")
+ return { handle, postId }
}
async function resolveHandle(handle: string) {
- const resolveRes = await fetch(
- `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${handle}`
- )
- if (!resolveRes.ok) throw new Error("Could not resolve handle")
- const { did } = await resolveRes.json()
- return did
+ const resolveRes = await fetch(
+ `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${handle}`,
+ )
+ if (!resolveRes.ok) throw new Error("Could not resolve handle")
+ const { did } = await resolveRes.json()
+ return did
}
async function fetchThread(did: string, postId: string) {
- const atUri = `at://${did}/app.bsky.feed.post/${postId}`
- const threadRes = await fetch(
- `https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=${atUri}&depth=10&parentHeight=0`
- )
+ const atUri = `at://${did}/app.bsky.feed.post/${postId}`
+ const threadRes = await fetch(
+ `https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=${atUri}&depth=10&parentHeight=0`,
+ )
- if (!threadRes.ok) throw new Error("Could not fetch thread")
- const data = await threadRes.json()
- return data.thread.replies
+ if (!threadRes.ok) throw new Error("Could not fetch thread")
+ const data = await threadRes.json()
+ return data.thread.replies
}
function renderComments(replies: any[]) {
- const commentsList = document.getElementById("bluesky-comments-list")
- if (!commentsList) return
+ const commentsList = document.getElementById("bluesky-comments-list")
+ if (!commentsList) return
- if (replies && replies.length > 0) {
- commentsList.innerHTML = "" // Clear loading text
- // Sort by likes
- replies.sort((a: any, b: any) =>
- (b.post.likeCount || 0) - (a.post.likeCount || 0)
- )
+ if (replies && replies.length > 0) {
+ commentsList.innerHTML = "" // Clear loading text
+ // Sort by likes
+ replies.sort((a: any, b: any) => (b.post.likeCount || 0) - (a.post.likeCount || 0))
- replies.forEach((reply: any) => {
- if (!reply.post) return
- commentsList.appendChild(createCommentNode(reply))
- })
- } else {
- commentsList.textContent = "No comments yet."
- }
+ replies.forEach((reply: any) => {
+ if (!reply.post) return
+ commentsList.appendChild(createCommentNode(reply))
+ })
+ } else {
+ commentsList.textContent = "No comments yet."
+ }
}
function createCommentNode(reply: any): HTMLElement {
- const post = reply.post
- const author = post.author
- const date = new Date(post.indexedAt).toLocaleDateString()
+ const post = reply.post
+ const author = post.author
+ const date = new Date(post.indexedAt).toLocaleDateString()
- const div = document.createElement("div")
- div.className = "comment"
+ const div = document.createElement("div")
+ div.className = "comment"
- // Header
- const header = document.createElement("div")
- header.className = "comment-header"
+ // Header
+ const header = document.createElement("div")
+ header.className = "comment-header"
- if (author.avatar) {
- const avatar = document.createElement("img")
- avatar.src = author.avatar
- avatar.alt = author.handle
- header.appendChild(avatar)
- }
+ if (author.avatar) {
+ const avatar = document.createElement("img")
+ avatar.src = author.avatar
+ avatar.alt = author.handle
+ header.appendChild(avatar)
+ }
- const handleLink = document.createElement("a")
- const postId = post.uri.split("/").pop()
- handleLink.href = `https://bsky.app/profile/${author.did}/post/${postId}`
- handleLink.target = "_blank"
- handleLink.className = "handle"
- handleLink.textContent = author.displayName || author.handle
- header.appendChild(handleLink)
+ const handleLink = document.createElement("a")
+ const postId = post.uri.split("/").pop()
+ handleLink.href = `https://bsky.app/profile/${author.did}/post/${postId}`
+ handleLink.target = "_blank"
+ handleLink.className = "handle"
+ handleLink.textContent = author.displayName || author.handle
+ header.appendChild(handleLink)
- const timeSpan = document.createElement("span")
- timeSpan.className = "time"
- timeSpan.textContent = date
- header.appendChild(timeSpan)
+ const timeSpan = document.createElement("span")
+ timeSpan.className = "time"
+ timeSpan.textContent = date
+ header.appendChild(timeSpan)
- div.appendChild(header)
+ div.appendChild(header)
- // Body
- div.appendChild(renderPostBody(post))
+ // Body
+ div.appendChild(renderPostBody(post))
- // Actions
- const actions = document.createElement("div")
- actions.className = "comment-actions"
+ // Actions
+ const actions = document.createElement("div")
+ actions.className = "comment-actions"
- const likes = document.createElement("span")
- likes.textContent = `❤️ ${post.likeCount || 0}`
- actions.appendChild(likes)
+ const likes = document.createElement("span")
+ likes.textContent = `❤️ ${post.likeCount || 0}`
+ actions.appendChild(likes)
- const reposts = document.createElement("span")
- reposts.textContent = `🔁 ${post.repostCount || 0}`
- actions.appendChild(reposts)
+ const reposts = document.createElement("span")
+ reposts.textContent = `🔁 ${post.repostCount || 0}`
+ actions.appendChild(reposts)
- div.appendChild(actions)
+ div.appendChild(actions)
- // Recursive rendering for nested replies
- if (reply.replies && reply.replies.length > 0) {
- const repliesContainer = document.createElement("div")
- repliesContainer.className = "replies"
- reply.replies.forEach((child: any) => {
- if (!child.post) return
- repliesContainer.appendChild(createCommentNode(child))
- })
- div.appendChild(repliesContainer)
- }
+ // Recursive rendering for nested replies
+ if (reply.replies && reply.replies.length > 0) {
+ const repliesContainer = document.createElement("div")
+ repliesContainer.className = "replies"
+ reply.replies.forEach((child: any) => {
+ if (!child.post) return
+ repliesContainer.appendChild(createCommentNode(child))
+ })
+ div.appendChild(repliesContainer)
+ }
- return div
+ return div
}
function renderPostBody(post: any): HTMLElement {
- const text = post.record.text
- const facets = post.record.facets
- const body = document.createElement("div")
- body.className = "comment-body"
-
- if (!facets || facets.length === 0) {
- body.textContent = text
- return body
- }
-
- facets.sort((a: any, b: any) => a.index.byteStart - b.index.byteStart)
-
- const bytes = new TextEncoder().encode(text)
- let lastByteIndex = 0
-
- for (const facet of facets) {
- const { byteStart, byteEnd } = facet.index
-
- if (byteStart > lastByteIndex) {
- const slice = bytes.slice(lastByteIndex, byteStart)
- const textNode = document.createTextNode(new TextDecoder().decode(slice))
- body.appendChild(textNode)
- }
-
- const slice = bytes.slice(byteStart, byteEnd)
- const facetText = new TextDecoder().decode(slice)
- const feature = facet.features[0]
-
- if (feature["$type"] === "app.bsky.richtext.facet#link") {
- const link = document.createElement("a")
- link.href = feature.uri
- link.target = "_blank"
- link.rel = "noopener noreferrer"
- link.textContent = facetText
- body.appendChild(link)
- } else if (feature["$type"] === "app.bsky.richtext.facet#mention") {
- const link = document.createElement("a")
- link.href = `https://bsky.app/profile/${feature.did}`
- link.target = "_blank"
- link.rel = "noopener noreferrer"
- link.textContent = facetText
- body.appendChild(link)
- } else if (feature["$type"] === "app.bsky.richtext.facet#tag") {
- const link = document.createElement("a")
- link.href = `https://bsky.app/hashtag/${feature.tag}`
- link.target = "_blank"
- link.rel = "noopener noreferrer"
- link.textContent = facetText
- body.appendChild(link)
- } else {
- body.appendChild(document.createTextNode(facetText))
- }
-
- lastByteIndex = byteEnd
- }
-
- if (lastByteIndex < bytes.length) {
- const slice = bytes.slice(lastByteIndex)
- body.appendChild(document.createTextNode(new TextDecoder().decode(slice)))
- }
+ const text = post.record.text
+ const facets = post.record.facets
+ const body = document.createElement("div")
+ body.className = "comment-body"
+ if (!facets || facets.length === 0) {
+ body.textContent = text
return body
+ }
+
+ facets.sort((a: any, b: any) => a.index.byteStart - b.index.byteStart)
+
+ const bytes = new TextEncoder().encode(text)
+ let lastByteIndex = 0
+
+ for (const facet of facets) {
+ const { byteStart, byteEnd } = facet.index
+
+ if (byteStart > lastByteIndex) {
+ const slice = bytes.slice(lastByteIndex, byteStart)
+ const textNode = document.createTextNode(new TextDecoder().decode(slice))
+ body.appendChild(textNode)
+ }
+
+ const slice = bytes.slice(byteStart, byteEnd)
+ const facetText = new TextDecoder().decode(slice)
+ const feature = facet.features[0]
+
+ if (feature["$type"] === "app.bsky.richtext.facet#link") {
+ const link = document.createElement("a")
+ link.href = feature.uri
+ link.target = "_blank"
+ link.rel = "noopener noreferrer"
+ link.textContent = facetText
+ body.appendChild(link)
+ } else if (feature["$type"] === "app.bsky.richtext.facet#mention") {
+ const link = document.createElement("a")
+ link.href = `https://bsky.app/profile/${feature.did}`
+ link.target = "_blank"
+ link.rel = "noopener noreferrer"
+ link.textContent = facetText
+ body.appendChild(link)
+ } else if (feature["$type"] === "app.bsky.richtext.facet#tag") {
+ const link = document.createElement("a")
+ link.href = `https://bsky.app/hashtag/${feature.tag}`
+ link.target = "_blank"
+ link.rel = "noopener noreferrer"
+ link.textContent = facetText
+ body.appendChild(link)
+ } else {
+ body.appendChild(document.createTextNode(facetText))
+ }
+
+ lastByteIndex = byteEnd
+ }
+
+ if (lastByteIndex < bytes.length) {
+ const slice = bytes.slice(lastByteIndex)
+ body.appendChild(document.createTextNode(new TextDecoder().decode(slice)))
+ }
+
+ return body
}
diff --git a/quartz/components/styles/bluesky.scss b/quartz/components/styles/bluesky.scss
index e587761d2..ad891d490 100644
--- a/quartz/components/styles/bluesky.scss
+++ b/quartz/components/styles/bluesky.scss
@@ -1,77 +1,77 @@
.bluesky-comments-container {
- margin-top: 4rem;
- padding-top: 2rem;
- border-top: 1px solid var(--lightgray);
+ margin-top: 4rem;
+ padding-top: 2rem;
+ border-top: 1px solid var(--lightgray);
- h2 {
- margin-bottom: 1rem;
+ h2 {
+ margin-bottom: 1rem;
+ }
+
+ .bluesky-meta {
+ margin-bottom: 1.5rem;
+ color: var(--gray);
+ font-size: 0.9rem;
+
+ a {
+ color: var(--secondary);
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
}
+ }
- .bluesky-meta {
- margin-bottom: 1.5rem;
- color: var(--gray);
+ .comment {
+ margin-top: 1rem;
+ padding-left: 1rem;
+ border-left: 2px solid var(--lightgray);
+
+ .comment-header {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: 0.5rem;
+
+ img {
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ }
+
+ .handle {
+ font-weight: bold;
+ color: var(--dark);
font-size: 0.9rem;
+ }
- a {
- color: var(--secondary);
- text-decoration: none;
-
- &:hover {
- text-decoration: underline;
- }
- }
+ .time {
+ color: var(--gray);
+ font-size: 0.8rem;
+ }
}
- .comment {
- margin-top: 1rem;
- padding-left: 1rem;
- border-left: 2px solid var(--lightgray);
+ .comment-body {
+ font-size: 0.95rem;
+ color: var(--darkgray);
+ white-space: pre-wrap;
- .comment-header {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- margin-bottom: 0.5rem;
+ a {
+ color: var(--secondary);
+ text-decoration: none;
- img {
- width: 24px;
- height: 24px;
- border-radius: 50%;
- }
-
- .handle {
- font-weight: bold;
- color: var(--dark);
- font-size: 0.9rem;
- }
-
- .time {
- color: var(--gray);
- font-size: 0.8rem;
- }
- }
-
- .comment-body {
- font-size: 0.95rem;
- color: var(--darkgray);
- white-space: pre-wrap;
-
- a {
- color: var(--secondary);
- text-decoration: none;
-
- &:hover {
- text-decoration: underline;
- }
- }
- }
-
- .comment-actions {
- margin-top: 0.25rem;
- font-size: 0.8rem;
- color: var(--gray);
- display: flex;
- gap: 1rem;
+ &:hover {
+ text-decoration: underline;
}
+ }
}
-}
\ No newline at end of file
+
+ .comment-actions {
+ margin-top: 0.25rem;
+ font-size: 0.8rem;
+ color: var(--gray);
+ display: flex;
+ gap: 1rem;
+ }
+ }
+}