Compare commits
4 commits
e59c84bb2f
...
f571670e13
Author | SHA1 | Date | |
---|---|---|---|
|
f571670e13 | ||
|
c09bb95eaa | ||
|
17f095239c | ||
|
79f5fcf947 |
5 changed files with 356 additions and 74 deletions
51
src/components/Comments.astro
Normal file
51
src/components/Comments.astro
Normal file
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
import {siteConfig} from "../config";
|
||||
import FediverseComments from "./helper/comments/Fediverse.astro";
|
||||
|
||||
const method = siteConfig.comments.type
|
||||
const ArtalkConfig = siteConfig.comments.artalk
|
||||
const giscusConfig = siteConfig.comments.giscus
|
||||
interface Props {
|
||||
path?: string;
|
||||
}
|
||||
|
||||
let { path='/' } = Astro.props;
|
||||
---
|
||||
{method === 'artalk' &&
|
||||
<div>
|
||||
<!-- CSS -->
|
||||
<link href={`https://${ArtalkConfig.instanceDomain}/dist/Artalk.css`} rel="stylesheet">
|
||||
|
||||
<!-- JS -->
|
||||
<script src={`https://${ArtalkConfig.instanceDomain}/dist/Artalk.js`}></script>
|
||||
|
||||
<!-- Artalk -->
|
||||
<div id="Comments"></div>
|
||||
<script define:vars={{ instanceDomain: ArtalkConfig.instanceDomain, pagePath: path }}>
|
||||
Artalk.init({
|
||||
el: '#Comments',
|
||||
pageKey: pagePath, // Using the passed variable
|
||||
server: `https://${instanceDomain}`, // Using the passed variable
|
||||
})
|
||||
</script>
|
||||
</div>
|
||||
}
|
||||
{method === 'giscus' && (
|
||||
<script
|
||||
src="https://giscus.app/client.js"
|
||||
data-repo={giscusConfig.repo}
|
||||
data-repo-id={giscusConfig.repoId}
|
||||
data-category={giscusConfig.category}
|
||||
data-category-id={giscusConfig.categoryId}
|
||||
data-mapping={giscusConfig.mapping}
|
||||
data-strict={giscusConfig.strict}
|
||||
data-reactions-enabled={giscusConfig.reactionsEnabled}
|
||||
data-emit-metadata={giscusConfig.emitMetadata}
|
||||
data-input-position={giscusConfig.inputPosition}
|
||||
data-theme={giscusConfig.theme}
|
||||
data-lang={giscusConfig.lang}
|
||||
crossorigin="anonymous"
|
||||
async
|
||||
></script>
|
||||
)}
|
||||
{method === 'fediverse' && <FediverseComments path={path} /> }
|
249
src/components/helper/comments/Fediverse.astro
Normal file
249
src/components/helper/comments/Fediverse.astro
Normal file
|
@ -0,0 +1,249 @@
|
|||
---
|
||||
import { siteConfig } from "../../../config";
|
||||
|
||||
const fediverseConfig = siteConfig.comments.fediverse;
|
||||
const {
|
||||
instanceDomain,
|
||||
useV2api,
|
||||
token,
|
||||
useReverseProxy,
|
||||
reverseProxyUrl,
|
||||
accountId
|
||||
} = fediverseConfig;
|
||||
|
||||
interface Props {
|
||||
path: string;
|
||||
}
|
||||
|
||||
const { path } = Astro.props;
|
||||
|
||||
// Create the full URL to search for
|
||||
const fullSiteUrl = Astro.url.host;
|
||||
const postUrl = `https://${fullSiteUrl}${path.startsWith('/') ? path : '/' + path}`;
|
||||
|
||||
// Define the search API endpoint based on configuration
|
||||
let searchEndpoint;
|
||||
if (useReverseProxy && reverseProxyUrl) {
|
||||
searchEndpoint = reverseProxyUrl;
|
||||
} else {
|
||||
const apiVersion = useV2api ? 'v2' : 'v1';
|
||||
searchEndpoint = `https://${instanceDomain}/api/${apiVersion}/search`;
|
||||
}
|
||||
---
|
||||
|
||||
<div class="fediverse-comments">
|
||||
<div class="fediverse-status">
|
||||
<div id="loading-message">Loading comments from the Fediverse...</div>
|
||||
<div id="error-message" style="display: none;"></div>
|
||||
<div id="no-posts-message" style="display: none;">
|
||||
No discussions found for this post on the Fediverse yet.
|
||||
</div>
|
||||
</div>
|
||||
<div id="fediverse-replies"></div>
|
||||
</div>
|
||||
|
||||
<script define:vars={{
|
||||
searchEndpoint,
|
||||
postUrl,
|
||||
token,
|
||||
useReverseProxy,
|
||||
accountId,
|
||||
instanceDomain
|
||||
}}>
|
||||
async function fetchFediverseComments() {
|
||||
const loadingEl = document.getElementById('loading-message');
|
||||
const errorEl = document.getElementById('error-message');
|
||||
const noPostsEl = document.getElementById('no-posts-message');
|
||||
const repliesContainer = document.getElementById('fediverse-replies');
|
||||
|
||||
try {
|
||||
// Define request parameters
|
||||
const params = new URLSearchParams();
|
||||
if (!useReverseProxy) {
|
||||
params.append('q', postUrl);
|
||||
params.append('type', 'statuses');
|
||||
if (accountId) {
|
||||
params.append('account_id', accountId);
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare fetch options
|
||||
const options = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
};
|
||||
|
||||
if (token && !useReverseProxy) {
|
||||
options.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// Make the API request
|
||||
const url = useReverseProxy
|
||||
? `${searchEndpoint}?url=${encodeURIComponent(postUrl)}`
|
||||
: `${searchEndpoint}?${params.toString()}`;
|
||||
|
||||
const response = await fetch(url, options);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch comments: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Process the statuses
|
||||
const statuses = data.statuses || [];
|
||||
|
||||
if (statuses.length === 0) {
|
||||
loadingEl.style.display = 'none';
|
||||
noPostsEl.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the original post (by checking for our URL)
|
||||
const originalPost = statuses[0]
|
||||
|
||||
if (!originalPost) {
|
||||
loadingEl.style.display = 'none';
|
||||
noPostsEl.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch the status and its context (replies)
|
||||
const statusId = originalPost.id;
|
||||
const contextUrl = `https://${instanceDomain}/api/v1/statuses/${statusId}/context`;
|
||||
const contextResponse = await fetch(contextUrl, options);
|
||||
|
||||
if (!contextResponse.ok) {
|
||||
throw new Error(`Failed to fetch replies: ${contextResponse.statusText}`);
|
||||
}
|
||||
|
||||
const contextData = await contextResponse.json();
|
||||
const replies = contextData.descendants || [];
|
||||
|
||||
// Render replies
|
||||
if (replies.length > 0) {
|
||||
const repliesHtml = replies.map(reply => `
|
||||
<div class="reply">
|
||||
<div class="post-header">
|
||||
<img src="${reply.account.avatar}" alt="${reply.account.display_name}" class="avatar">
|
||||
<div class="post-meta">
|
||||
<div class="post-author">${reply.account.display_name}</div>
|
||||
<div class="post-username">@${reply.account.acct}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="reply-content">
|
||||
${reply.content}
|
||||
</div>
|
||||
<div class="post-stats">
|
||||
<span>🔁 ${reply.reblogs_count || 0}</span>
|
||||
<span>⭐ ${reply.favourites_count || 0}</span>
|
||||
</div>
|
||||
<div class="post-link">
|
||||
<a href="${reply.url}" target="_blank" rel="noopener noreferrer">View reply</a>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
repliesContainer.innerHTML += `
|
||||
<div class="replies-container">
|
||||
${repliesHtml}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
loadingEl.style.display = 'none';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching Fediverse comments:', error);
|
||||
loadingEl.style.display = 'none';
|
||||
errorEl.textContent = `Failed to load comments: ${error.message}`;
|
||||
errorEl.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Call the fetch function when the component is loaded
|
||||
document.addEventListener('DOMContentLoaded', fetchFediverseComments);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.fediverse-comments {
|
||||
margin-top: 2rem;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
.fediverse-status {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#error-message {
|
||||
color: #e74c3c;
|
||||
padding: 0.5rem;
|
||||
border-left: 3px solid #e74c3c;
|
||||
}
|
||||
|
||||
.original-post, .reply {
|
||||
border: 1px solid var(--border-color, #ddd);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
background-color: var(--comment-bg, rgba(0, 0, 0, 0.02));
|
||||
}
|
||||
|
||||
.post-header, .reply-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.post-author {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.post-username {
|
||||
opacity: 0.7;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.post-content, .reply-content {
|
||||
margin: 0.75rem 0;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.post-stats {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary, #666);
|
||||
}
|
||||
|
||||
.post-link {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.post-link a {
|
||||
color: var(--link-color, #0366d6);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.post-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.replies-container {
|
||||
margin-top: 1rem;
|
||||
margin-left: 1rem;
|
||||
border-left: 2px solid var(--border-color, #ddd);
|
||||
padding-left: 1rem;
|
||||
}
|
||||
</style>
|
|
@ -1,4 +1,47 @@
|
|||
export const siteConfig = {
|
||||
title: '/var/log/mercury',
|
||||
description: 'A blog about software development, technology, and life.',
|
||||
comments: {
|
||||
type: 'fediverse', // 'artalk','giscus','fediverse','hatsu'
|
||||
artalk: {
|
||||
instanceDomain: '', // the domain of your artalk instance
|
||||
},
|
||||
giscus: {
|
||||
// get these params from giscus.app
|
||||
repo:"[ENTER REPO HERE]",
|
||||
repoId: "[ENTER REPO ID HERE]",
|
||||
category:"[ENTER CATEGORY NAME HERE]",
|
||||
categoryId:"[ENTER CATEGORY ID HERE]",
|
||||
mapping:"pathname",
|
||||
strict:"0",
|
||||
reactionsEnabled:"1",
|
||||
emitMetadata:"0",
|
||||
inputPosition:"bottom",
|
||||
theme:"preferred_color_scheme",
|
||||
lang:"en"
|
||||
},
|
||||
// WIP
|
||||
fediverse: {
|
||||
// use Mastodon (compatible) api to search posts and parse replies
|
||||
// it will search for the post's link by default
|
||||
// the comments are rendered at the client side (by now)
|
||||
// a reverse proxy is required in pure client-side rendering mode to get the posts from the fediverse instance
|
||||
useReverseProxy: true,
|
||||
reverseProxyUrl: '', // the url of the reverse proxy, usually a cloudflare worker proxying the search api
|
||||
// the reverse proxy should be able to handle the following request:
|
||||
// GET /api/v1/search?q={query}&type=statuses&account_id=12345678
|
||||
// GET /api/v1/statuses/12345678/context
|
||||
// response body should be returned from the origin fediverse instance as-is.
|
||||
accountId: '', // the account id to search posts from, can be got from api like: https://{instance}/api/v1/accounts/{username without domain part}
|
||||
// for development purpose only (by now):
|
||||
// TODO: render the comments on the server-side when in server-side render/hybrid render mode
|
||||
instanceDomain: '', // the domain of the fediverse instance to search posts from
|
||||
useV2api: true, // use /api/v2/search instead of /api/v1/search to search on instance using newer version of mastodon/pleroma/akkoma
|
||||
token: process.env.MASTODON_API_TOKEN, // the token to use to authenticate with the fediverse instance, usually a read:search-only token
|
||||
},
|
||||
hatsu: {
|
||||
// use hatsu.cli.rs to get replies from the fediverse
|
||||
instanceDomain: '',
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,33 +1,14 @@
|
|||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
|
||||
---
|
||||
|
||||
<Layout title="Home | Terminal Blog">
|
||||
<h1 class="post-title">~/home</h1>
|
||||
<h1 class="post-title">~/</h1>
|
||||
|
||||
<div class="post-content">
|
||||
<p class="typewriter">A random grassblock do some some writing work.</p>
|
||||
<p class="typewriter" style="margin-top: 1.5rem;">Welcome to my terminal blog. Navigate using the links above.</p>
|
||||
|
||||
<div style="margin-top: 2rem;">
|
||||
<span class="command">ls -la</span>
|
||||
<div style="margin-top: 0.5rem; margin-left: 1rem;">
|
||||
<p>drwxr-xr-x 3 user group 96 Jun 8 15:42 .</p>
|
||||
<p>drwxr-xr-x 15 user group 480 Jun 8 14:22 ..</p>
|
||||
<p>-rw-r--r-- 1 user group 283 Jun 8 15:42 about.md</p>
|
||||
<p>-rw-r--r-- 1 user group 148 Jun 8 15:40 projects.md</p>
|
||||
<p>-rw-r--r-- 1 user group 892 Jun 8 15:35 notes.md</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 2rem;">
|
||||
<span class="command">cat about.md</span>
|
||||
<div style="margin-top: 0.5rem; margin-left: 1rem;">
|
||||
<p>
|
||||
I'm a developer who enjoys minimalist design and terminal aesthetics.
|
||||
This blog is a collection of my thoughts, projects, and experiments.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
Not much here yet, but you can check out my blog posts <a href="/blog">here</a>.
|
||||
<br />
|
||||
If you are site owner, please edit <code>src/pages/index.astro</code> to customize this page.
|
||||
</div>
|
||||
</Layout>
|
|
@ -1,6 +1,7 @@
|
|||
---
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import Comments from "../../components/Comments.astro";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const blogEntries = await getCollection('posts');
|
||||
|
@ -12,66 +13,23 @@ export async function getStaticPaths() {
|
|||
const { entry } = Astro.props;
|
||||
const { Content } = await entry.render();
|
||||
|
||||
// Sample content for demo purposes if no actual content collection is set up
|
||||
const samplePosts = {
|
||||
'terminal-setup': {
|
||||
title: 'My Terminal Setup',
|
||||
date: '2025-06-08',
|
||||
content: `
|
||||
<p>Here's my current terminal setup:</p>
|
||||
<ul>
|
||||
<li>Shell: ZSH with Oh My Zsh</li>
|
||||
<li>Terminal: Alacritty</li>
|
||||
<li>Color Scheme: Nord</li>
|
||||
<li>Font: JetBrains Mono</li>
|
||||
</ul>
|
||||
<p>I've been using this setup for about a year now and it's been working great for me.</p>
|
||||
`
|
||||
},
|
||||
'minimalism': {
|
||||
title: 'The Art of Minimalism',
|
||||
date: '2025-06-05',
|
||||
content: `
|
||||
<p>Minimalism isn't just about having less, it's about making room for what matters.</p>
|
||||
<p>In code, this means writing clean, maintainable code that does exactly what it needs to do, nothing more, nothing less.</p>
|
||||
<p>This terminal blog is an exercise in digital minimalism - stripping away the unnecessary to focus on what's important: the content.</p>
|
||||
`
|
||||
},
|
||||
'first-post': {
|
||||
title: 'First Post',
|
||||
date: '2025-06-01',
|
||||
content: `
|
||||
<p>Hello world! This is the first post on my new terminal blog.</p>
|
||||
<p>I built this blog using Astro and vanilla CSS/JS to create a terminal-like experience.</p>
|
||||
<p>More posts coming soon...</p>
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
const slug = Astro.params.slug;
|
||||
---
|
||||
|
||||
<Layout
|
||||
title={entry ? entry.data.title : samplePosts[slug]?.title}
|
||||
title={entry.data.title}
|
||||
path={`~/grassblock/micr0blog/blog/${slug}`}
|
||||
>
|
||||
{entry ? (
|
||||
<>
|
||||
<h1 class="post-title">{entry.data.title}</h1>
|
||||
<span class="post-date">{new Date(entry.data.pubDate).toISOString().split('T')[0]}</span>
|
||||
<div class="post-content">
|
||||
<Content />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h1 class="post-title">{samplePosts[slug]?.title}</h1>
|
||||
<span class="post-date">{samplePosts[slug]?.date}</span>
|
||||
<div class="post-content" set:html={samplePosts[slug]?.content}></div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div style="margin-top: 2rem; border-top: 1px solid var(--border-color); padding-top: 1rem;">
|
||||
<div style="margin-top: 2rem; border-top: 1px solid var(--border-color); padding-top: 1rem;">
|
||||
<h2>Comments</h2>
|
||||
<Comments path={`post/${slug}`} />
|
||||
<a href="/blog">← Back to posts</a>
|
||||
</div>
|
||||
|
||||
</Layout>
|
Loading…
Add table
Add a link
Reference in a new issue