From 39a5cb4a9bc1dd41a2823997a68b077565614fff Mon Sep 17 00:00:00 2001 From: grassblock Date: Mon, 21 Jul 2025 23:14:16 +0800 Subject: [PATCH 1/8] feat: init 'oom' comment provider --- src/assets/css/oom-styles.css | 93 +++++++ src/assets/js/oom-comments.js | 312 +++++++++++++++++++++++ src/components/Comments.astro | 10 +- src/components/helper/comments/OOM.astro | 14 + src/config.ts | 2 +- 5 files changed, 429 insertions(+), 2 deletions(-) create mode 100644 src/assets/css/oom-styles.css create mode 100644 src/assets/js/oom-comments.js create mode 100644 src/components/helper/comments/OOM.astro diff --git a/src/assets/css/oom-styles.css b/src/assets/css/oom-styles.css new file mode 100644 index 0000000..c4cffdd --- /dev/null +++ b/src/assets/css/oom-styles.css @@ -0,0 +1,93 @@ +/* https://github.com/oom-components/mastodon-comments, under MIT License */ +oom-comments { + display: block; + padding: 2em; +} +oom-comments ul { + list-style: none; + margin: 0; + padding: 0; +} +oom-comments li { + margin: 32px 0; +} +oom-comments article { + max-width: 600px; +} +oom-comments ul ul { + margin-left: 64px; +} +oom-comments .comment-avatar { + width: 50px; + height: 50px; + border-radius: 6px; + float: left; + margin-right: 14px; + box-shadow: 0 0 1px #0009; +} +oom-comments .comment-user { + color: currentColor; + text-decoration: none; + display: block; + position: relative; +} +oom-comments .comment-author { + position: absolute; + left: 35px; + top: 35px; + background: white; + border-radius: 50%; + width: 20px; + height: 20px; + color: gray; +} +oom-comments .comment-user:hover .comment-username { + text-decoration: underline; +} +oom-comments .comment-username { + margin-right: 0.5em; +} +oom-comments .comment-useraddress { + color: gray; + font-size: small; + font-style: normal; +} +oom-comments .comment-time { + font-size: small; + display: flex; + align-items: center; + column-gap: 0.4em; +} +oom-comments .comment-time svg { + width: 1em; + height: 1em; + fill: gray; +} +oom-comments .comment-address { + color: currentColor; + text-decoration: none; + display: block; + margin-top: 0.25em; +} +oom-comments .comment-address:hover { + text-decoration: underline; +} +oom-comments .comment-body { + margin-top: 0.5em; + margin-left: 64px; + line-height: 1.5; +} +oom-comments .comment-body p { + margin: 0.5em 0; +} +oom-comments .comment-counts { + display: flex; + column-gap: 1em; + font-size: small; +} +oom-comments .comment-counts > span { + display: flex; + align-items: center; + column-gap: 0.3em; + color: gray; +} diff --git a/src/assets/js/oom-comments.js b/src/assets/js/oom-comments.js new file mode 100644 index 0000000..404a483 --- /dev/null +++ b/src/assets/js/oom-comments.js @@ -0,0 +1,312 @@ +/* https://github.com/oom-components/mastodon-comments, under MIT License */ +// © https://phosphoricons.com/ +export const icons = { + reblog: + ``, + favourite: + ``, + author: + ``, + + // @ https://simpleicons.org/ + mastodon: + `Mastodon`, + + pleroma: + `Pleroma`, + + bluesky: + `Bluesky`, +}; + +export default class SocialComments extends HTMLElement { + comments = {}; + + async connectedCallback() { + const lang = this.closest("[lang]")?.lang || navigator.language || "en"; + + this.dateTimeFormatter = new Intl.DateTimeFormat(lang, { + dateStyle: "medium", + timeStyle: "short", + }); + + const mastodon = this.getAttribute("mastodon") || this.getAttribute("src"); + const bluesky = this.getAttribute("bluesky"); + + await Promise.all([ + mastodon && this.#fetchMastodon(new URL(mastodon)), + bluesky && this.#fetchBluesky(new URL(bluesky)), + ]); + + this.refresh(); + } + + refresh() { + const comments = [ + ...this.comments.mastodon || [], + ...this.comments.bluesky || [], + ].sort( + (a, b) => new Date(a.createdAt) - new Date(b.createdAt), + ); + + if (comments.length) { + this.innerHTML = ""; + this.render(this, comments); + } + } + + async #fetchBluesky(url) { + const { pathname } = url; + + const [, handle, rkey] = pathname.match( + /\/profile\/([\w\.]+)\/post\/(\w+)/, + ); + + if (!handle || !rkey) { + return; + } + + const options = { + ttl: Number(this.getAttribute("cache") || 0), + }; + + const didData = await fetchJSON( + `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${handle}`, + options, + ); + const uri = `at://${didData.did}/app.bsky.feed.post/${rkey}`; + + this.comments.bluesky = dataFromBluesky( + await fetchJSON( + `https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=${uri}`, + options, + ), + ); + } + + async #fetchMastodon(url) { + const { origin, pathname } = url; + let id; + + const source = pathname.includes("/notice/") ? "pleroma" : "mastodon"; + + if (source === "pleroma") { + [, id] = pathname.match(/^\/notice\/([^\/?#]+)/); + } else { + [, id] = pathname.match(/\/(\d+)$/); + } + + if (!id) { + return; + } + + const token = this.getAttribute("token"); + const options = { + ttl: Number(this.getAttribute("cache") || 0), + }; + if (token) { + options.headers = { + Authorization: `Bearer ${token}`, + }; + } + + const user = url.pathname.split("/")[1]; + const author = `${user}@${url.hostname}`; + + const comments = dataFromMastodon( + await fetchJSON( + new URL(`${origin}/api/v1/statuses/${id}/context`), + options, + ), + author, + source, + ); + + this.comments.mastodon = comments.filter((comment) => + comment.parent === id + ); + } + + render(container, replies) { + const ul = document.createElement("ul"); + + for (const reply of replies) { + const comment = document.createElement("li"); + comment.innerHTML = this.renderComment(reply); + + if (reply.replies.length) { + this.render(comment, reply.replies); + } + ul.appendChild(comment); + } + + container.appendChild(ul); + } + + renderComment(comment) { + return ` +
+ +
+ ${comment.content} + +

+ ${ + comment.boosts ? `${icons.reblog} ${comment.boosts}` : "" + } + ${ + comment.likes ? `${icons.favourite} ${comment.likes}` : "" + } +

+
+
+ `; + } +} + +function formatEmojis(html, emojis) { + emojis.forEach(({ shortcode, static_url, url }) => { + html = html.replace( + `:${shortcode}:`, + ` + + :${shortcode}: + `, + ); + }); + return html; +} + +async function fetchJSON(url, options = {}) { + const headers = new Headers(); + + if (options.headers) { + for (const [key, value] of Object.entries(options.headers)) { + headers.set(key, value); + } + } + + if (typeof caches === "undefined") { + return await (await fetch(url), { headers }).json(); + } + + const cache = await caches.open("mastodon-comments"); + let cached = await cache.match(url); + + if (cached && options.ttl) { + const cacheTime = new Date(cached.headers.get("x-cached-at")); + const diff = Date.now() - cacheTime.getTime(); + + if (diff <= options.ttl * 1000) { + return await cached.json(); + } + } + + try { + const response = await fetch(url, { headers }); + const body = await response.json(); + + cached = new Response(JSON.stringify(body)); + cached.headers.set("x-cached-at", new Date()); + cached.headers.set("content-type", "application/json; charset=utf-8"); + await cache.put(url, cached); + return body; + } catch { + if (cached) { + return await cached.json(); + } + } +} + +function dataFromMastodon(data, author, source) { + const comments = new Map(); + + // Transform data to a more usable format + for (const comment of data.descendants) { + if (comment.visibility !== "public") { + continue; + } + + const { account } = comment; + const handler = `@${account.username}@${new URL(account.url).hostname}`; + comments.set(comment.id, { + id: comment.id, + isMine: author === handler, + source, + url: comment.url, + parent: comment.in_reply_to_id, + createdAt: new Date(comment.created_at), + content: formatEmojis(comment.content, comment.emojis), + author: { + name: formatEmojis(account.display_name, account.emojis), + handler, + url: account.url, + avatar: account.avatar_static, + alt: account.display_name, + }, + boosts: comment.reblogs_count, + likes: comment.favourites_count, + replies: [], + }); + } + + // Group comments by parent + for (const comment of comments.values()) { + if (comment.parent && comments.has(comment.parent)) { + comments.get(comment.parent).replies.push(comment); + } + } + + return Array.from(comments.values()); +} + +function dataFromBluesky(data) { + const { thread } = data; + + return blueskyComments( + thread.post.author.did, + thread.post.cid, + thread.replies, + ); +} + +function blueskyComments(author, parent, comments) { + return comments.map((reply) => { + const { post, replies } = reply; + const rkey = post.uri.split("/").pop(); + return { + id: post.cid, + isMine: post.author.did === author, + source: "bluesky", + url: `https://bsky.app/profile/${post.author.handle}/post/${rkey}`, + parent, + createdAt: new Date(post.record.createdAt), + content: post.record.text, + author: { + name: post.author.displayName, + handler: post.author.handle, + url: `https://bsky.app/profile/${post.author.handle}`, + avatar: post.author.avatar, + alt: post.author.displayName, + }, + boosts: post.repostCount, + likes: post.likeCount, + replies: blueskyComments(author, post.cid, replies || []), + }; + }); +} diff --git a/src/components/Comments.astro b/src/components/Comments.astro index e6e6bd4..c0429bc 100644 --- a/src/components/Comments.astro +++ b/src/components/Comments.astro @@ -4,15 +4,23 @@ import FediverseComments from "./helper/comments/Fediverse.astro"; import HatsuComments from "./helper/comments/Hatsu.astro"; import Artalk from "./helper/comments/Artalk.astro"; import Giscus from "./helper/comments/Giscus.astro"; +import OomComments from "./helper/comments/OOM.astro"; const method = siteConfig.comments.type const FediverseConfig = siteConfig.comments.fediverse +interface Props { + mastodonLink?: string; + bskyLink?: string; +} + +const { mastodonLink, bskyLink } = Astro.props; --- {method === 'artalk' && } {method === 'giscus' && } {(method === 'fediverse' && !FediverseConfig.renderOnServer ) && } {(method === 'fediverse' && FediverseConfig.renderOnServer ) &&

Loading comments...

} -{method === 'hatsu' && } \ No newline at end of file +{method === 'hatsu' && } +{method === 'oom' && } \ No newline at end of file diff --git a/src/components/helper/comments/OOM.astro b/src/components/helper/comments/OOM.astro new file mode 100644 index 0000000..c410a64 --- /dev/null +++ b/src/components/helper/comments/OOM.astro @@ -0,0 +1,14 @@ +--- +import Comments from "../../../assets/js/oom-comments.js"; +import "../../../assets/css/oom-styles.css"; + +//Register the custom element with your desired name +customElements.define("oom-comments", Comments); +const { mastodonLink, bskyLink } = Astro.props; +--- + + No comments yet + \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index c8cd0eb..666a0bd 100644 --- a/src/config.ts +++ b/src/config.ts @@ -44,7 +44,7 @@ export const siteConfig = { contentPassword: 'p1easeChangeMe!', // comments comments: { - type: 'artalk', // 'artalk','giscus','fediverse','email','hatsu' + type: 'artalk', // 'artalk','giscus','fediverse','email','hatsu','oom' artalk: { instanceDomain: '', // the domain of your artalk instance }, From d8bf0e69a0a94efd13111251fa5d303faa8580b1 Mon Sep 17 00:00:00 2001 From: grassblock Date: Tue, 22 Jul 2025 10:47:22 +0800 Subject: [PATCH 2/8] fix: HTMLElement is not defined --- src/components/helper/comments/OOM.astro | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/helper/comments/OOM.astro b/src/components/helper/comments/OOM.astro index c410a64..cca1f37 100644 --- a/src/components/helper/comments/OOM.astro +++ b/src/components/helper/comments/OOM.astro @@ -1,9 +1,5 @@ --- -import Comments from "../../../assets/js/oom-comments.js"; import "../../../assets/css/oom-styles.css"; - -//Register the custom element with your desired name -customElements.define("oom-comments", Comments); const { mastodonLink, bskyLink } = Astro.props; --- No comments yet - \ No newline at end of file + + \ No newline at end of file From 3401ad19a4643b0782e1ef02a18db5f2dd0e9f3d Mon Sep 17 00:00:00 2001 From: grassblock Date: Tue, 22 Jul 2025 11:01:13 +0800 Subject: [PATCH 3/8] feat: add option to open navbar links in new tab --- src/components/Navbar.astro | 2 +- src/config.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Navbar.astro b/src/components/Navbar.astro index 7bcb4bc..0780ee9 100644 --- a/src/components/Navbar.astro +++ b/src/components/Navbar.astro @@ -5,5 +5,5 @@ const navBarItems = siteConfig.navBarItems \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index 666a0bd..7184d62 100644 --- a/src/config.ts +++ b/src/config.ts @@ -20,9 +20,9 @@ export const siteConfig = { navBarItems: [ // additional items in the navbar // the order of the items will be the same as the order in the array - // format is { text: string, link: string } - { text: "RSS", link: "/rss.xml" }, - { text: "GitHub", link: "https://github.com/GrassBlock1/mercury" }, + // format is { text: string, link: string, openInNewTab?: boolean (default: true) } + { text: "RSS", link: "/rss.xml", openInNewTab: true }, + { text: "GitHub", link: "https://github.com/GrassBlock1/mercury", openInNewTab: false }, ], // search // This only works when noClientJavaScript is enabled From 7700e0aaf53b7f6923105452523847ef1f34a2f0 Mon Sep 17 00:00:00 2001 From: grassblock Date: Tue, 22 Jul 2025 12:49:16 +0800 Subject: [PATCH 4/8] fix: color theme switch when spa mode is on --- src/components/ThemeSwitcher.astro | 46 ++++++++++++++++-------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/src/components/ThemeSwitcher.astro b/src/components/ThemeSwitcher.astro index 78a8c17..df8b8ab 100644 --- a/src/components/ThemeSwitcher.astro +++ b/src/components/ThemeSwitcher.astro @@ -50,25 +50,19 @@ \ No newline at end of file From 276ee70e71ce440fe4e3a7b5b0591302a1da3caf Mon Sep 17 00:00:00 2001 From: grassblock Date: Tue, 22 Jul 2025 13:39:23 +0800 Subject: [PATCH 5/8] feat: better styling for ToCs --- src/components/TableOfContents.astro | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/TableOfContents.astro b/src/components/TableOfContents.astro index 6e91d7c..4345944 100644 --- a/src/components/TableOfContents.astro +++ b/src/components/TableOfContents.astro @@ -73,6 +73,9 @@ const toc = buildHierarchy(filteredHeadings); From 98d23e7c94e8e2708f2044a5d2345288adcc5d74 Mon Sep 17 00:00:00 2001 From: grassblock Date: Tue, 22 Jul 2025 18:03:12 +0800 Subject: [PATCH 8/8] feat: multi-authors within one article support --- src/components/helper/authors/Info.astro | 7 +++---- src/content/posts/_schemas.ts | 2 +- src/content/posts/terminal-setup/index.md | 3 +++ src/pages/blog/[...slug].astro | 14 +++++++------- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/components/helper/authors/Info.astro b/src/components/helper/authors/Info.astro index ed37cb0..4155f11 100644 --- a/src/components/helper/authors/Info.astro +++ b/src/components/helper/authors/Info.astro @@ -1,18 +1,17 @@ --- import {Image} from "astro:assets"; -import {getEntry} from "astro:content"; import {siteConfig} from "../../../config"; const { data } = Astro.props; // Get author data -const authorAvatar = data?.data.mcplayerid ? `/images/avatars/${data.id}.png` : null; -const authorName = data ? data.data.name : null; +const authorAvatar = data.mcplayerid ? `/images/avatars/${data.mcplayerid}.png` : null; +const authorName = data.name ? data.name : null; --- {(siteConfig.displayAvatar && data) && <> {authorAvatar && {`avatar} - {authorName} @ + {authorName} | }