From 320d0047e961a64d34c3c8c774004103fb3688d3 Mon Sep 17 00:00:00 2001 From: eamag Date: Sun, 25 Jan 2026 17:16:13 +0100 Subject: [PATCH 1/2] feat: add Bluesky comments component --- docs/features/comments.md | 29 ++- quartz.layout.ts | 4 +- quartz/components/BlueskyComments.tsx | 30 +++ quartz/components/index.ts | 2 + quartz/components/scripts/bluesky.inline.ts | 199 ++++++++++++++++++++ quartz/components/styles/bluesky.scss | 77 ++++++++ 6 files changed, 338 insertions(+), 3 deletions(-) create mode 100644 quartz/components/BlueskyComments.tsx create mode 100644 quartz/components/scripts/bluesky.inline.ts create mode 100644 quartz/components/styles/bluesky.scss diff --git a/docs/features/comments.md b/docs/features/comments.md index 6e5a25ca1..b73ac4646 100644 --- a/docs/features/comments.md +++ b/docs/features/comments.md @@ -8,7 +8,7 @@ Quartz also has the ability to hook into various providers to enable readers to ![[giscus-example.png]] -As of today, only [Giscus](https://giscus.app/) is supported out of the box but PRs to support other providers are welcome! +As of today, [Giscus](https://giscus.app/) and [Bluesky](https://bsky.app/) are supported out of the box, and PRs to support other providers are welcome! ## Providers @@ -52,6 +52,31 @@ afterBody: [ ], ``` +### Bluesky + +Bluesky comments allow you to pull a conversation from a [Bluesky](https://bsky.app) post directly into your Quartz pages. + +#### Setup + +1. Add `Component.BlueskyComments()` to the `afterBody` field of `sharedPageComponents` in `quartz.layout.ts`: + +```ts title="quartz.layout.ts" +afterBody: [ + Component.BlueskyComments(), +], +``` + +1. On any page where you want comments to appear, add the `blueskyUrl` field to your frontmatter pointing to the Bluesky post: + +```markdown +--- +title: My Cool Post +blueskyUrl: https://bsky.app/profile/jacky.zys.xyz/post/3lf... +--- +``` + +The component will automatically resolve the thread and render the replies recursively. + ### Customization Quartz also exposes a few of the other Giscus options as well and you can provide them the same way `repo`, `repoId`, `category`, and `categoryId` are provided. @@ -125,7 +150,7 @@ afterBody: [ Quartz can conditionally display the comment box based on a field `comments` in the frontmatter. By default, all pages will display comments, to disable it for a specific page, set `comments` to `false`. -``` +```markdown --- title: Comments disabled here! comments: false diff --git a/quartz.layout.ts b/quartz.layout.ts index 970a5be34..13a1e191f 100644 --- a/quartz.layout.ts +++ b/quartz.layout.ts @@ -5,7 +5,9 @@ import * as Component from "./quartz/components" export const sharedPageComponents: SharedLayout = { head: Component.Head(), header: [], - afterBody: [], + 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 new file mode 100644 index 000000000..ce6a3b989 --- /dev/null +++ b/quartz/components/BlueskyComments.tsx @@ -0,0 +1,30 @@ + +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { classNames } from "../util/lang" +// @ts-ignore +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 + } + + return ( +
+

Comments

+

+ Post a reply on Bluesky to join the conversation. +

+
+

Loading comments...

+
+
+ ) +} + +BlueskyComments.afterDOMLoaded = script +BlueskyComments.css = style + +export default (() => BlueskyComments) satisfies QuartzComponentConstructor diff --git a/quartz/components/index.ts b/quartz/components/index.ts index cece8e614..344f67f84 100644 --- a/quartz/components/index.ts +++ b/quartz/components/index.ts @@ -23,6 +23,7 @@ import Breadcrumbs from "./Breadcrumbs" import Comments from "./Comments" import Flex from "./Flex" import ConditionalRender from "./ConditionalRender" +import BlueskyComments from "./BlueskyComments" export { ArticleTitle, @@ -50,4 +51,5 @@ export { Comments, Flex, ConditionalRender, + BlueskyComments, } diff --git a/quartz/components/scripts/bluesky.inline.ts b/quartz/components/scripts/bluesky.inline.ts new file mode 100644 index 000000000..e3f1218e9 --- /dev/null +++ b/quartz/components/scripts/bluesky.inline.ts @@ -0,0 +1,199 @@ + +document.addEventListener("nav", async () => { + const container = document.getElementById("bluesky-comments") + if (!container) 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." + } + } +}) + +function parseBlueskyUrl(url: string) { + 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 } +} + +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 +} + +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` + ) + + 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 + + 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." + } +} + +function createCommentNode(reply: any): HTMLElement { + const post = reply.post + const author = post.author + const date = new Date(post.indexedAt).toLocaleDateString() + + const div = document.createElement("div") + div.className = "comment" + + // 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) + } + + 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) + + div.appendChild(header) + + // Body + div.appendChild(renderPostBody(post)) + + // Actions + const actions = document.createElement("div") + actions.className = "comment-actions" + + 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) + + 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) + } + + 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))) + } + + return body +} diff --git a/quartz/components/styles/bluesky.scss b/quartz/components/styles/bluesky.scss new file mode 100644 index 000000000..e587761d2 --- /dev/null +++ b/quartz/components/styles/bluesky.scss @@ -0,0 +1,77 @@ +.bluesky-comments-container { + margin-top: 4rem; + padding-top: 2rem; + border-top: 1px solid var(--lightgray); + + 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; + } + } + } + + .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; + } + + .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; + } + } +} \ No newline at end of file From 93f6bc2e55679c9e000e1f0110507179868be603 Mon Sep 17 00:00:00 2001 From: eamag Date: Sun, 25 Jan 2026 17:20:40 +0100 Subject: [PATCH 2/2] lint --- quartz.layout.ts | 4 +- quartz/components/BlueskyComments.tsx | 39 ++- quartz/components/scripts/bluesky.inline.ts | 313 ++++++++++---------- quartz/components/styles/bluesky.scss | 130 ++++---- 4 files changed, 244 insertions(+), 242 deletions(-) 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 ( -
-

Comments

-

- Post a reply on Bluesky to join the conversation. -

-
-

Loading comments...

-
-
- ) + return ( +
+

Comments

+

+ Post a reply on{" "} + + Bluesky + {" "} + to join the conversation. +

+
+

Loading comments...

+
+
+ ) } 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; + } + } +}