Last active
February 12, 2026 23:44
-
-
Save tiagovercosa/9c52869e09e06a0352ed05e84ee388e0 to your computer and use it in GitHub Desktop.
JavaScript code necessary to retrieve comments from Mastodon and Bluesky servers for a static Hugo blog.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| {{ if or .Params.mastodon_id .Params.bluesky_id }} | |
| <section class="social-comments hx:mt-12 hx:pt-6 hx:border-t hx:border-gray-200 dark:hx:border-neutral-800"> | |
| <h2 class="hx:text-2xl hx:font-bold hx:mb-6">Comentários</h2> | |
| <div id="comments-loading" class="hx:text-sm hx:text-gray-500 hx:mb-4 hx:animate-pulse"> | |
| Carregando interações... | |
| </div> | |
| {{ if .Params.mastodon_id }} | |
| <div id="mastodon-comments" class="comment-section hx:mb-8" style="display:none;"> | |
| <h3 class="hx:font-semibold hx:mb-2 hx:flex hx:items-center hx:gap-2"> | |
| {{ partial "utils/icon.html" (dict "name" "mastodon" "attributes" "height=1.2em") }} | |
| No Mastodon | |
| </h3> | |
| <p class="hx:text-sm hx:mb-4 hx:text-gray-600 dark:hx:text-gray-400"> | |
| Responda a <a href="https://{{ site.Params.social_comments.mastodon.host }}/@{{ site.Params.social_comments.mastodon.username }}/{{ .Params.mastodon_id }}" target="_blank" class="hx:underline">este post</a> para aparecer aqui. | |
| </p> | |
| <div id="mastodon-replies" class="hx:space-y-4"></div> | |
| </div> | |
| {{ end }} | |
| {{ if .Params.bluesky_id }} | |
| <div id="bluesky-comments" class="comment-section" style="display:none;"> | |
| <h3 class="hx:font-semibold hx:mb-2 hx:flex hx:items-center hx:gap-2"> | |
| {{ partial "utils/icon.html" (dict "name" "bluesky" "attributes" "height=1.2em") }} | |
| No Bluesky | |
| </h3> | |
| <p class="hx:text-sm hx:mb-4 hx:text-gray-600 dark:hx:text-gray-400"> | |
| Responda a <a href="https://bsky.app/profile/{{ site.Params.social_comments.bluesky.did }}/post/{{ .Params.bluesky_id }}" target="_blank" class="hx:underline">este post</a> para aparecer aqui. | |
| </p> | |
| <div id="bluesky-replies" class="hx:space-y-4"></div> | |
| </div> | |
| {{ end }} | |
| </section> | |
| <script> | |
| (function() { | |
| const escapeHtml = (unsafe) => { | |
| return unsafe | |
| .replace(/&/g, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">") | |
| .replace(/"/g, """) | |
| .replace(/'/g, "'"); | |
| } | |
| // Função de renderização unificada para o estilo minimalista | |
| const renderCommentHTML = (account, content, date, depth) => { | |
| const div = document.createElement('div'); | |
| div.className = "hx:p-4 hx:rounded-r-lg hx:rounded-l-none hx:bg-gray-50 dark:hx:bg-neutral-900/50 hx:mb-2"; | |
| if (depth > 0) { | |
| const indentSize = Math.min(depth, 5) * 1.2; | |
| div.style.marginLeft = `${indentSize}rem`; | |
| div.style.borderLeft = "2px solid #d1d5db"; // Linha clara | |
| if (document.documentElement.classList.contains('dark')) { | |
| div.style.borderLeft = "2px solid #333333"; // Linha escura | |
| } | |
| } else { | |
| div.classList.add('hx:rounded-l-lg'); | |
| } | |
| div.innerHTML = ` | |
| <div class="hx:flex hx:items-center hx:gap-2 hx:mb-2"> | |
| <img src="${escapeHtml(account.avatar)}" class="hx:w-8 hx:h-8 hx:rounded-full" style="width: 32px !important; height: 32px !important; min-width: 32px;" loading="lazy"> | |
| <a href="${account.url}" target="_blank" class="hx:font-medium hx:text-sm hx:hover:underline"> | |
| ${escapeHtml(account.displayName || account.username)} | |
| </a> | |
| <span class="hx:text-xs hx:text-gray-500">${new Date(date).toLocaleDateString()}</span> | |
| </div> | |
| <div class="hx:text-sm prose dark:prose-invert max-w-none"> | |
| ${content} | |
| </div> | |
| `; | |
| return div; | |
| }; | |
| const loadComments = async () => { | |
| const loading = document.getElementById('comments-loading'); | |
| // --- MASTODON --- | |
| {{ if .Params.mastodon_id }} | |
| try { | |
| const host = "{{ site.Params.social_comments.mastodon.host }}"; | |
| const id = "{{ .Params.mastodon_id }}"; | |
| const container = document.getElementById('mastodon-replies'); | |
| const section = document.getElementById('mastodon-comments'); | |
| const response = await fetch(`https://${host}/api/v1/statuses/${id}/context`); | |
| const data = await response.json(); | |
| if(data.descendants && data.descendants.length > 0) { | |
| section.style.display = 'block'; | |
| container.innerHTML = ''; | |
| const replyMap = {}; | |
| data.descendants.forEach(reply => { | |
| const parentId = reply.in_reply_to_id; | |
| if (!replyMap[parentId]) replyMap[parentId] = []; | |
| replyMap[parentId].push(reply); | |
| }); | |
| const renderMastodonTree = (parentId, depth) => { | |
| const replies = replyMap[parentId] || []; | |
| replies.forEach(reply => { | |
| if(reply.visibility !== 'public') return; | |
| // Adaptação dos dados do Mastodon para o renderizador padrão | |
| const accountData = { | |
| avatar: reply.account.avatar, | |
| url: reply.account.url, | |
| displayName: reply.account.display_name, | |
| username: reply.account.username | |
| }; | |
| const element = renderCommentHTML(accountData, reply.content, reply.created_at, depth); | |
| container.appendChild(element); | |
| renderMastodonTree(reply.id, depth + 1); | |
| }); | |
| }; | |
| renderMastodonTree(id, 0); | |
| } else { | |
| section.style.display = 'block'; | |
| container.innerHTML = '<p class="hx:text-sm hx:italic hx:text-gray-500">Sem comentários ainda.</p>'; | |
| } | |
| } catch (e) { console.warn(e); } | |
| {{ end }} | |
| // --- BLUESKY --- | |
| {{ if .Params.bluesky_id }} | |
| try { | |
| const did = "{{ site.Params.social_comments.bluesky.did }}"; | |
| const postId = "{{ .Params.bluesky_id }}"; | |
| const container = document.getElementById('bluesky-replies'); | |
| const section = document.getElementById('bluesky-comments'); | |
| // CORREÇÃO: Aumentamos o depth para 10 para pegar respostas das respostas | |
| const atUri = `at://${did}/app.bsky.feed.post/${postId}`; | |
| const apiUrl = `https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=${encodeURIComponent(atUri)}&depth=10`; | |
| const response = await fetch(apiUrl); | |
| const data = await response.json(); | |
| if (data.thread && data.thread.replies && data.thread.replies.length > 0) { | |
| section.style.display = 'block'; | |
| container.innerHTML = ''; | |
| // Ordenar comentários de topo por likes | |
| const topLevelReplies = data.thread.replies.sort((a, b) => (b.post.likeCount || 0) - (a.post.likeCount || 0)); | |
| // Função recursiva para percorrer a árvore do Bluesky | |
| const renderBlueskyTree = (replies, depth) => { | |
| if (!replies) return; | |
| replies.forEach(thread => { | |
| if(!thread.post) return; | |
| const author = thread.post.author; | |
| const record = thread.post.record; | |
| // Prepara dados para o renderizador | |
| const accountData = { | |
| avatar: author.avatar, | |
| url: `https://bsky.app/profile/${author.handle}`, | |
| displayName: author.displayName, | |
| username: author.handle | |
| }; | |
| // O texto no Bluesky vem plano, envolvemos em <p> para manter padrão visual | |
| const contentHTML = `<p>${escapeHtml(record.text)}</p>`; | |
| const element = renderCommentHTML(accountData, contentHTML, record.createdAt, depth); | |
| container.appendChild(element); | |
| // Se houver respostas dentro desta resposta, chamamos a função novamente (cascata) | |
| if (thread.replies) { | |
| // Opcional: ordenar as respostas filhas por data (antigas primeiro) ou likes | |
| const sortedReplies = thread.replies.sort((a, b) => new Date(a.post.record.createdAt) - new Date(b.post.record.createdAt)); | |
| renderBlueskyTree(sortedReplies, depth + 1); | |
| } | |
| }); | |
| } | |
| renderBlueskyTree(topLevelReplies, 0); | |
| } else { | |
| section.style.display = 'block'; | |
| container.innerHTML = '<p class="hx:text-sm hx:italic hx:text-gray-500">Sem comentários ainda.</p>'; | |
| } | |
| } catch (e) { console.warn(e); } | |
| {{ end }} | |
| if(loading) loading.style.display = 'none'; | |
| }; | |
| loadComments(); | |
| })(); | |
| </script> | |
| {{ end }} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment