add bsky and hackernews

This commit is contained in:
eamag 2026-07-18 15:28:13 +02:00
parent 42c5023e42
commit 183b1f9bfa
14 changed files with 1254 additions and 53 deletions

114
README.md
View file

@ -1,6 +1,6 @@
# @quartz-community/comments
Adds a comment section to pages using Giscus (GitHub Discussions-based).
Adds a comment section to pages using Giscus (GitHub Discussions), Bluesky, or Hacker News.
## Installation
@ -15,46 +15,100 @@ plugins:
- source: github:quartz-community/comments
enabled: true
options:
provider: giscus
provider: giscus # Or "bluesky" or "hackernews"
options: {}
layout:
position: afterBody
priority: 10
```
For advanced use cases, you can override in TypeScript:
---
```ts title="quartz.ts (override)"
import * as ExternalPlugin from "./.quartz/plugins";
## Providers Setup
ExternalPlugin.Comments({
provider: "giscus",
options: {
repo: "your-repo",
repoId: "your-repo-id",
category: "your-category",
categoryId: "your-category-id",
},
});
```
### 1. Bluesky
Pulls a reply thread directly from a Bluesky post.
1. Set the provider to `"bluesky"` in `quartz.config.yaml`.
2. Add the `blueskyUrl` field pointing to the post in the page's frontmatter:
```markdown
---
title: My Blog Post
blueskyUrl: https://bsky.app/profile/username.bsky.social/post/3mq...
---
```
### 2. Hacker News
Pulls a reply thread directly from a Hacker News item thread.
1. Set the provider to `"hackernews"` in `quartz.config.yaml`.
2. Add the `hnId` or `hnUrl` field in the page's frontmatter:
```markdown
---
title: My Blog Post
hnId: 123456
# Or full url:
# hnUrl: https://news.ycombinator.com/item?id=123456
---
```
### 3. Giscus
GitHub Discussions-based commenting system.
1. Set the provider to `"giscus"`.
2. Provide your repository options:
```ts title="quartz.ts (override)"
import * as ExternalPlugin from "./.quartz/plugins";
ExternalPlugin.Comments({
provider: "giscus",
options: {
repo: "your-repo",
repoId: "your-repo-id",
category: "your-category",
categoryId: "your-category-id",
},
});
```
---
## Configuration
| Option | Type | Default | Description |
| -------------------------- | ------------------- | ----------- | ----------------------------------------------- |
| `provider` | `"giscus"` | - | The comment provider to use. |
| `options.repo` | `string` | - | The GitHub repository to use for comments. |
| `options.repoId` | `string` | - | The ID of the GitHub repository. |
| `options.category` | `string` | - | The GitHub Discussions category to use. |
| `options.categoryId` | `string` | - | The ID of the GitHub Discussions category. |
| `options.themeUrl` | `string` | `undefined` | Custom theme URL for Giscus. |
| `options.lightTheme` | `string` | `"light"` | The light theme for Giscus. |
| `options.darkTheme` | `string` | `"dark"` | The dark theme for Giscus. |
| `options.mapping` | `string` | `"url"` | The mapping between pages and discussions. |
| `options.strict` | `boolean` | `true` | Whether to use strict matching for discussions. |
| `options.reactionsEnabled` | `boolean` | `true` | Whether to enable reactions for comments. |
| `options.inputPosition` | `"top" \| "bottom"` | `"bottom"` | The position of the comment input box. |
| `options.lang` | `string` | `"en"` | The language for Giscus. |
| Option | Type | Default | Description |
| -------------------------- | --------------------------------------- | ----------- | ---------------------------------------------- |
| `provider` | `"giscus" \| "bluesky" \| "hackernews"` | - | The comment provider to use. |
| `options.repo` | `string` | - | (Giscus only) The GitHub repository to use. |
| `options.repoId` | `string` | - | (Giscus only) The ID of the GitHub repository. |
| `options.category` | `string` | - | (Giscus only) The Discussions category to use. |
| `options.categoryId` | `string` | - | (Giscus only) The ID of the category. |
| `options.themeUrl` | `string` | `undefined` | (Giscus only) Custom theme URL for Giscus. |
| `options.lightTheme` | `string` | `"light"` | (Giscus only) The light theme for Giscus. |
| `options.darkTheme` | `string` | `"dark"` | (Giscus only) The dark theme for Giscus. |
| `options.mapping` | `string` | `"url"` | (Giscus only) Page to discussion mapping. |
| `options.strict` | `boolean` | `true` | (Giscus only) Use strict discussion matching. |
| `options.reactionsEnabled` | `boolean` | `true` | (Giscus only) Enable reactions for comments. |
| `options.inputPosition` | `"top" \| "bottom"` | `"bottom"` | (Giscus only) The position of the input box. |
| `options.lang` | `string` | `"en"` | (Giscus only) The language for Giscus. |
---
## Customization
You can conditionally disable comments on a specific page by setting `comments: false` in that page's frontmatter:
```markdown
---
title: Comments Disabled
comments: false
---
```
## Documentation

View file

@ -5,6 +5,18 @@ function classNames(...classes) {
// src/components/scripts/comments.inline.ts
var comments_inline_default = 'var d=s=>{let e=s.detail.theme,t=document.querySelector("iframe.giscus-frame");t&&t.contentWindow&&t.contentWindow.postMessage({giscus:{setConfig:{theme:c(r(e))}}},"https://giscus.app")},r=s=>{if(s!=="dark"&&s!=="light")return s;let e=document.querySelector(".giscus");if(!e)return s;let t=e.dataset.darkTheme??"dark",a=e.dataset.lightTheme??"light";return s==="dark"?t:a},c=s=>{let e=document.querySelector(".giscus");return e?`${e.dataset.themeUrl??"https://giscus.app/themes"}/${s}.css`:`https://giscus.app/themes/${s}.css`},n=[],u=s=>{n.push(s)};if(typeof document<"u"){let s=()=>{n.forEach(o=>o()),n.length=0;let e=document.querySelector(".giscus");if(!e)return;let t=document.createElement("script");t.src="https://giscus.app/client.js",t.async=!0,t.crossOrigin="anonymous",t.setAttribute("data-loading","lazy"),t.setAttribute("data-emit-metadata","0"),t.setAttribute("data-repo",e.dataset.repo),t.setAttribute("data-repo-id",e.dataset.repoId),t.setAttribute("data-category",e.dataset.category),t.setAttribute("data-category-id",e.dataset.categoryId),t.setAttribute("data-mapping",e.dataset.mapping),t.setAttribute("data-strict",e.dataset.strict),t.setAttribute("data-reactions-enabled",e.dataset.reactionsEnabled),t.setAttribute("data-input-position",e.dataset.inputPosition),t.setAttribute("data-lang",e.dataset.lang);let a=document.documentElement.getAttribute("saved-theme");a&&t.setAttribute("data-theme",c(r(a))),e.appendChild(t);let i=d;document.addEventListener("themechange",i),u(()=>document.removeEventListener("themechange",i))};document.addEventListener("nav",s),document.addEventListener("render",s)}\n';
// src/components/scripts/bluesky.inline.ts
var bluesky_inline_default = 'function f(a){let n=new URL(a).pathname.split("/"),t=n[2],o=n[4];if(!t||!o)throw new Error("Invalid Bluesky URL");return{handle:t,postId:o}}async function y(a){let n=await fetch(`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${a}`);if(!n.ok)throw new Error("Could not resolve handle");let{did:t}=await n.json();return t}async function C(a,n){let t=`at://${a}/app.bsky.feed.post/${n}`,o=await fetch(`https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=${t}&depth=10&parentHeight=0`);if(!o.ok)throw new Error("Could not fetch thread");return(await o.json()).thread.replies}function k(a){let n=document.getElementById("bluesky-comments-list");n&&(a&&a.length>0?(n.innerHTML="",a.sort((t,o)=>(o.post.likeCount||0)-(t.post.likeCount||0)),a.forEach(t=>{t.post&&n.appendChild(h(t))})):n.textContent="No comments yet.")}function h(a){let n=a.post,t=n.author,o=new Date(n.indexedAt).toLocaleDateString(),s=document.createElement("div");s.className="comment";let c=document.createElement("div");if(c.className="comment-header",t.avatar){let e=document.createElement("img");e.src=t.avatar,e.alt=t.handle,c.appendChild(e)}let r=document.createElement("a"),d=n.uri.split("/").pop();r.href=`https://bsky.app/profile/${t.did}/post/${d}`,r.target="_blank",r.className="handle",r.textContent=t.displayName||t.handle,c.appendChild(r);let p=document.createElement("span");p.className="time",p.textContent=o,c.appendChild(p),s.appendChild(c),s.appendChild(x(n));let m=document.createElement("div");m.className="comment-actions";let l=document.createElement("span");l.textContent=`\\u2764\\uFE0F ${n.likeCount||0}`,m.appendChild(l);let i=document.createElement("span");if(i.textContent=`\\u{1F501} ${n.repostCount||0}`,m.appendChild(i),s.appendChild(m),a.replies&&a.replies.length>0){let e=document.createElement("div");e.className="replies",a.replies.forEach(u=>{u.post&&e.appendChild(h(u))}),s.appendChild(e)}return s}function x(a){let n=a.record.text,t=a.record.facets,o=document.createElement("div");if(o.className="comment-body",!t||t.length===0)return o.textContent=n,o;t.sort((r,d)=>r.index.byteStart-d.index.byteStart);let s=new TextEncoder().encode(n),c=0;for(let r of t){let{byteStart:d,byteEnd:p}=r.index;if(d>c){let e=s.slice(c,d),u=document.createTextNode(new TextDecoder().decode(e));o.appendChild(u)}let m=s.slice(d,p),l=new TextDecoder().decode(m),i=r.features[0];if(i.$type==="app.bsky.richtext.facet#link"){let e=document.createElement("a");e.href=i.uri,e.target="_blank",e.rel="noopener noreferrer",e.textContent=l,o.appendChild(e)}else if(i.$type==="app.bsky.richtext.facet#mention"){let e=document.createElement("a");e.href=`https://bsky.app/profile/${i.did}`,e.target="_blank",e.rel="noopener noreferrer",e.textContent=l,o.appendChild(e)}else if(i.$type==="app.bsky.richtext.facet#tag"){let e=document.createElement("a");e.href=`https://bsky.app/hashtag/${i.tag}`,e.target="_blank",e.rel="noopener noreferrer",e.textContent=l,o.appendChild(e)}else o.appendChild(document.createTextNode(l));c=p}if(c<s.length){let r=s.slice(c);o.appendChild(document.createTextNode(new TextDecoder().decode(r)))}return o}if(typeof document<"u"){let a=async()=>{let n=document.getElementById("bluesky-comments");if(!n)return;let t=n.getAttribute("data-url");if(!t)return;let o=document.getElementById("bluesky-comments-list");o&&(o.innerHTML="<p>Loading comments...</p>");try{let{handle:s,postId:c}=f(t),r=await y(s),d=await C(r,c);k(d)}catch(s){console.error("Error loading Bluesky comments:",s),o&&(o.textContent="Error loading comments.")}};document.addEventListener("nav",a),document.addEventListener("render",a)}\n';
// src/components/styles/bluesky.scss
var bluesky_default = ".bluesky-comments-container {\n margin-top: 4rem;\n padding-top: 2rem;\n border-top: 1px solid var(--lightgray);\n h2 {\n margin-bottom: 1rem;\n }\n .bluesky-meta {\n margin-bottom: 1.5rem;\n color: var(--gray);\n font-size: 0.9rem;\n a {\n color: var(--secondary);\n text-decoration: none;\n &:hover {\n text-decoration: underline;\n }\n }\n }\n .comment {\n margin-top: 1rem;\n padding-left: 1rem;\n border-left: 2px solid var(--lightgray);\n .comment-header {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n margin-bottom: 0.5rem;\n img {\n width: 24px;\n height: 24px;\n border-radius: 50%;\n }\n .handle {\n font-weight: bold;\n color: var(--dark);\n font-size: 0.9rem;\n }\n .time {\n color: var(--gray);\n font-size: 0.8rem;\n }\n }\n .comment-body {\n font-size: 0.95rem;\n color: var(--darkgray);\n white-space: pre-wrap;\n a {\n color: var(--secondary);\n text-decoration: none;\n &:hover {\n text-decoration: underline;\n }\n }\n }\n .comment-actions {\n margin-top: 0.25rem;\n font-size: 0.8rem;\n color: var(--gray);\n display: flex;\n gap: 1rem;\n }\n }\n}\n";
// src/components/scripts/hackernews.inline.ts
var hackernews_inline_default = 'function d(e){if(/^\\d+$/.test(e))return e;try{let n=new URL(e).searchParams.get("id");if(n)return n}catch{}throw new Error("Invalid Hacker News URL or ID")}async function m(e){let t=await fetch(`https://hn.algolia.com/api/v1/items/${e}`);if(!t.ok)throw new Error("Failed to fetch HN comments");return(await t.json()).children}function h(e){let t=document.getElementById("hackernews-comments-list");t&&(e&&e.length>0?(t.innerHTML="",e.forEach(n=>{!n.author&&!n.text||t.appendChild(i(n))})):t.textContent="No comments yet.")}function i(e){let t=document.createElement("div");t.className="comment";let n=document.createElement("div");n.className="comment-header";let a=document.createElement("a");a.href=`https://news.ycombinator.com/user?id=${e.author||""}`,a.target="_blank",a.className="author",a.textContent=e.author||"[deleted]",n.appendChild(a);let o=document.createElement("span");o.className="time",e.created_at?o.textContent=new Date(e.created_at).toLocaleDateString():o.textContent="",n.appendChild(o),t.appendChild(n);let c=document.createElement("div");if(c.className="comment-body",c.innerHTML=e.text||"[deleted]",t.appendChild(c),e.children&&e.children.length>0){let r=document.createElement("div");r.className="replies",e.children.forEach(s=>{!s.author&&!s.text||r.appendChild(i(s))}),t.appendChild(r)}return t}if(typeof document<"u"){let e=async()=>{let t=document.getElementById("hackernews-comments");if(!t)return;let n=t.getAttribute("data-id");if(!n)return;let a=document.getElementById("hackernews-comments-list");a&&(a.innerHTML="<p>Loading comments...</p>");try{let o=d(n),c=await m(o);h(c)}catch(o){console.error("Error loading Hacker News comments:",o),a&&(a.textContent="Error loading comments.")}};document.addEventListener("nav",e),document.addEventListener("render",e)}\n';
// src/components/styles/hackernews.scss
var hackernews_default = ".hackernews-comments-container {\n margin-top: 4rem;\n padding-top: 2rem;\n border-top: 1px solid var(--lightgray);\n h2 {\n margin-bottom: 1rem;\n }\n .hackernews-meta {\n margin-bottom: 1.5rem;\n color: var(--gray);\n font-size: 0.9rem;\n a {\n color: var(--secondary);\n text-decoration: none;\n &:hover {\n text-decoration: underline;\n }\n }\n }\n .comment {\n margin-top: 1rem;\n padding-left: 1rem;\n border-left: 2px solid var(--lightgray);\n .comment-header {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n margin-bottom: 0.5rem;\n .author {\n font-weight: bold;\n color: var(--dark);\n font-size: 0.9rem;\n text-decoration: none;\n &:hover {\n text-decoration: underline;\n }\n }\n .time {\n color: var(--gray);\n font-size: 0.8rem;\n }\n }\n .comment-body {\n font-size: 0.95rem;\n color: var(--darkgray);\n p {\n margin: 0.5rem 0;\n }\n pre {\n background-color: var(--lightgray);\n padding: 0.5rem;\n border-radius: 4px;\n overflow-x: auto;\n }\n code {\n font-family: var(--codeVal);\n font-size: 0.85rem;\n }\n a {\n color: var(--secondary);\n text-decoration: none;\n &:hover {\n text-decoration: underline;\n }\n }\n }\n }\n}\n";
var l;
function S(n2) {
return n2.children;
@ -39,6 +51,57 @@ var Comments_default = ((opts) => {
if (commentsOverride === false || commentsOverride === "false") {
return /* @__PURE__ */ u2(S, {});
}
if (opts.provider === "bluesky") {
const blueskyUrl = fileData.frontmatter?.blueskyUrl;
if (!blueskyUrl) {
return /* @__PURE__ */ u2(S, {});
}
return /* @__PURE__ */ u2(
"div",
{
class: classNames(displayClass, "bluesky-comments-container"),
id: "bluesky-comments",
"data-url": blueskyUrl,
children: [
/* @__PURE__ */ u2("h2", { children: "Comments" }),
/* @__PURE__ */ u2("p", { class: "bluesky-meta", children: [
"Post a reply on",
" ",
/* @__PURE__ */ u2("a", { href: blueskyUrl, target: "_blank", rel: "noopener noreferrer", children: "Bluesky" }),
" ",
"to join the conversation."
] }),
/* @__PURE__ */ u2("div", { id: "bluesky-comments-list", children: /* @__PURE__ */ u2("p", { children: "Loading comments..." }) })
]
}
);
}
if (opts.provider === "hackernews") {
const hnId = fileData.frontmatter?.hnId ? String(fileData.frontmatter.hnId) : fileData.frontmatter?.hnUrl;
if (!hnId) {
return /* @__PURE__ */ u2(S, {});
}
const threadUrl = hnId.startsWith("http") ? hnId : `https://news.ycombinator.com/item?id=${hnId}`;
return /* @__PURE__ */ u2(
"div",
{
class: classNames(displayClass, "hackernews-comments-container"),
id: "hackernews-comments",
"data-id": hnId,
children: [
/* @__PURE__ */ u2("h2", { children: "Comments" }),
/* @__PURE__ */ u2("p", { class: "hackernews-meta", children: [
"Post a reply on",
" ",
/* @__PURE__ */ u2("a", { href: threadUrl, target: "_blank", rel: "noopener noreferrer", children: "Hacker News" }),
" ",
"to join the conversation."
] }),
/* @__PURE__ */ u2("div", { id: "hackernews-comments-list", children: /* @__PURE__ */ u2("p", { children: "Loading comments..." }) })
]
}
);
}
return /* @__PURE__ */ u2(
"div",
{
@ -58,7 +121,15 @@ var Comments_default = ((opts) => {
}
);
};
Comments.afterDOMLoaded = comments_inline_default;
if (opts.provider === "giscus") {
Comments.afterDOMLoaded = comments_inline_default;
} else if (opts.provider === "bluesky") {
Comments.afterDOMLoaded = bluesky_inline_default;
Comments.css = bluesky_default;
} else if (opts.provider === "hackernews") {
Comments.afterDOMLoaded = hackernews_inline_default;
Comments.css = hackernews_default;
}
return Comments;
});

File diff suppressed because one or more lines are too long

6
dist/index.d.ts vendored
View file

@ -16,6 +16,12 @@ type CommentsOptions = {
inputPosition?: "top" | "bottom";
lang?: string;
};
} | {
provider: "bluesky";
options?: Record<string, never>;
} | {
provider: "hackernews";
options?: Record<string, never>;
};
declare const _default: (opts: CommentsOptions) => QuartzComponent;

73
dist/index.js vendored
View file

@ -5,6 +5,18 @@ function classNames(...classes) {
// src/components/scripts/comments.inline.ts
var comments_inline_default = 'var d=s=>{let e=s.detail.theme,t=document.querySelector("iframe.giscus-frame");t&&t.contentWindow&&t.contentWindow.postMessage({giscus:{setConfig:{theme:c(r(e))}}},"https://giscus.app")},r=s=>{if(s!=="dark"&&s!=="light")return s;let e=document.querySelector(".giscus");if(!e)return s;let t=e.dataset.darkTheme??"dark",a=e.dataset.lightTheme??"light";return s==="dark"?t:a},c=s=>{let e=document.querySelector(".giscus");return e?`${e.dataset.themeUrl??"https://giscus.app/themes"}/${s}.css`:`https://giscus.app/themes/${s}.css`},n=[],u=s=>{n.push(s)};if(typeof document<"u"){let s=()=>{n.forEach(o=>o()),n.length=0;let e=document.querySelector(".giscus");if(!e)return;let t=document.createElement("script");t.src="https://giscus.app/client.js",t.async=!0,t.crossOrigin="anonymous",t.setAttribute("data-loading","lazy"),t.setAttribute("data-emit-metadata","0"),t.setAttribute("data-repo",e.dataset.repo),t.setAttribute("data-repo-id",e.dataset.repoId),t.setAttribute("data-category",e.dataset.category),t.setAttribute("data-category-id",e.dataset.categoryId),t.setAttribute("data-mapping",e.dataset.mapping),t.setAttribute("data-strict",e.dataset.strict),t.setAttribute("data-reactions-enabled",e.dataset.reactionsEnabled),t.setAttribute("data-input-position",e.dataset.inputPosition),t.setAttribute("data-lang",e.dataset.lang);let a=document.documentElement.getAttribute("saved-theme");a&&t.setAttribute("data-theme",c(r(a))),e.appendChild(t);let i=d;document.addEventListener("themechange",i),u(()=>document.removeEventListener("themechange",i))};document.addEventListener("nav",s),document.addEventListener("render",s)}\n';
// src/components/scripts/bluesky.inline.ts
var bluesky_inline_default = 'function f(a){let n=new URL(a).pathname.split("/"),t=n[2],o=n[4];if(!t||!o)throw new Error("Invalid Bluesky URL");return{handle:t,postId:o}}async function y(a){let n=await fetch(`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${a}`);if(!n.ok)throw new Error("Could not resolve handle");let{did:t}=await n.json();return t}async function C(a,n){let t=`at://${a}/app.bsky.feed.post/${n}`,o=await fetch(`https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=${t}&depth=10&parentHeight=0`);if(!o.ok)throw new Error("Could not fetch thread");return(await o.json()).thread.replies}function k(a){let n=document.getElementById("bluesky-comments-list");n&&(a&&a.length>0?(n.innerHTML="",a.sort((t,o)=>(o.post.likeCount||0)-(t.post.likeCount||0)),a.forEach(t=>{t.post&&n.appendChild(h(t))})):n.textContent="No comments yet.")}function h(a){let n=a.post,t=n.author,o=new Date(n.indexedAt).toLocaleDateString(),s=document.createElement("div");s.className="comment";let c=document.createElement("div");if(c.className="comment-header",t.avatar){let e=document.createElement("img");e.src=t.avatar,e.alt=t.handle,c.appendChild(e)}let r=document.createElement("a"),d=n.uri.split("/").pop();r.href=`https://bsky.app/profile/${t.did}/post/${d}`,r.target="_blank",r.className="handle",r.textContent=t.displayName||t.handle,c.appendChild(r);let p=document.createElement("span");p.className="time",p.textContent=o,c.appendChild(p),s.appendChild(c),s.appendChild(x(n));let m=document.createElement("div");m.className="comment-actions";let l=document.createElement("span");l.textContent=`\\u2764\\uFE0F ${n.likeCount||0}`,m.appendChild(l);let i=document.createElement("span");if(i.textContent=`\\u{1F501} ${n.repostCount||0}`,m.appendChild(i),s.appendChild(m),a.replies&&a.replies.length>0){let e=document.createElement("div");e.className="replies",a.replies.forEach(u=>{u.post&&e.appendChild(h(u))}),s.appendChild(e)}return s}function x(a){let n=a.record.text,t=a.record.facets,o=document.createElement("div");if(o.className="comment-body",!t||t.length===0)return o.textContent=n,o;t.sort((r,d)=>r.index.byteStart-d.index.byteStart);let s=new TextEncoder().encode(n),c=0;for(let r of t){let{byteStart:d,byteEnd:p}=r.index;if(d>c){let e=s.slice(c,d),u=document.createTextNode(new TextDecoder().decode(e));o.appendChild(u)}let m=s.slice(d,p),l=new TextDecoder().decode(m),i=r.features[0];if(i.$type==="app.bsky.richtext.facet#link"){let e=document.createElement("a");e.href=i.uri,e.target="_blank",e.rel="noopener noreferrer",e.textContent=l,o.appendChild(e)}else if(i.$type==="app.bsky.richtext.facet#mention"){let e=document.createElement("a");e.href=`https://bsky.app/profile/${i.did}`,e.target="_blank",e.rel="noopener noreferrer",e.textContent=l,o.appendChild(e)}else if(i.$type==="app.bsky.richtext.facet#tag"){let e=document.createElement("a");e.href=`https://bsky.app/hashtag/${i.tag}`,e.target="_blank",e.rel="noopener noreferrer",e.textContent=l,o.appendChild(e)}else o.appendChild(document.createTextNode(l));c=p}if(c<s.length){let r=s.slice(c);o.appendChild(document.createTextNode(new TextDecoder().decode(r)))}return o}if(typeof document<"u"){let a=async()=>{let n=document.getElementById("bluesky-comments");if(!n)return;let t=n.getAttribute("data-url");if(!t)return;let o=document.getElementById("bluesky-comments-list");o&&(o.innerHTML="<p>Loading comments...</p>");try{let{handle:s,postId:c}=f(t),r=await y(s),d=await C(r,c);k(d)}catch(s){console.error("Error loading Bluesky comments:",s),o&&(o.textContent="Error loading comments.")}};document.addEventListener("nav",a),document.addEventListener("render",a)}\n';
// src/components/styles/bluesky.scss
var bluesky_default = ".bluesky-comments-container {\n margin-top: 4rem;\n padding-top: 2rem;\n border-top: 1px solid var(--lightgray);\n h2 {\n margin-bottom: 1rem;\n }\n .bluesky-meta {\n margin-bottom: 1.5rem;\n color: var(--gray);\n font-size: 0.9rem;\n a {\n color: var(--secondary);\n text-decoration: none;\n &:hover {\n text-decoration: underline;\n }\n }\n }\n .comment {\n margin-top: 1rem;\n padding-left: 1rem;\n border-left: 2px solid var(--lightgray);\n .comment-header {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n margin-bottom: 0.5rem;\n img {\n width: 24px;\n height: 24px;\n border-radius: 50%;\n }\n .handle {\n font-weight: bold;\n color: var(--dark);\n font-size: 0.9rem;\n }\n .time {\n color: var(--gray);\n font-size: 0.8rem;\n }\n }\n .comment-body {\n font-size: 0.95rem;\n color: var(--darkgray);\n white-space: pre-wrap;\n a {\n color: var(--secondary);\n text-decoration: none;\n &:hover {\n text-decoration: underline;\n }\n }\n }\n .comment-actions {\n margin-top: 0.25rem;\n font-size: 0.8rem;\n color: var(--gray);\n display: flex;\n gap: 1rem;\n }\n }\n}\n";
// src/components/scripts/hackernews.inline.ts
var hackernews_inline_default = 'function d(e){if(/^\\d+$/.test(e))return e;try{let n=new URL(e).searchParams.get("id");if(n)return n}catch{}throw new Error("Invalid Hacker News URL or ID")}async function m(e){let t=await fetch(`https://hn.algolia.com/api/v1/items/${e}`);if(!t.ok)throw new Error("Failed to fetch HN comments");return(await t.json()).children}function h(e){let t=document.getElementById("hackernews-comments-list");t&&(e&&e.length>0?(t.innerHTML="",e.forEach(n=>{!n.author&&!n.text||t.appendChild(i(n))})):t.textContent="No comments yet.")}function i(e){let t=document.createElement("div");t.className="comment";let n=document.createElement("div");n.className="comment-header";let a=document.createElement("a");a.href=`https://news.ycombinator.com/user?id=${e.author||""}`,a.target="_blank",a.className="author",a.textContent=e.author||"[deleted]",n.appendChild(a);let o=document.createElement("span");o.className="time",e.created_at?o.textContent=new Date(e.created_at).toLocaleDateString():o.textContent="",n.appendChild(o),t.appendChild(n);let c=document.createElement("div");if(c.className="comment-body",c.innerHTML=e.text||"[deleted]",t.appendChild(c),e.children&&e.children.length>0){let r=document.createElement("div");r.className="replies",e.children.forEach(s=>{!s.author&&!s.text||r.appendChild(i(s))}),t.appendChild(r)}return t}if(typeof document<"u"){let e=async()=>{let t=document.getElementById("hackernews-comments");if(!t)return;let n=t.getAttribute("data-id");if(!n)return;let a=document.getElementById("hackernews-comments-list");a&&(a.innerHTML="<p>Loading comments...</p>");try{let o=d(n),c=await m(o);h(c)}catch(o){console.error("Error loading Hacker News comments:",o),a&&(a.textContent="Error loading comments.")}};document.addEventListener("nav",e),document.addEventListener("render",e)}\n';
// src/components/styles/hackernews.scss
var hackernews_default = ".hackernews-comments-container {\n margin-top: 4rem;\n padding-top: 2rem;\n border-top: 1px solid var(--lightgray);\n h2 {\n margin-bottom: 1rem;\n }\n .hackernews-meta {\n margin-bottom: 1.5rem;\n color: var(--gray);\n font-size: 0.9rem;\n a {\n color: var(--secondary);\n text-decoration: none;\n &:hover {\n text-decoration: underline;\n }\n }\n }\n .comment {\n margin-top: 1rem;\n padding-left: 1rem;\n border-left: 2px solid var(--lightgray);\n .comment-header {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n margin-bottom: 0.5rem;\n .author {\n font-weight: bold;\n color: var(--dark);\n font-size: 0.9rem;\n text-decoration: none;\n &:hover {\n text-decoration: underline;\n }\n }\n .time {\n color: var(--gray);\n font-size: 0.8rem;\n }\n }\n .comment-body {\n font-size: 0.95rem;\n color: var(--darkgray);\n p {\n margin: 0.5rem 0;\n }\n pre {\n background-color: var(--lightgray);\n padding: 0.5rem;\n border-radius: 4px;\n overflow-x: auto;\n }\n code {\n font-family: var(--codeVal);\n font-size: 0.85rem;\n }\n a {\n color: var(--secondary);\n text-decoration: none;\n &:hover {\n text-decoration: underline;\n }\n }\n }\n }\n}\n";
var l;
function S(n2) {
return n2.children;
@ -39,6 +51,57 @@ var Comments_default = ((opts) => {
if (commentsOverride === false || commentsOverride === "false") {
return /* @__PURE__ */ u2(S, {});
}
if (opts.provider === "bluesky") {
const blueskyUrl = fileData.frontmatter?.blueskyUrl;
if (!blueskyUrl) {
return /* @__PURE__ */ u2(S, {});
}
return /* @__PURE__ */ u2(
"div",
{
class: classNames(displayClass, "bluesky-comments-container"),
id: "bluesky-comments",
"data-url": blueskyUrl,
children: [
/* @__PURE__ */ u2("h2", { children: "Comments" }),
/* @__PURE__ */ u2("p", { class: "bluesky-meta", children: [
"Post a reply on",
" ",
/* @__PURE__ */ u2("a", { href: blueskyUrl, target: "_blank", rel: "noopener noreferrer", children: "Bluesky" }),
" ",
"to join the conversation."
] }),
/* @__PURE__ */ u2("div", { id: "bluesky-comments-list", children: /* @__PURE__ */ u2("p", { children: "Loading comments..." }) })
]
}
);
}
if (opts.provider === "hackernews") {
const hnId = fileData.frontmatter?.hnId ? String(fileData.frontmatter.hnId) : fileData.frontmatter?.hnUrl;
if (!hnId) {
return /* @__PURE__ */ u2(S, {});
}
const threadUrl = hnId.startsWith("http") ? hnId : `https://news.ycombinator.com/item?id=${hnId}`;
return /* @__PURE__ */ u2(
"div",
{
class: classNames(displayClass, "hackernews-comments-container"),
id: "hackernews-comments",
"data-id": hnId,
children: [
/* @__PURE__ */ u2("h2", { children: "Comments" }),
/* @__PURE__ */ u2("p", { class: "hackernews-meta", children: [
"Post a reply on",
" ",
/* @__PURE__ */ u2("a", { href: threadUrl, target: "_blank", rel: "noopener noreferrer", children: "Hacker News" }),
" ",
"to join the conversation."
] }),
/* @__PURE__ */ u2("div", { id: "hackernews-comments-list", children: /* @__PURE__ */ u2("p", { children: "Loading comments..." }) })
]
}
);
}
return /* @__PURE__ */ u2(
"div",
{
@ -58,7 +121,15 @@ var Comments_default = ((opts) => {
}
);
};
Comments.afterDOMLoaded = comments_inline_default;
if (opts.provider === "giscus") {
Comments.afterDOMLoaded = comments_inline_default;
} else if (opts.provider === "bluesky") {
Comments.afterDOMLoaded = bluesky_inline_default;
Comments.css = bluesky_default;
} else if (opts.provider === "hackernews") {
Comments.afterDOMLoaded = hackernews_inline_default;
Comments.css = hackernews_default;
}
return Comments;
});

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

420
package-lock.json generated
View file

@ -22,6 +22,7 @@
"eslint-config-prettier": "^9.1.0",
"preact": "^10.28.2",
"prettier": "^3.6.2",
"sass": "^1.101.0",
"tsup": "^8.5.0",
"typescript": "^5.9.3",
"vitest": "^2.1.9"
@ -707,6 +708,348 @@
"node": ">= 8"
}
},
"node_modules/@parcel/watcher": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
"integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"detect-libc": "^2.0.3",
"is-glob": "^4.0.3",
"node-addon-api": "^7.0.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.5.6",
"@parcel/watcher-darwin-arm64": "2.5.6",
"@parcel/watcher-darwin-x64": "2.5.6",
"@parcel/watcher-freebsd-x64": "2.5.6",
"@parcel/watcher-linux-arm-glibc": "2.5.6",
"@parcel/watcher-linux-arm-musl": "2.5.6",
"@parcel/watcher-linux-arm64-glibc": "2.5.6",
"@parcel/watcher-linux-arm64-musl": "2.5.6",
"@parcel/watcher-linux-x64-glibc": "2.5.6",
"@parcel/watcher-linux-x64-musl": "2.5.6",
"@parcel/watcher-win32-arm64": "2.5.6",
"@parcel/watcher-win32-ia32": "2.5.6",
"@parcel/watcher-win32-x64": "2.5.6"
}
},
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
"integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
"integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-x64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
"integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
"integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
"integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
"cpu": [
"arm"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-musl": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
"integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
"cpu": [
"arm"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
"integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
"integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
"integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
"integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
"integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-ia32": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
"integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
"integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher/node_modules/picomatch": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@quartz-community/types": {
"version": "0.2.1",
"resolved": "git+ssh://git@github.com/quartz-community/types.git#d342893db8240099cfd9d4499996333f1ca636b4",
@ -1826,6 +2169,17 @@
"node": ">=6"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"engines": {
"node": ">=8"
}
},
"node_modules/devlop": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
@ -2424,6 +2778,13 @@
"node": ">= 4"
}
},
"node_modules/immutable": {
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz",
"integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==",
"dev": true,
"license": "MIT"
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@ -2768,6 +3129,14 @@
"dev": true,
"license": "MIT"
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -3210,6 +3579,57 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/sass": {
"version": "1.101.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.101.0.tgz",
"integrity": "sha512-OL3GoQyoUdDt843DpVmDO6y2k1sc5IhUDSpu8XucEI+35neq5QivZ1iuegnpraEVTJXlQGK1gl27zKcTLEPbQw==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^5.0.0",
"immutable": "^5.1.5",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=20.19.0"
},
"optionalDependencies": {
"@parcel/watcher": "^2.4.1"
}
},
"node_modules/sass/node_modules/chokidar": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"dev": true,
"license": "MIT",
"dependencies": {
"readdirp": "^5.0.0"
},
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/sass/node_modules/readdirp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",

View file

@ -72,6 +72,7 @@
"eslint-config-prettier": "^9.1.0",
"preact": "^10.28.2",
"prettier": "^3.6.2",
"sass": "^1.101.0",
"tsup": "^8.5.0",
"typescript": "^5.9.3",
"vitest": "^2.1.9"
@ -97,7 +98,9 @@
"provider": {
"type": "enum",
"values": [
"giscus"
"giscus",
"bluesky",
"hackernews"
]
},
"options": {

View file

@ -2,26 +2,41 @@ import type { QuartzComponent, QuartzComponentProps } from "@quartz-community/ty
import { classNames } from "../util/lang";
// @ts-expect-error - inline script imported as string by esbuild loader
import script from "./scripts/comments.inline";
// @ts-expect-error - inline script imported as string by esbuild loader
import blueskyScript from "./scripts/bluesky.inline";
import blueskyStyle from "./styles/bluesky.scss";
// @ts-expect-error - inline script imported as string by esbuild loader
import hackernewsScript from "./scripts/hackernews.inline";
import hackernewsStyle from "./styles/hackernews.scss";
type RequiredOptsConstructor<Options> = (opts: Options) => QuartzComponent;
export type CommentsOptions = {
provider: "giscus";
options: {
repo: `${string}/${string}`;
repoId: string;
category: string;
categoryId: string;
themeUrl?: string;
lightTheme?: string;
darkTheme?: string;
mapping?: "url" | "title" | "og:title" | "specific" | "number" | "pathname";
strict?: boolean;
reactionsEnabled?: boolean;
inputPosition?: "top" | "bottom";
lang?: string;
};
};
export type CommentsOptions =
| {
provider: "giscus";
options: {
repo: `${string}/${string}`;
repoId: string;
category: string;
categoryId: string;
themeUrl?: string;
lightTheme?: string;
darkTheme?: string;
mapping?: "url" | "title" | "og:title" | "specific" | "number" | "pathname";
strict?: boolean;
reactionsEnabled?: boolean;
inputPosition?: "top" | "bottom";
lang?: string;
};
}
| {
provider: "bluesky";
options?: Record<string, never>;
}
| {
provider: "hackernews";
options?: Record<string, never>;
};
function boolToStringBool(b: boolean): string {
return b ? "1" : "0";
@ -34,6 +49,63 @@ export default ((opts: CommentsOptions) => {
return <></>;
}
if (opts.provider === "bluesky") {
const blueskyUrl = fileData.frontmatter?.blueskyUrl as string | undefined;
if (!blueskyUrl) {
return <></>;
}
return (
<div
class={classNames(displayClass, "bluesky-comments-container")}
id="bluesky-comments"
data-url={blueskyUrl}
>
<h2>Comments</h2>
<p class="bluesky-meta">
Post a reply on{" "}
<a href={blueskyUrl} target="_blank" rel="noopener noreferrer">
Bluesky
</a>{" "}
to join the conversation.
</p>
<div id="bluesky-comments-list">
<p>Loading comments...</p>
</div>
</div>
);
}
if (opts.provider === "hackernews") {
const hnId = fileData.frontmatter?.hnId
? String(fileData.frontmatter.hnId)
: (fileData.frontmatter?.hnUrl as string | undefined);
if (!hnId) {
return <></>;
}
const threadUrl = hnId.startsWith("http")
? hnId
: `https://news.ycombinator.com/item?id=${hnId}`;
return (
<div
class={classNames(displayClass, "hackernews-comments-container")}
id="hackernews-comments"
data-id={hnId}
>
<h2>Comments</h2>
<p class="hackernews-meta">
Post a reply on{" "}
<a href={threadUrl} target="_blank" rel="noopener noreferrer">
Hacker News
</a>{" "}
to join the conversation.
</p>
<div id="hackernews-comments-list">
<p>Loading comments...</p>
</div>
</div>
);
}
return (
<div
class={classNames(displayClass, "giscus")}
@ -55,7 +127,15 @@ export default ((opts: CommentsOptions) => {
);
};
Comments.afterDOMLoaded = script;
if (opts.provider === "giscus") {
Comments.afterDOMLoaded = script;
} else if (opts.provider === "bluesky") {
Comments.afterDOMLoaded = blueskyScript;
Comments.css = blueskyStyle;
} else if (opts.provider === "hackernews") {
Comments.afterDOMLoaded = hackernewsScript;
Comments.css = hackernewsStyle;
}
return Comments;
}) satisfies RequiredOptsConstructor<CommentsOptions>;

View file

@ -0,0 +1,179 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
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;
}
if (typeof document !== "undefined") {
const setupBluesky = async () => {
const container = document.getElementById("bluesky-comments");
if (!container) return;
const url = container.getAttribute("data-url");
if (!url) return;
const commentsList = document.getElementById("bluesky-comments-list");
if (commentsList) {
commentsList.innerHTML = "<p>Loading comments...</p>";
}
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);
if (commentsList) {
commentsList.textContent = "Error loading comments.";
}
}
};
document.addEventListener("nav", setupBluesky);
document.addEventListener("render", setupBluesky);
}

View file

@ -0,0 +1,106 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
function parseHnUrl(urlOrId: string): string {
if (/^\d+$/.test(urlOrId)) return urlOrId;
try {
const url = new URL(urlOrId);
const id = url.searchParams.get("id");
if (id) return id;
} catch (e) {
// Ignore URL parse error
}
throw new Error("Invalid Hacker News URL or ID");
}
async function fetchHnThread(id: string) {
const res = await fetch(`https://hn.algolia.com/api/v1/items/${id}`);
if (!res.ok) throw new Error("Failed to fetch HN comments");
const data = await res.json();
return data.children;
}
function renderHnComments(replies: any[]) {
const commentsList = document.getElementById("hackernews-comments-list");
if (!commentsList) return;
if (replies && replies.length > 0) {
commentsList.innerHTML = ""; // Clear loading text
replies.forEach((reply: any) => {
if (!reply.author && !reply.text) return;
commentsList.appendChild(createHnCommentNode(reply));
});
} else {
commentsList.textContent = "No comments yet.";
}
}
function createHnCommentNode(reply: any): HTMLElement {
const div = document.createElement("div");
div.className = "comment";
// Header
const header = document.createElement("div");
header.className = "comment-header";
const authorLink = document.createElement("a");
authorLink.href = `https://news.ycombinator.com/user?id=${reply.author || ""}`;
authorLink.target = "_blank";
authorLink.className = "author";
authorLink.textContent = reply.author || "[deleted]";
header.appendChild(authorLink);
const timeSpan = document.createElement("span");
timeSpan.className = "time";
if (reply.created_at) {
timeSpan.textContent = new Date(reply.created_at).toLocaleDateString();
} else {
timeSpan.textContent = "";
}
header.appendChild(timeSpan);
div.appendChild(header);
// Body
const body = document.createElement("div");
body.className = "comment-body";
body.innerHTML = reply.text || "[deleted]";
div.appendChild(body);
// Recursive replies
if (reply.children && reply.children.length > 0) {
const repliesContainer = document.createElement("div");
repliesContainer.className = "replies";
reply.children.forEach((child: any) => {
if (!child.author && !child.text) return;
repliesContainer.appendChild(createHnCommentNode(child));
});
div.appendChild(repliesContainer);
}
return div;
}
if (typeof document !== "undefined") {
const setupHnComments = async () => {
const container = document.getElementById("hackernews-comments");
if (!container) return;
const urlOrId = container.getAttribute("data-id");
if (!urlOrId) return;
const commentsList = document.getElementById("hackernews-comments-list");
if (commentsList) {
commentsList.innerHTML = "<p>Loading comments...</p>";
}
try {
const id = parseHnUrl(urlOrId);
const replies = await fetchHnThread(id);
renderHnComments(replies);
} catch (e) {
console.error("Error loading Hacker News comments:", e);
if (commentsList) {
commentsList.textContent = "Error loading comments.";
}
}
};
document.addEventListener("nav", setupHnComments);
document.addEventListener("render", setupHnComments);
}

View file

@ -0,0 +1,64 @@
.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;
}
}
}

View file

@ -0,0 +1,68 @@
.hackernews-comments-container {
margin-top: 4rem;
padding-top: 2rem;
border-top: 1px solid var(--lightgray);
h2 {
margin-bottom: 1rem;
}
.hackernews-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;
.author {
font-weight: bold;
color: var(--dark);
font-size: 0.9rem;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.time {
color: var(--gray);
font-size: 0.8rem;
}
}
.comment-body {
font-size: 0.95rem;
color: var(--darkgray);
p {
margin: 0.5rem 0;
}
pre {
background-color: var(--lightgray);
padding: 0.5rem;
border-radius: 4px;
overflow-x: auto;
}
code {
font-family: var(--codeVal);
font-size: 0.85rem;
}
a {
color: var(--secondary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
}
}

View file

@ -187,3 +187,82 @@ describe("Comments: data attribute wiring", () => {
expect(props?.["data-theme-url"]).toBe("https://cdn.test/theme");
});
});
describe("Comments: Bluesky Provider", () => {
const bskyOpts: CommentsOptions = {
provider: "bluesky",
};
it("should create a component with bluesky options", () => {
const component = Comments(bskyOpts);
expect(component).toBeDefined();
expect(typeof component).toBe("function");
});
it("renders nothing when frontmatter.blueskyUrl is missing", () => {
const result = renderComments(bskyOpts, buildProps({ frontmatter: {} }));
expect(isFragmentOrEmpty(result)).toBe(true);
});
it("renders the bluesky container when frontmatter.blueskyUrl is present", () => {
const url = "https://bsky.app/profile/user/post/123456";
const result = renderComments(bskyOpts, buildProps({ frontmatter: { blueskyUrl: url } }));
const props = getDivProps(result);
expect(props).not.toBeNull();
expect(props?.id).toBe("bluesky-comments");
expect(props?.["data-url"]).toBe(url);
});
it("suppresses bluesky comments when frontmatter.comments is false", () => {
const url = "https://bsky.app/profile/user/post/123456";
const result = renderComments(
bskyOpts,
buildProps({ frontmatter: { blueskyUrl: url, comments: false } }),
);
expect(isFragmentOrEmpty(result)).toBe(true);
});
});
describe("Comments: Hacker News Provider", () => {
const hnOpts: CommentsOptions = {
provider: "hackernews",
};
it("should create a component with hackernews options", () => {
const component = Comments(hnOpts);
expect(component).toBeDefined();
expect(typeof component).toBe("function");
});
it("renders nothing when frontmatter.hnId/hnUrl is missing", () => {
const result = renderComments(hnOpts, buildProps({ frontmatter: {} }));
expect(isFragmentOrEmpty(result)).toBe(true);
});
it("renders the HN container when frontmatter.hnId is present", () => {
const id = "123456";
const result = renderComments(hnOpts, buildProps({ frontmatter: { hnId: id } }));
const props = getDivProps(result);
expect(props).not.toBeNull();
expect(props?.id).toBe("hackernews-comments");
expect(props?.["data-id"]).toBe(id);
});
it("renders the HN container when frontmatter.hnUrl is present", () => {
const url = "https://news.ycombinator.com/item?id=123456";
const result = renderComments(hnOpts, buildProps({ frontmatter: { hnUrl: url } }));
const props = getDivProps(result);
expect(props).not.toBeNull();
expect(props?.id).toBe("hackernews-comments");
expect(props?.["data-id"]).toBe(url);
});
it("suppresses HN comments when frontmatter.comments is false", () => {
const id = "123456";
const result = renderComments(
hnOpts,
buildProps({ frontmatter: { hnId: id, comments: false } }),
);
expect(isFragmentOrEmpty(result)).toBe(true);
});
});