diff --git a/packages/cli/src/components/sequoia-comments.js b/packages/cli/src/components/sequoia-comments.js
index f979cd1..dfc7502 100644
--- a/packages/cli/src/components/sequoia-comments.js
+++ b/packages/cli/src/components/sequoia-comments.js
@@ -12,6 +12,7 @@
* 2. A tag in the document head
*
* Attributes:
+ * - post-uri: Bluesky post as AT-URI (at://...) or bsky.app URL — skips PDS document lookup
* - document-uri: AT Protocol URI for the document (optional if link tag exists)
* - depth: Maximum depth of nested replies to fetch (default: 6)
* - hide: Set to "auto" to hide if no document link is detected
@@ -614,6 +615,40 @@ function isThreadViewPost(post) {
* @param {string} postUri - AT Protocol URI for the post
* @returns {Promise} Array of PostView objects
*/
+/**
+ * Normalise a user-supplied post reference to an AT-URI.
+ * Accepts:
+ * - AT-URIs as-is: at://did:plc:.../app.bsky.feed.post/rkey
+ * - bsky.app post URLs: https://bsky.app/profile//post/
+ * When the profile segment is already a DID no network request is made.
+ * @param {string} uriOrUrl
+ * @returns {Promise} AT-URI
+ */
+async function resolvePostUri(uriOrUrl) {
+ if (uriOrUrl.startsWith("at://")) return uriOrUrl;
+
+ const match = uriOrUrl.match(
+ /bsky\.app\/profile\/([^/?#]+)\/post\/([^/?#]+)/,
+ );
+ if (!match) throw new Error(`Cannot parse Bluesky URL: ${uriOrUrl}`);
+
+ const [, handleOrDid, rkey] = match;
+
+ let did = handleOrDid;
+ if (!handleOrDid.startsWith("did:")) {
+ const url = new URL(
+ "https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle",
+ );
+ url.searchParams.set("handle", handleOrDid);
+ const response = await fetch(url.toString());
+ if (!response.ok)
+ throw new Error(`Failed to resolve handle: ${response.status}`);
+ did = (await response.json()).did;
+ }
+
+ return `at://${did}/app.bsky.feed.post/${rkey}`;
+}
+
async function getQuotes(postUri) {
const quotes = [];
let cursor;
@@ -676,7 +711,7 @@ class SequoiaComments extends BaseElement {
}
static get observedAttributes() {
- return ["document-uri", "depth", "hide"];
+ return ["post-uri", "document-uri", "depth", "hide"];
}
connectedCallback() {
@@ -726,31 +761,36 @@ class SequoiaComments extends BaseElement {
this.state = { type: "loading" };
this.render();
- const docUri = this.documentUri;
- if (!docUri) {
- this.state = { type: "no-document" };
- this.render();
- return;
- }
-
try {
- // Fetch the document record
- const document = await getDocument(docUri);
+ // Resolve the post URI — either directly from the attribute or via the
+ // document record (which requires a PDS roundtrip)
+ const rawPostUri = this.getAttribute("post-uri");
+ let postUri = rawPostUri ? await resolvePostUri(rawPostUri) : null;
+ if (!postUri) {
+ const docUri = this.documentUri;
+ if (!docUri) {
+ this.state = { type: "no-document" };
+ this.render();
+ return;
+ }
- // Check if document has a Bluesky post reference
- if (!document.bskyPostRef) {
- this.state = { type: "no-comments-enabled" };
- this.render();
- return;
+ const document = await getDocument(docUri);
+ if (!document.bskyPostRef) {
+ this.state = { type: "no-comments-enabled" };
+ this.render();
+ return;
+ }
+
+ postUri = document.bskyPostRef.uri;
}
- const postUrl = buildBskyAppUrl(document.bskyPostRef.uri);
- const blackskyPostUrl = buildBlackskyAppUrl(document.bskyPostRef.uri);
+ const postUrl = buildBskyAppUrl(postUri);
+ const blackskyPostUrl = buildBlackskyAppUrl(postUri);
// Fetch thread and quotes in parallel; quote failures degrade gracefully
const [threadResult, quotesResult] = await Promise.allSettled([
- getPostThread(document.bskyPostRef.uri, this.depth),
- getQuotes(document.bskyPostRef.uri),
+ getPostThread(postUri, this.depth),
+ getQuotes(postUri),
]);
if (threadResult.status === "rejected") {