From 6bc34263c29703791fc2a1b8e1c23d3d4ef8bb30 Mon Sep 17 00:00:00 2001 From: David Merfield Date: Thu, 13 Nov 2025 11:57:12 +0100 Subject: [PATCH 1/9] Add Bluesky comments plugin --- app/blog/entry.js | 1 + app/blog/tests/pluginHTML.js | 46 ++++++++++++ app/build/plugins/blueskyComments/entry.html | 70 +++++++++++++++++++ app/build/plugins/blueskyComments/index.js | 9 +++ app/build/plugins/blueskyComments/public.css | 1 + app/build/plugins/index.js | 1 + app/views/how/configure/bluesky-comments.html | 33 +++++++++ app/views/how/configure/comments.html | 12 ++++ app/views/tools/comments/bluesky.html | 12 ++++ app/views/tools/comments/index.html | 7 +- 10 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 app/build/plugins/blueskyComments/entry.html create mode 100644 app/build/plugins/blueskyComments/index.js create mode 100644 app/build/plugins/blueskyComments/public.css create mode 100644 app/views/how/configure/bluesky-comments.html create mode 100644 app/views/tools/comments/bluesky.html diff --git a/app/blog/entry.js b/app/blog/entry.js index 10492421f7c..6fb645cea81 100644 --- a/app/blog/entry.js +++ b/app/blog/entry.js @@ -38,6 +38,7 @@ module.exports = function (request, response, next) { ) { delete blog.plugins.commento; delete blog.plugins.disqus; + delete blog.plugins.blueskyComments; } // Redirect this entry to the file from which it was generated diff --git a/app/blog/tests/pluginHTML.js b/app/blog/tests/pluginHTML.js index 59363fe1c92..465f1acc5ba 100644 --- a/app/blog/tests/pluginHTML.js +++ b/app/blog/tests/pluginHTML.js @@ -81,6 +81,52 @@ describe("pluginHTML", function () { expect(await areThereComments('/foo')).toBe(false, 'comments should not appear on posts with comments disabled'); }); + it("injects bluesky comments html", async function () { + + const blueskyUriPost = 'at://did:plc:example/app.bsky.feed.post/3jqf4zda6q2'; + const blueskyUriPage = 'at://did:plc:example/app.bsky.feed.post/6p9z7m8n1v0'; + + const plugins = { + ...this.blog.plugins, + blueskyComments: { + enabled: true, + options: { + authorHandle: 'example.bsky.social', + service: 'https://public.api.bsky.app' + } + } + }; + + await this.blog.update({plugins}) + + await this.template({ "entry.html": "{{{entry.html}}} {{> pluginHTML}}" }); + await this.write({path: '/a.txt', content: `Link: /foo\nBluesky: ${blueskyUriPost}\n\nHello, world!`}); + await this.write({path: '/Pages/about.txt', content: `Link: /about\nComments: Yes\nBluesky: ${blueskyUriPage}\n\nHello, page!`}); + await this.write({path: '/Drafts/test.txt', content: 'Hello, draft!'}); + + const areThereComments = async (path) => { + const res = await this.get(path); + const body = await res.text(); + return body.includes('id="bluesky-comments"') && body.includes('bluesky-comments.es.js'); + } + + expect(await areThereComments('/foo')).toBe(true, 'comments should appear on posts with Bluesky metadata'); + expect(await areThereComments('/about')).toBe(true, 'comments should appear on pages with comments enabled and Bluesky metadata'); + expect(await areThereComments('/draft/view/Drafts/test.txt')).toBe(false, 'comments should not appear on drafts'); + + const res = await this.fetch(config.protocol + 'preview-of-my-local-on-' + this.blog.handle + '.' + config.host + '/foo'); + const body = await res.text(); + + expect(res.status).toEqual(200); + expect(body).not.toContain('bluesky-comments.es.js'); + + await this.write({path: '/a.txt', content: `Link: /foo\nComments: No\nBluesky: ${blueskyUriPost}\n\nHello, world!`}); + expect(await areThereComments('/foo')).toBe(false, 'comments should not appear on posts with comments disabled'); + + await this.write({path: '/a.txt', content: 'Link: /foo\n\nHello, world!'}); + expect(await areThereComments('/foo')).toBe(false, 'comments should not appear when Bluesky metadata is missing'); + }); + it("injects google analytics into appJS", async function () { const plugins = {...this.blog.plugins, analytics: {enabled: true, options: {provider: {Google: true}, trackingID: 'UA-12345678-9'}}}; diff --git a/app/build/plugins/blueskyComments/entry.html b/app/build/plugins/blueskyComments/entry.html new file mode 100644 index 00000000000..6d666552745 --- /dev/null +++ b/app/build/plugins/blueskyComments/entry.html @@ -0,0 +1,70 @@ +{{#entry.metadata.bluesky}} +
+ + +{{/entry.metadata.bluesky}} diff --git a/app/build/plugins/blueskyComments/index.js b/app/build/plugins/blueskyComments/index.js new file mode 100644 index 00000000000..f6b3a982abc --- /dev/null +++ b/app/build/plugins/blueskyComments/index.js @@ -0,0 +1,9 @@ +module.exports = { + description: "Bluesky comments integration", + category: "external", + isDefault: false, + options: { + authorHandle: "", + service: "https://public.api.bsky.app", + }, +}; diff --git a/app/build/plugins/blueskyComments/public.css b/app/build/plugins/blueskyComments/public.css new file mode 100644 index 00000000000..f18cf5c08fd --- /dev/null +++ b/app/build/plugins/blueskyComments/public.css @@ -0,0 +1 @@ +@import url("https://unpkg.com/bluesky-comments@0.8.0/dist/bluesky-comments.css"); diff --git a/app/build/plugins/index.js b/app/build/plugins/index.js index 8e712750aa5..dddc7911fcd 100644 --- a/app/build/plugins/index.js +++ b/app/build/plugins/index.js @@ -22,6 +22,7 @@ var loaded = loadPlugins({ analytics: require("./analytics"), autoImage: require("./autoImage"), bluesky: require("./bluesky"), + blueskyComments: require("./blueskyComments"), codeHighlighting: require("./codeHighlighting"), commento: require("./commento"), disqus: require("./disqus"), diff --git a/app/views/how/configure/bluesky-comments.html b/app/views/how/configure/bluesky-comments.html new file mode 100644 index 00000000000..092b5004bc7 --- /dev/null +++ b/app/views/how/configure/bluesky-comments.html @@ -0,0 +1,33 @@ +

Bluesky comments

+ +

+ Blot’s Bluesky comments plugin lets you embed the discussion for a Bluesky + post at the end of any entry. The comments are rendered by the + Bluesky comments widget + and require the identifier of the Bluesky post you want to attach. +

+ +
    +
  1. Open the Services page and enable Bluesky comments.
  2. +
  3. + Enter the default Bluesky handle you would like to use when loading the widget. + This should usually be the author of the blog or the account responsible for the + linked discussion. +
  4. +
  5. + For every post that should show Bluesky comments, include a + Bluesky field in the entry’s metadata set to the full post URI + (for example at://did:plc:example/app.bsky.feed.post/123). +
  6. +
+ +

+ Without the Bluesky metadata field the widget will not appear, even if the + plugin is enabled. Comments remain hidden on pages unless you explicitly set + Comments: Yes in the page metadata. +

+ +

+ You can combine the plugin with other comment systems and switch between them + whenever you need from the Services page. +

diff --git a/app/views/how/configure/comments.html b/app/views/how/configure/comments.html index f3807106e2c..50d7300ad61 100644 --- a/app/views/how/configure/comments.html +++ b/app/views/how/configure/comments.html @@ -13,8 +13,20 @@

Comments

Disqus +
  • + + Bluesky comments +
  • +

    + See the Bluesky comments guide + for details on how to attach a Bluesky thread to a post. Each entry needs a + Bluesky metadata field pointing to the post’s AT URI for the + widget to appear. +

    +

    Controlling comments on individual entries

    When enabled, comments are shown on posts and are not shown on pages. You can diff --git a/app/views/tools/comments/bluesky.html b/app/views/tools/comments/bluesky.html new file mode 100644 index 00000000000..d2c10cfc15b --- /dev/null +++ b/app/views/tools/comments/bluesky.html @@ -0,0 +1,12 @@ +--- +Link: https://bsky.app/ +--- + +

    Bluesky comments

    + +

    + Embed Bluesky discussion threads at the end of your posts. Enable the plugin on + the Services page, add your default Bluesky + handle, and include a Bluesky metadata field with the AT URI of the + post you want to attach. +

    diff --git a/app/views/tools/comments/index.html b/app/views/tools/comments/index.html index 23e5ab9f2aa..fae8f0e8120 100644 --- a/app/views/tools/comments/index.html +++ b/app/views/tools/comments/index.html @@ -1 +1,6 @@ -

    Let your readers comment.

    \ No newline at end of file +

    Let your readers comment.

    + + From fc1e3400bc65814abfd05dcc9feefaee87c122c5 Mon Sep 17 00:00:00 2001 From: David Merfield Date: Fri, 14 Nov 2025 10:32:33 +0100 Subject: [PATCH 2/9] Revise --- app/blog/entry.js | 2 +- app/build/plugins/blueskyComments/entry.html | 76 ++------------- app/build/plugins/blueskyComments/index.js | 6 +- app/build/plugins/blueskyComments/public.css | 72 +++++++++++++- app/build/plugins/blueskyComments/public.js | 93 +++++++++++++++++++ app/views/tools/icons/bluesky.png | Bin 0 -> 7783 bytes 6 files changed, 173 insertions(+), 76 deletions(-) create mode 100644 app/build/plugins/blueskyComments/public.js create mode 100644 app/views/tools/icons/bluesky.png diff --git a/app/blog/entry.js b/app/blog/entry.js index 6fb645cea81..6a205e9e97d 100644 --- a/app/blog/entry.js +++ b/app/blog/entry.js @@ -84,7 +84,7 @@ module.exports = function (request, response, next) { // Don't show plugin HTML on a preview subdomain. // This is to prevent Disqus getting stuck on one URL. if (entry.draft || request.preview) { - pluginHTML = ""; + pluginHTML = !!pluginHTML ? "

    Comments are hidden on site previews.

    " : ''; } response.locals.partials.pluginHTML = pluginHTML; diff --git a/app/build/plugins/blueskyComments/entry.html b/app/build/plugins/blueskyComments/entry.html index 6d666552745..3127ca4ee27 100644 --- a/app/build/plugins/blueskyComments/entry.html +++ b/app/build/plugins/blueskyComments/entry.html @@ -1,70 +1,8 @@ {{#entry.metadata.bluesky}} -
    - - -{{/entry.metadata.bluesky}} + + +
    +

    Comments

    +

    Post a comment on Bluesky!

    +
    +{{/entry.metadata.bluesky}} \ No newline at end of file diff --git a/app/build/plugins/blueskyComments/index.js b/app/build/plugins/blueskyComments/index.js index f6b3a982abc..a07ae36d5c4 100644 --- a/app/build/plugins/blueskyComments/index.js +++ b/app/build/plugins/blueskyComments/index.js @@ -1,9 +1,5 @@ module.exports = { - description: "Bluesky comments integration", + description: "Bluesky comments", category: "external", isDefault: false, - options: { - authorHandle: "", - service: "https://public.api.bsky.app", - }, }; diff --git a/app/build/plugins/blueskyComments/public.css b/app/build/plugins/blueskyComments/public.css index f18cf5c08fd..eb07c72d9dc 100644 --- a/app/build/plugins/blueskyComments/public.css +++ b/app/build/plugins/blueskyComments/public.css @@ -1 +1,71 @@ -@import url("https://unpkg.com/bluesky-comments@0.8.0/dist/bluesky-comments.css"); +/* Post Actions */ +.comment-container a, #main-post-actions { + text-decoration: none; + color: var(--highlight2); + + .post-actions { + display: flex; + gap: 1em; + + div svg, div span { + vertical-align: middle; + display: inline-block; + } + + div:hover { + color: var(--accent); + + & svg { + stroke: var(--accent); + } + } + } +} + +/* Comment Section */ +#comments { + .comment { + position: relative; /* Needed for pseudo-element positioning */ + .comment-container { + display: flex; + margin-bottom: var(--mPad); + + img { + width: 32px; + height: 32px; + border-radius: 50%; + margin-right: var(--sPad); + } + + a p { + color: var(--accent); + margin: .25em 0; + } + } + + .replies { + padding-left: calc(32px + var(--sPad)); + } + } + + #see-more { + position: absolute; + left: 50%; + transform: translate(-50%); + padding: var(--mPad); + } +} + +.comment:has(.replies)::after { + content: ""; + position: absolute; + top: calc(32px + .5em); + left: 16px; + width: 20px; + height: calc(100% - 41px - .5em); + border: 2px solid var(--highlight); + border-top: none; + border-right: none; + border-radius: 0 0 0 20px; +} + diff --git a/app/build/plugins/blueskyComments/public.js b/app/build/plugins/blueskyComments/public.js new file mode 100644 index 00000000000..5d1be58b8b6 --- /dev/null +++ b/app/build/plugins/blueskyComments/public.js @@ -0,0 +1,93 @@ + const convertToAtUri = (url) => { + const match = url.match(/https:\/\/bsky\.app\/profile\/([^/]+)\/post\/([^/]+)/); + if (!match) + throw new Error("Invalid Bluesky URL format."); + const did = match[1]; + const rkey = match[2]; + return `at://${did}/app.bsky.feed.post/${rkey}`; + }; + + const renderActions = (post) => { + return ` +
    +
    + + + + ${post.likeCount} +
    +
    + + + + ${post.repostCount} +
    +
    + + + + ${post.replyCount} +
    +
    + `; + }; + + const renderComment = (post) => { + const author = post.author; + const postUrl = `https://bsky.app/profile/${author.did}/post/${post.uri.split("/").pop()}`; + + return ` +
    + `; + }; + + const renderThread = (thread) => { + // Handle replies: filter, sort, and limit to 5 + const repliesHtml = thread.replies + .sort((a, b) => (b.post.likeCount || 0) - (a.post.likeCount || 0)) + .slice(0, 3) // Limit to 5 replies + .map(reply => renderComment(reply.post)) + .join("") || ''; + + return ` +
    + ${renderComment(thread.post)} + ${repliesHtml ? `
    ${repliesHtml}
    ` : ''} +
    + `; + }; + + const container = document.getElementById("comments"); + const uri = convertToAtUri(container.dataset.uri); + + fetch(`https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=${encodeURIComponent(uri)}`, { + method: "GET", + headers: { Accept: "application/json" }, + }) + .then(async (response) => { + if (!response.ok) + throw new Error(await response.text()); + + const { thread } = await response.json(); + const replies = (thread.replies || []) + .sort((a, b) => (b.post.likeCount || 0) - (a.post.likeCount || 0)); + + document.getElementById("main-post-actions").innerHTML = renderActions(thread.post); + // Append top 25 comments + container.innerHTML += replies.slice(0, 25).map(renderThread).join(""); + + // Add "Show More" button if there are more comments + if (replies.length > 25 && !container.querySelector('#see-more')) + container.innerHTML += `See more on Bluesky`; + }) + .catch((error) => { + console.error("Error:", error); + container.style.display = "none"; + }); diff --git a/app/views/tools/icons/bluesky.png b/app/views/tools/icons/bluesky.png new file mode 100644 index 0000000000000000000000000000000000000000..09393a741312acca23b3b55bba734d0bbeec2f6c GIT binary patch literal 7783 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 literal 0 HcmV?d00001 From c119aed1f1022d84dbfcc96b5561c32266bdfac2 Mon Sep 17 00:00:00 2001 From: David Merfield Date: Fri, 14 Nov 2025 12:27:03 +0100 Subject: [PATCH 3/9] Revise --- app/build/plugins/blueskyComments/entry.html | 16 +- app/build/plugins/blueskyComments/form.html | 6 + app/build/plugins/blueskyComments/public.css | 131 ++++----- app/build/plugins/blueskyComments/public.js | 278 +++++++++++++------ 4 files changed, 280 insertions(+), 151 deletions(-) create mode 100644 app/build/plugins/blueskyComments/form.html diff --git a/app/build/plugins/blueskyComments/entry.html b/app/build/plugins/blueskyComments/entry.html index 3127ca4ee27..820aa9ccf32 100644 --- a/app/build/plugins/blueskyComments/entry.html +++ b/app/build/plugins/blueskyComments/entry.html @@ -1,8 +1,20 @@ {{#entry.metadata.bluesky}} +{{/entry.metadata.bluesky}} -
    + -{{/entry.metadata.bluesky}} \ No newline at end of file diff --git a/app/build/plugins/blueskyComments/form.html b/app/build/plugins/blueskyComments/form.html new file mode 100644 index 00000000000..80f9d7d04f5 --- /dev/null +++ b/app/build/plugins/blueskyComments/form.html @@ -0,0 +1,6 @@ + +

    Your Bluesky profile URL (e.g., https://bsky.app/profile/example.bsky.social). This will be used to automatically find your posts linking to each page when no Bluesky metadata is set on the entry.

    + diff --git a/app/build/plugins/blueskyComments/public.css b/app/build/plugins/blueskyComments/public.css index eb07c72d9dc..b11b1c3c03c 100644 --- a/app/build/plugins/blueskyComments/public.css +++ b/app/build/plugins/blueskyComments/public.css @@ -1,71 +1,76 @@ /* Post Actions */ -.comment-container a, #main-post-actions { - text-decoration: none; - color: var(--highlight2); - - .post-actions { - display: flex; - gap: 1em; - - div svg, div span { - vertical-align: middle; - display: inline-block; - } - - div:hover { - color: var(--accent); - - & svg { - stroke: var(--accent); - } - } - } +.comment-container a, +#main-post-actions { + text-decoration: none; + color: var(--highlight2); +} + +.comment-container a .post-actions, +#main-post-actions .post-actions { + display: flex; + gap: 1em; +} + +.comment-container a .post-actions div svg, +.comment-container a .post-actions div span, +#main-post-actions .post-actions div svg, +#main-post-actions .post-actions div span { + vertical-align: middle; + display: inline-block; +} + +.comment-container a .post-actions div:hover, +#main-post-actions .post-actions div:hover { + color: var(--accent); +} + +.comment-container a .post-actions div:hover svg, +#main-post-actions .post-actions div:hover svg { + stroke: var(--accent); } /* Comment Section */ -#comments { - .comment { - position: relative; /* Needed for pseudo-element positioning */ - .comment-container { - display: flex; - margin-bottom: var(--mPad); - - img { - width: 32px; - height: 32px; - border-radius: 50%; - margin-right: var(--sPad); - } - - a p { - color: var(--accent); - margin: .25em 0; - } - } - - .replies { - padding-left: calc(32px + var(--sPad)); - } - } - - #see-more { - position: absolute; - left: 50%; - transform: translate(-50%); - padding: var(--mPad); - } +#comments .comment { + position: relative; /* Needed for pseudo-element positioning */ } -.comment:has(.replies)::after { - content: ""; - position: absolute; - top: calc(32px + .5em); - left: 16px; - width: 20px; - height: calc(100% - 41px - .5em); - border: 2px solid var(--highlight); - border-top: none; - border-right: none; - border-radius: 0 0 0 20px; +#comments .comment .comment-container { + display: flex; + margin-bottom: var(--mPad); } +#comments .comment .comment-container img { + width: 32px; + height: 32px; + border-radius: 50%; + margin-right: var(--sPad); +} + +#comments .comment .comment-container a p { + color: var(--accent); + margin: 0.25em 0; +} + +#comments .comment .replies { + padding-left: calc(32px + var(--sPad)); +} + +#comments #see-more { + position: absolute; + left: 50%; + transform: translate(-50%); + padding: var(--mPad); +} + +.comment:has(.replies)::after { + content: ""; + position: absolute; + top: calc(32px + 0.5em); + left: 16px; + width: 20px; + height: calc(100% - 41px - 0.5em); + border: 2px solid var(--highlight); + border-top: none; + border-right: none; + border-radius: 0 0 0 20px; +} diff --git a/app/build/plugins/blueskyComments/public.js b/app/build/plugins/blueskyComments/public.js index 5d1be58b8b6..d35226ba493 100644 --- a/app/build/plugins/blueskyComments/public.js +++ b/app/build/plugins/blueskyComments/public.js @@ -1,93 +1,199 @@ - const convertToAtUri = (url) => { - const match = url.match(/https:\/\/bsky\.app\/profile\/([^/]+)\/post\/([^/]+)/); - if (!match) - throw new Error("Invalid Bluesky URL format."); - const did = match[1]; - const rkey = match[2]; - return `at://${did}/app.bsky.feed.post/${rkey}`; - }; +const convertToAtUri = (url) => { + const match = url.match( + /https:\/\/bsky\.app\/profile\/([^/]+)\/post\/([^/]+)/ + ); + if (!match) throw new Error("Invalid Bluesky URL format."); + const did = match[1]; + const rkey = match[2]; + return `at://${did}/app.bsky.feed.post/${rkey}`; +}; - const renderActions = (post) => { - return ` -
    -
    - - - - ${post.likeCount} -
    -
    - - - - ${post.repostCount} -
    -
    - - - - ${post.replyCount} -
    -
    - `; - }; +const extractHandleFromProfileUrl = (profileUrl) => { + // Extract handle from profile URL (e.g., https://bsky.app/profile/example.bsky.social) + const match = profileUrl.match(/https?:\/\/bsky\.app\/profile\/([^/?]+)/); + if (match) { + return match[1]; + } - const renderComment = (post) => { - const author = post.author; - const postUrl = `https://bsky.app/profile/${author.did}/post/${post.uri.split("/").pop()}`; - - return ` -
    - `; - }; + throw new Error("Invalid Bluesky profile URL format."); +}; - const renderThread = (thread) => { - // Handle replies: filter, sort, and limit to 5 - const repliesHtml = thread.replies - .sort((a, b) => (b.post.likeCount || 0) - (a.post.likeCount || 0)) - .slice(0, 3) // Limit to 5 replies - .map(reply => renderComment(reply.post)) - .join("") || ''; - - return ` -
    - ${renderComment(thread.post)} - ${repliesHtml ? `
    ${repliesHtml}
    ` : ''} +const renderActions = (post) => { + return ` +
    +
    + + + + ${post.likeCount}
    - `; - }; +
    + + + + ${post.repostCount} +
    +
    + + + + ${post.replyCount} +
    +
    + `; +}; - const container = document.getElementById("comments"); - const uri = convertToAtUri(container.dataset.uri); +const renderComment = (post) => { + const author = post.author; + const postUrl = `https://bsky.app/profile/${author.did}/post/${post.uri + .split("/") + .pop()}`; + + return ` + + `; +}; + +const renderThread = (thread) => { + // Handle replies: filter, sort, and limit to 3 + const repliesHtml = + thread.replies + .sort((a, b) => (b.post.likeCount || 0) - (a.post.likeCount || 0)) + .slice(0, 3) + .map((reply) => renderComment(reply.post)) + .join("") || ""; + + return ` +
    + ${renderComment(thread.post)} + ${repliesHtml ? `
    ${repliesHtml}
    ` : ""} +
    + `; +}; + +const loadThread = (uri, container, originalUrl) => { + fetch( + `https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=${encodeURIComponent( + uri + )}`, + { + method: "GET", + headers: { Accept: "application/json" }, + } + ) + .then(async (response) => { + if (!response.ok) throw new Error(await response.text()); + + const { thread } = await response.json(); + const replies = (thread.replies || []).sort( + (a, b) => (b.post.likeCount || 0) - (a.post.likeCount || 0) + ); - fetch(`https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=${encodeURIComponent(uri)}`, { - method: "GET", - headers: { Accept: "application/json" }, + // Add main post actions if element exists + const mainPostActions = document.getElementById("main-post-actions"); + if (mainPostActions) { + mainPostActions.innerHTML = renderActions(thread.post); + } + + // Append top 25 comments + container.innerHTML += replies.slice(0, 25).map(renderThread).join(""); + + // Add "Show More" button if there are more comments + if (replies.length > 25 && !container.querySelector("#see-more")) { + const postUrl = + originalUrl || + `https://bsky.app/profile/${thread.post.author.did}/post/${uri + .split("/") + .pop()}`; + container.innerHTML += `See more on Bluesky`; + } }) - .then(async (response) => { - if (!response.ok) - throw new Error(await response.text()); - - const { thread } = await response.json(); - const replies = (thread.replies || []) - .sort((a, b) => (b.post.likeCount || 0) - (a.post.likeCount || 0)); - - document.getElementById("main-post-actions").innerHTML = renderActions(thread.post); - // Append top 25 comments - container.innerHTML += replies.slice(0, 25).map(renderThread).join(""); - - // Add "Show More" button if there are more comments - if (replies.length > 25 && !container.querySelector('#see-more')) - container.innerHTML += `See more on Bluesky`; - }) - .catch((error) => { - console.error("Error:", error); - container.style.display = "none"; - }); + .catch((error) => { + console.error("Error loading thread:", error); + container.innerHTML = `

    Error loading comments. Post on Bluesky to start the discussion!

    `; + }); +}; + +const init = () => { + const container = document.getElementById("comments"); + + if (!container) return; + + // Priority 1: Use Bluesky metadata if set + if (container.dataset.uri) { + const uri = convertToAtUri(container.dataset.uri); + loadThread(uri, container, container.dataset.uri); + } + // Priority 2: Auto-discover using author search + else if ( + container.dataset.autoDiscover === "true" && + container.dataset.author + ) { + const authorProfileUrl = container.dataset.author; + const author = extractHandleFromProfileUrl(authorProfileUrl); + const fetchPost = async () => { + const currentUrl = window.location.href; + const apiUrl = `https://public.api.bsky.app/xrpc/app.bsky.feed.searchPosts?q=*&url=${encodeURIComponent( + currentUrl + )}&author=${encodeURIComponent(author)}&sort=top`; + + try { + const response = await fetch(apiUrl); + const data = await response.json(); + + if (data.posts && data.posts.length > 0) { + const post = data.posts[0]; + const uri = post.uri; + + // Update container with comments header + container.innerHTML = ` +

    Comments

    +

    Post a comment on Bluesky!

    + `; + + // Add main post actions element + container.innerHTML += ``; + + // Load the thread + loadThread( + uri, + container, + `https://bsky.app/profile/${post.author.did}/post/${uri + .split("/") + .pop()}` + ); + } else { + container.innerHTML = ` +

    Comments

    +

    No Bluesky post found for this page. Post on Bluesky to start the discussion!

    + `; + } + } catch (err) { + console.error("Error fetching post:", err); + container.innerHTML = ` +

    Comments

    +

    Error searching for Bluesky post. Post on Bluesky to start the discussion!

    + `; + } + }; + + fetchPost(); + } + // Priority 3: Show error message if no author configured + // Error message is already in the HTML from entry.html, no action needed +}; + +init(); From b6f23f3dc943ad079d64bec9671815b69a875715 Mon Sep 17 00:00:00 2001 From: David Merfield Date: Fri, 14 Nov 2025 12:55:45 +0100 Subject: [PATCH 4/9] Revise --- app/build/plugins/blueskyComments/entry.html | 11 ------ app/build/plugins/blueskyComments/public.css | 36 ++++++++++++------- app/build/plugins/blueskyComments/public.js | 37 ++++++++++++-------- 3 files changed, 46 insertions(+), 38 deletions(-) diff --git a/app/build/plugins/blueskyComments/entry.html b/app/build/plugins/blueskyComments/entry.html index 820aa9ccf32..903105b4cfd 100644 --- a/app/build/plugins/blueskyComments/entry.html +++ b/app/build/plugins/blueskyComments/entry.html @@ -6,15 +6,4 @@ {{#entry.metadata.bluesky}}data-uri="{{{entry.metadata.bluesky}}}"{{/entry.metadata.bluesky}} {{^entry.metadata.bluesky}}{{#plugins.blueskyComments.options.authorHandle}}data-author="{{plugins.blueskyComments.options.authorHandle}}" data-auto-discover="true"{{/plugins.blueskyComments.options.authorHandle}}{{^plugins.blueskyComments.options.authorHandle}}data-error="no-author"{{/plugins.blueskyComments.options.authorHandle}}{{/entry.metadata.bluesky}}>

    Comments

    - {{#entry.metadata.bluesky}} -

    Post a comment on Bluesky!

    - {{/entry.metadata.bluesky}} - {{^entry.metadata.bluesky}} - {{#plugins.blueskyComments.options.authorHandle}} -

    Loading comments...

    - {{/plugins.blueskyComments.options.authorHandle}} - {{^plugins.blueskyComments.options.authorHandle}} -

    Bluesky comments are not configured. Please set a Bluesky metadata field on this entry, or configure your Bluesky author handle in the plugin settings.

    - {{/plugins.blueskyComments.options.authorHandle}} - {{/entry.metadata.bluesky}}
    diff --git a/app/build/plugins/blueskyComments/public.css b/app/build/plugins/blueskyComments/public.css index b11b1c3c03c..ffd8ad4979a 100644 --- a/app/build/plugins/blueskyComments/public.css +++ b/app/build/plugins/blueskyComments/public.css @@ -1,3 +1,11 @@ +.comment-container { + display: flex; +} + +.comment-container > a { + flex-shrink: 0; +} + /* Post Actions */ .comment-container a, #main-post-actions { @@ -5,26 +13,31 @@ color: var(--highlight2); } -.comment-container a .post-actions, +.comment-container .post-actions, #main-post-actions .post-actions { display: flex; gap: 1em; } -.comment-container a .post-actions div svg, -.comment-container a .post-actions div span, +.post-actions svg { + width: 12px; + height: 12px; +} + +.comment-container .post-actions div svg, +.comment-container .post-actions div span, #main-post-actions .post-actions div svg, #main-post-actions .post-actions div span { vertical-align: middle; display: inline-block; } -.comment-container a .post-actions div:hover, +.comment-container .post-actions div:hover, #main-post-actions .post-actions div:hover { color: var(--accent); } -.comment-container a .post-actions div:hover svg, +.comment-container .post-actions div:hover svg, #main-post-actions .post-actions div:hover svg { stroke: var(--accent); } @@ -35,31 +48,30 @@ } #comments .comment .comment-container { - display: flex; - margin-bottom: var(--mPad); + margin-bottom: 24px; } -#comments .comment .comment-container img { +#comments .comment-container img { width: 32px; height: 32px; border-radius: 50%; - margin-right: var(--sPad); + margin-right: 16px; } -#comments .comment .comment-container a p { +#comments .comment .comment-container p { color: var(--accent); margin: 0.25em 0; } #comments .comment .replies { - padding-left: calc(32px + var(--sPad)); + padding-left: calc(32px + 16px); } #comments #see-more { position: absolute; left: 50%; transform: translate(-50%); - padding: var(--mPad); + padding: 24px; } .comment:has(.replies)::after { diff --git a/app/build/plugins/blueskyComments/public.js b/app/build/plugins/blueskyComments/public.js index d35226ba493..36bab575ed4 100644 --- a/app/build/plugins/blueskyComments/public.js +++ b/app/build/plugins/blueskyComments/public.js @@ -22,21 +22,15 @@ const renderActions = (post) => { return `
    - - - + ${post.likeCount}
    - - - + ${post.repostCount}
    - - - + ${post.replyCount}
    @@ -44,6 +38,8 @@ const renderActions = (post) => { }; const renderComment = (post) => { + console.log(post); + const author = post.author; const postUrl = `https://bsky.app/profile/${author.did}/post/${post.uri .split("/") @@ -51,12 +47,16 @@ const renderComment = (post) => { return `
    - avatar - @${author.handle} + avatar + +
    + + ${author.displayName} +

    ${post.record?.text}

    ${renderActions(post)} - +
    `; }; @@ -129,6 +129,8 @@ const init = () => { // Priority 1: Use Bluesky metadata if set if (container.dataset.uri) { const uri = convertToAtUri(container.dataset.uri); + // Add initial content for metadata case + container.innerHTML += `

    Reply on Bluesky

    `; loadThread(uri, container, container.dataset.uri); } // Priority 2: Auto-discover using author search @@ -136,6 +138,9 @@ const init = () => { container.dataset.autoDiscover === "true" && container.dataset.author ) { + // Add loading message + container.innerHTML += `

    Loading comments...

    `; + const authorProfileUrl = container.dataset.author; const author = extractHandleFromProfileUrl(authorProfileUrl); const fetchPost = async () => { @@ -155,11 +160,11 @@ const init = () => { // Update container with comments header container.innerHTML = `

    Comments

    -

    Post a comment on Bluesky!

    + .pop()}" target="_blank">Reply on Bluesky

    `; // Add main post actions element @@ -193,7 +198,9 @@ const init = () => { fetchPost(); } // Priority 3: Show error message if no author configured - // Error message is already in the HTML from entry.html, no action needed + else if (container.dataset.error === "no-author") { + container.innerHTML += `

    Bluesky comments are not configured. Please set a Bluesky metadata field on this entry, or configure your Bluesky author handle in the plugin settings.

    `; + } }; init(); From 1a8570104736a7f3b72528cc9952f99f4a4eb778 Mon Sep 17 00:00:00 2001 From: David Merfield Date: Fri, 14 Nov 2025 13:00:40 +0100 Subject: [PATCH 5/9] Simplfy --- app/build/plugins/blueskyComments/public.css | 23 +++--- app/build/plugins/blueskyComments/public.js | 82 ++++++++++---------- 2 files changed, 51 insertions(+), 54 deletions(-) diff --git a/app/build/plugins/blueskyComments/public.css b/app/build/plugins/blueskyComments/public.css index ffd8ad4979a..23e4d814062 100644 --- a/app/build/plugins/blueskyComments/public.css +++ b/app/build/plugins/blueskyComments/public.css @@ -1,9 +1,9 @@ .comment-container { - display: flex; + display: flex; } .comment-container > a { - flex-shrink: 0; + flex-shrink: 0; } /* Post Actions */ @@ -13,32 +13,27 @@ color: var(--highlight2); } -.comment-container .post-actions, -#main-post-actions .post-actions { +.post-actions { display: flex; gap: 1em; } .post-actions svg { - width: 12px; - height: 12px; + width: 12px; + height: 12px; } -.comment-container .post-actions div svg, -.comment-container .post-actions div span, -#main-post-actions .post-actions div svg, -#main-post-actions .post-actions div span { +.post-actions div svg, +.post-actions div span { vertical-align: middle; display: inline-block; } -.comment-container .post-actions div:hover, -#main-post-actions .post-actions div:hover { +.post-actions div:hover { color: var(--accent); } -.comment-container .post-actions div:hover svg, -#main-post-actions .post-actions div:hover svg { +.post-actions div:hover svg { stroke: var(--accent); } diff --git a/app/build/plugins/blueskyComments/public.js b/app/build/plugins/blueskyComments/public.js index 36bab575ed4..86bc0baf502 100644 --- a/app/build/plugins/blueskyComments/public.js +++ b/app/build/plugins/blueskyComments/public.js @@ -18,6 +18,16 @@ const extractHandleFromProfileUrl = (profileUrl) => { throw new Error("Invalid Bluesky profile URL format."); }; +const getRkeyFromUri = (uri) => { + return uri.split("/").pop(); +}; + +const getPostUrl = (authorDid, rkey) => { + return `https://bsky.app/profile/${authorDid}/post/${rkey}`; +}; + +const sortByLikeCount = (a, b) => (b.post.likeCount || 0) - (a.post.likeCount || 0); + const renderActions = (post) => { return `
    @@ -38,12 +48,9 @@ const renderActions = (post) => { }; const renderComment = (post) => { - console.log(post); - const author = post.author; - const postUrl = `https://bsky.app/profile/${author.did}/post/${post.uri - .split("/") - .pop()}`; + const rkey = getRkeyFromUri(post.uri); + const postUrl = getPostUrl(author.did, rkey); return `
    @@ -65,7 +72,7 @@ const renderThread = (thread) => { // Handle replies: filter, sort, and limit to 3 const repliesHtml = thread.replies - .sort((a, b) => (b.post.likeCount || 0) - (a.post.likeCount || 0)) + .sort(sortByLikeCount) .slice(0, 3) .map((reply) => renderComment(reply.post)) .join("") || ""; @@ -78,6 +85,20 @@ const renderThread = (thread) => { `; }; +const renderCommentsHeader = (postUrl) => { + return ` +

    Comments

    +

    Reply on Bluesky

    + `; +}; + +const renderErrorMessage = (message) => { + return ` +

    Comments

    +

    ${message} Post on Bluesky to start the discussion!

    + `; +}; + const loadThread = (uri, container, originalUrl) => { fetch( `https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=${encodeURIComponent( @@ -92,9 +113,7 @@ const loadThread = (uri, container, originalUrl) => { if (!response.ok) throw new Error(await response.text()); const { thread } = await response.json(); - const replies = (thread.replies || []).sort( - (a, b) => (b.post.likeCount || 0) - (a.post.likeCount || 0) - ); + const replies = (thread.replies || []).sort(sortByLikeCount); // Add main post actions if element exists const mainPostActions = document.getElementById("main-post-actions"); @@ -109,15 +128,13 @@ const loadThread = (uri, container, originalUrl) => { if (replies.length > 25 && !container.querySelector("#see-more")) { const postUrl = originalUrl || - `https://bsky.app/profile/${thread.post.author.did}/post/${uri - .split("/") - .pop()}`; + getPostUrl(thread.post.author.did, getRkeyFromUri(uri)); container.innerHTML += `See more on Bluesky`; } }) .catch((error) => { console.error("Error loading thread:", error); - container.innerHTML = `

    Error loading comments. Post on Bluesky to start the discussion!

    `; + container.innerHTML = renderErrorMessage("Error loading comments."); }); }; @@ -140,7 +157,7 @@ const init = () => { ) { // Add loading message container.innerHTML += `

    Loading comments...

    `; - + const authorProfileUrl = container.dataset.author; const author = extractHandleFromProfileUrl(authorProfileUrl); const fetchPost = async () => { @@ -156,42 +173,27 @@ const init = () => { if (data.posts && data.posts.length > 0) { const post = data.posts[0]; const uri = post.uri; + const rkey = getRkeyFromUri(uri); + const postUrl = getPostUrl(post.author.did, rkey); // Update container with comments header - container.innerHTML = ` -

    Comments

    -

    Reply on Bluesky

    - `; + container.innerHTML = renderCommentsHeader(postUrl); // Add main post actions element - container.innerHTML += ``; + container.innerHTML += ``; // Load the thread - loadThread( - uri, - container, - `https://bsky.app/profile/${post.author.did}/post/${uri - .split("/") - .pop()}` - ); + loadThread(uri, container, postUrl); } else { - container.innerHTML = ` -

    Comments

    -

    No Bluesky post found for this page. Post on Bluesky to start the discussion!

    - `; + container.innerHTML = renderErrorMessage( + "No Bluesky post found for this page." + ); } } catch (err) { console.error("Error fetching post:", err); - container.innerHTML = ` -

    Comments

    -

    Error searching for Bluesky post. Post on Bluesky to start the discussion!

    - `; + container.innerHTML = renderErrorMessage( + "Error searching for Bluesky post." + ); } }; From 9ca93f35e3d88caac9135bf4207f7e830637a662 Mon Sep 17 00:00:00 2001 From: David Merfield Date: Fri, 14 Nov 2025 13:04:50 +0100 Subject: [PATCH 6/9] Simplify --- app/build/plugins/blueskyComments/entry.html | 55 +++++- app/build/plugins/blueskyComments/form.html | 2 +- app/build/plugins/blueskyComments/index.js | 3 + app/build/plugins/blueskyComments/public.js | 168 +++++++++++-------- 4 files changed, 157 insertions(+), 71 deletions(-) diff --git a/app/build/plugins/blueskyComments/entry.html b/app/build/plugins/blueskyComments/entry.html index 903105b4cfd..3ac129b4b07 100644 --- a/app/build/plugins/blueskyComments/entry.html +++ b/app/build/plugins/blueskyComments/entry.html @@ -4,6 +4,59 @@
    + {{^entry.metadata.bluesky}}{{#plugins.blueskyComments.options.authorURI}}data-author="{{plugins.blueskyComments.options.authorURI}}" data-auto-discover="true"{{/plugins.blueskyComments.options.authorURI}}{{^plugins.blueskyComments.options.authorURI}}data-error="no-author"{{/plugins.blueskyComments.options.authorURI}}{{/entry.metadata.bluesky}}>

    Comments

    + + + + + + + + + + + + + diff --git a/app/build/plugins/blueskyComments/form.html b/app/build/plugins/blueskyComments/form.html index 80f9d7d04f5..14ebf4a7e35 100644 --- a/app/build/plugins/blueskyComments/form.html +++ b/app/build/plugins/blueskyComments/form.html @@ -1,6 +1,6 @@

    Your Bluesky profile URL (e.g., https://bsky.app/profile/example.bsky.social). This will be used to automatically find your posts linking to each page when no Bluesky metadata is set on the entry.

    diff --git a/app/build/plugins/blueskyComments/index.js b/app/build/plugins/blueskyComments/index.js index a07ae36d5c4..4a4f6149adb 100644 --- a/app/build/plugins/blueskyComments/index.js +++ b/app/build/plugins/blueskyComments/index.js @@ -2,4 +2,7 @@ module.exports = { description: "Bluesky comments", category: "external", isDefault: false, + options: { + authorURI: "", + }, }; diff --git a/app/build/plugins/blueskyComments/public.js b/app/build/plugins/blueskyComments/public.js index 86bc0baf502..5130cfe4a93 100644 --- a/app/build/plugins/blueskyComments/public.js +++ b/app/build/plugins/blueskyComments/public.js @@ -9,12 +9,10 @@ const convertToAtUri = (url) => { }; const extractHandleFromProfileUrl = (profileUrl) => { - // Extract handle from profile URL (e.g., https://bsky.app/profile/example.bsky.social) const match = profileUrl.match(/https?:\/\/bsky\.app\/profile\/([^/?]+)/); if (match) { return match[1]; } - throw new Error("Invalid Bluesky profile URL format."); }; @@ -28,75 +26,86 @@ const getPostUrl = (authorDid, rkey) => { const sortByLikeCount = (a, b) => (b.post.likeCount || 0) - (a.post.likeCount || 0); -const renderActions = (post) => { - return ` -
    -
    - - ${post.likeCount} -
    -
    - - ${post.repostCount} -
    -
    - - ${post.replyCount} -
    -
    - `; +const getTemplate = (id) => { + const template = document.getElementById(id); + if (!template) throw new Error(`Template ${id} not found`); + return template.content.cloneNode(true); }; -const renderComment = (post) => { +const renderPostActions = (post) => { + const fragment = getTemplate("template-post-actions"); + fragment.querySelector("[data-like-count]").textContent = post.likeCount || 0; + fragment.querySelector("[data-repost-count]").textContent = post.repostCount || 0; + fragment.querySelector("[data-reply-count]").textContent = post.replyCount || 0; + return fragment; +}; + +const renderCommentContainer = (post) => { const author = post.author; const rkey = getRkeyFromUri(post.uri); const postUrl = getPostUrl(author.did, rkey); + + const fragment = getTemplate("template-comment-container"); + const links = fragment.querySelectorAll("[data-post-url]"); + links.forEach(link => { + link.href = postUrl; + }); + fragment.querySelector("[data-avatar]").src = author.avatar; + fragment.querySelector("[data-avatar]").alt = `${author.displayName}'s avatar`; + fragment.querySelector("[data-display-name]").textContent = author.displayName; + fragment.querySelector("[data-text]").textContent = post.record?.text || ""; + + // Add post actions + const actionsContainer = fragment.querySelector(".comment-container > div"); + actionsContainer.appendChild(renderPostActions(post)); + + return fragment; +}; - return ` -
    - - avatar - -
    - - ${author.displayName} - -

    ${post.record?.text}

    - ${renderActions(post)} -
    -
    - `; +const renderComment = (post) => { + const fragment = getTemplate("template-comment"); + const containerPlaceholder = fragment.querySelector("[data-comment-container]"); + const commentContainer = renderCommentContainer(post); + containerPlaceholder.replaceWith(commentContainer.querySelector(".comment-container")); + return fragment; }; const renderThread = (thread) => { - // Handle replies: filter, sort, and limit to 3 - const repliesHtml = - thread.replies - .sort(sortByLikeCount) - .slice(0, 3) - .map((reply) => renderComment(reply.post)) - .join("") || ""; - - return ` -
    - ${renderComment(thread.post)} - ${repliesHtml ? `
    ${repliesHtml}
    ` : ""} -
    - `; + const fragment = renderComment(thread.post); + const repliesContainer = fragment.querySelector("[data-replies]"); + + const replies = thread.replies + .sort(sortByLikeCount) + .slice(0, 3) + .map((reply) => renderComment(reply.post)); + + if (replies.length > 0) { + replies.forEach(reply => { + repliesContainer.appendChild(reply); + }); + } else { + repliesContainer.remove(); + } + + return fragment; }; const renderCommentsHeader = (postUrl) => { - return ` -

    Comments

    -

    Reply on Bluesky

    - `; + const fragment = getTemplate("template-comments-header"); + fragment.querySelector("[data-post-url]").href = postUrl; + return fragment; }; const renderErrorMessage = (message) => { - return ` -

    Comments

    -

    ${message} Post on Bluesky to start the discussion!

    - `; + const fragment = getTemplate("template-error-message"); + fragment.querySelector("[data-message]").textContent = message; + return fragment; +}; + +const renderReplyLink = (postUrl) => { + const fragment = getTemplate("template-reply-link"); + fragment.querySelector("[data-post-url]").href = postUrl; + return fragment; }; const loadThread = (uri, container, originalUrl) => { @@ -118,23 +127,27 @@ const loadThread = (uri, container, originalUrl) => { // Add main post actions if element exists const mainPostActions = document.getElementById("main-post-actions"); if (mainPostActions) { - mainPostActions.innerHTML = renderActions(thread.post); + mainPostActions.innerHTML = ""; + mainPostActions.appendChild(renderPostActions(thread.post)); } // Append top 25 comments - container.innerHTML += replies.slice(0, 25).map(renderThread).join(""); + replies.slice(0, 25).forEach(reply => { + container.appendChild(renderThread(reply)); + }); // Add "Show More" button if there are more comments if (replies.length > 25 && !container.querySelector("#see-more")) { const postUrl = originalUrl || getPostUrl(thread.post.author.did, getRkeyFromUri(uri)); - container.innerHTML += `See more on Bluesky`; + container.appendChild(renderReplyLink(postUrl)); } }) .catch((error) => { console.error("Error loading thread:", error); - container.innerHTML = renderErrorMessage("Error loading comments."); + container.innerHTML = ""; + container.appendChild(renderErrorMessage("Error loading comments.")); }); }; @@ -147,7 +160,13 @@ const init = () => { if (container.dataset.uri) { const uri = convertToAtUri(container.dataset.uri); // Add initial content for metadata case - container.innerHTML += `

    Reply on Bluesky

    `; + const replyLink = document.createElement("p"); + const link = document.createElement("a"); + link.href = container.dataset.uri; + link.target = "_blank"; + link.textContent = "Reply on Bluesky"; + replyLink.appendChild(link); + container.appendChild(replyLink); loadThread(uri, container, container.dataset.uri); } // Priority 2: Auto-discover using author search @@ -156,7 +175,9 @@ const init = () => { container.dataset.author ) { // Add loading message - container.innerHTML += `

    Loading comments...

    `; + const loadingMsg = document.createElement("p"); + loadingMsg.textContent = "Loading comments..."; + container.appendChild(loadingMsg); const authorProfileUrl = container.dataset.author; const author = extractHandleFromProfileUrl(authorProfileUrl); @@ -177,23 +198,30 @@ const init = () => { const postUrl = getPostUrl(post.author.did, rkey); // Update container with comments header - container.innerHTML = renderCommentsHeader(postUrl); + container.innerHTML = ""; + container.appendChild(renderCommentsHeader(postUrl)); // Add main post actions element - container.innerHTML += ``; + const mainPostActions = document.createElement("a"); + mainPostActions.id = "main-post-actions"; + mainPostActions.target = "_blank"; + mainPostActions.href = postUrl; + container.appendChild(mainPostActions); // Load the thread loadThread(uri, container, postUrl); } else { - container.innerHTML = renderErrorMessage( + container.innerHTML = ""; + container.appendChild(renderErrorMessage( "No Bluesky post found for this page." - ); + )); } } catch (err) { console.error("Error fetching post:", err); - container.innerHTML = renderErrorMessage( + container.innerHTML = ""; + container.appendChild(renderErrorMessage( "Error searching for Bluesky post." - ); + )); } }; @@ -201,7 +229,9 @@ const init = () => { } // Priority 3: Show error message if no author configured else if (container.dataset.error === "no-author") { - container.innerHTML += `

    Bluesky comments are not configured. Please set a Bluesky metadata field on this entry, or configure your Bluesky author handle in the plugin settings.

    `; + const errorMsg = document.createElement("p"); + errorMsg.innerHTML = `Bluesky comments are not configured. Please set a Bluesky metadata field on this entry, or configure your Bluesky author handle in the plugin settings.`; + container.appendChild(errorMsg); } }; From 6db6dec6444cea50d1f6af9a528357c556e4100a Mon Sep 17 00:00:00 2001 From: David Merfield Date: Fri, 14 Nov 2025 13:22:01 +0100 Subject: [PATCH 7/9] Simplify --- app/build/plugins/blueskyComments/entry.html | 8 ++---- app/build/plugins/blueskyComments/public.js | 27 ++++++++++---------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/app/build/plugins/blueskyComments/entry.html b/app/build/plugins/blueskyComments/entry.html index 3ac129b4b07..bb4c622eeb6 100644 --- a/app/build/plugins/blueskyComments/entry.html +++ b/app/build/plugins/blueskyComments/entry.html @@ -1,10 +1,6 @@ -{{#entry.metadata.bluesky}} - -{{/entry.metadata.bluesky}} -
    + data-uri="{{{entry.metadata.bluesky}}}" + data-author="{{plugins.blueskyComments.options.authorURI}}">

    Comments

    diff --git a/app/build/plugins/blueskyComments/public.js b/app/build/plugins/blueskyComments/public.js index 5130cfe4a93..d7c8def0882 100644 --- a/app/build/plugins/blueskyComments/public.js +++ b/app/build/plugins/blueskyComments/public.js @@ -156,31 +156,30 @@ const init = () => { if (!container) return; - // Priority 1: Use Bluesky metadata if set - if (container.dataset.uri) { - const uri = convertToAtUri(container.dataset.uri); + const dataUri = container.dataset.uri?.trim(); + const dataAuthor = container.dataset.author?.trim(); + + // Priority 1: Use Bluesky metadata if set (non-empty URL) + if (dataUri) { + const uri = convertToAtUri(dataUri); // Add initial content for metadata case const replyLink = document.createElement("p"); const link = document.createElement("a"); - link.href = container.dataset.uri; + link.href = dataUri; link.target = "_blank"; link.textContent = "Reply on Bluesky"; replyLink.appendChild(link); container.appendChild(replyLink); - loadThread(uri, container, container.dataset.uri); + loadThread(uri, container, dataUri); } - // Priority 2: Auto-discover using author search - else if ( - container.dataset.autoDiscover === "true" && - container.dataset.author - ) { + // Priority 2: Auto-discover using author search (non-empty URL) + else if (dataAuthor) { // Add loading message const loadingMsg = document.createElement("p"); loadingMsg.textContent = "Loading comments..."; container.appendChild(loadingMsg); - const authorProfileUrl = container.dataset.author; - const author = extractHandleFromProfileUrl(authorProfileUrl); + const author = extractHandleFromProfileUrl(dataAuthor); const fetchPost = async () => { const currentUrl = window.location.href; const apiUrl = `https://public.api.bsky.app/xrpc/app.bsky.feed.searchPosts?q=*&url=${encodeURIComponent( @@ -227,8 +226,8 @@ const init = () => { fetchPost(); } - // Priority 3: Show error message if no author configured - else if (container.dataset.error === "no-author") { + // Priority 3: Show error message if neither is configured + else { const errorMsg = document.createElement("p"); errorMsg.innerHTML = `Bluesky comments are not configured. Please set a Bluesky metadata field on this entry, or configure your Bluesky author handle in the plugin settings.`; container.appendChild(errorMsg); From ada659b8018e2414107d9bb0f8f487b1078999d1 Mon Sep 17 00:00:00 2001 From: David Merfield Date: Sat, 15 Nov 2025 16:40:26 +0100 Subject: [PATCH 8/9] x --- app/blog/tests/pluginHTML.js | 46 ------------------------------------ 1 file changed, 46 deletions(-) diff --git a/app/blog/tests/pluginHTML.js b/app/blog/tests/pluginHTML.js index 465f1acc5ba..59363fe1c92 100644 --- a/app/blog/tests/pluginHTML.js +++ b/app/blog/tests/pluginHTML.js @@ -81,52 +81,6 @@ describe("pluginHTML", function () { expect(await areThereComments('/foo')).toBe(false, 'comments should not appear on posts with comments disabled'); }); - it("injects bluesky comments html", async function () { - - const blueskyUriPost = 'at://did:plc:example/app.bsky.feed.post/3jqf4zda6q2'; - const blueskyUriPage = 'at://did:plc:example/app.bsky.feed.post/6p9z7m8n1v0'; - - const plugins = { - ...this.blog.plugins, - blueskyComments: { - enabled: true, - options: { - authorHandle: 'example.bsky.social', - service: 'https://public.api.bsky.app' - } - } - }; - - await this.blog.update({plugins}) - - await this.template({ "entry.html": "{{{entry.html}}} {{> pluginHTML}}" }); - await this.write({path: '/a.txt', content: `Link: /foo\nBluesky: ${blueskyUriPost}\n\nHello, world!`}); - await this.write({path: '/Pages/about.txt', content: `Link: /about\nComments: Yes\nBluesky: ${blueskyUriPage}\n\nHello, page!`}); - await this.write({path: '/Drafts/test.txt', content: 'Hello, draft!'}); - - const areThereComments = async (path) => { - const res = await this.get(path); - const body = await res.text(); - return body.includes('id="bluesky-comments"') && body.includes('bluesky-comments.es.js'); - } - - expect(await areThereComments('/foo')).toBe(true, 'comments should appear on posts with Bluesky metadata'); - expect(await areThereComments('/about')).toBe(true, 'comments should appear on pages with comments enabled and Bluesky metadata'); - expect(await areThereComments('/draft/view/Drafts/test.txt')).toBe(false, 'comments should not appear on drafts'); - - const res = await this.fetch(config.protocol + 'preview-of-my-local-on-' + this.blog.handle + '.' + config.host + '/foo'); - const body = await res.text(); - - expect(res.status).toEqual(200); - expect(body).not.toContain('bluesky-comments.es.js'); - - await this.write({path: '/a.txt', content: `Link: /foo\nComments: No\nBluesky: ${blueskyUriPost}\n\nHello, world!`}); - expect(await areThereComments('/foo')).toBe(false, 'comments should not appear on posts with comments disabled'); - - await this.write({path: '/a.txt', content: 'Link: /foo\n\nHello, world!'}); - expect(await areThereComments('/foo')).toBe(false, 'comments should not appear when Bluesky metadata is missing'); - }); - it("injects google analytics into appJS", async function () { const plugins = {...this.blog.plugins, analytics: {enabled: true, options: {provider: {Google: true}, trackingID: 'UA-12345678-9'}}}; From 83c4d29117b2097da2f1020a51c85b6d47b88ae6 Mon Sep 17 00:00:00 2001 From: David Merfield Date: Sat, 15 Nov 2025 17:46:43 +0100 Subject: [PATCH 9/9] x --- app/views/tools/comments/index.html | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/views/tools/comments/index.html b/app/views/tools/comments/index.html index fae8f0e8120..23e5ab9f2aa 100644 --- a/app/views/tools/comments/index.html +++ b/app/views/tools/comments/index.html @@ -1,6 +1 @@ -

    Let your readers comment.

    - - +

    Let your readers comment.

    \ No newline at end of file