Compare commits

..

No commits in common. "30cdde0fc2e7dc748ddef0cf44cce125412f5eba" and "f571670e13daa179f3644de2e61e907ca7e59230" have entirely different histories.

6 changed files with 112 additions and 1006 deletions

View file

@ -4,8 +4,6 @@ import sitemap from '@astrojs/sitemap';
import mdx from '@astrojs/mdx'; import mdx from '@astrojs/mdx';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({ export default defineConfig({
site: 'https://terminal-blog.example.com', site: 'https://terminal-blog.example.com',
base: '/', base: '/',
@ -22,7 +20,5 @@ export default defineConfig({
} }
}, },
integrations: [sitemap(), mdx()], integrations: [sitemap(), mdx()]
adapter: cloudflare()
}); });

View file

@ -11,9 +11,7 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "^12.5.2",
"@astrojs/mdx": "^4.2.6", "@astrojs/mdx": "^4.2.6",
"@astrojs/node": "^9.2.1",
"@astrojs/rss": "^4.0.1", "@astrojs/rss": "^4.0.1",
"@astrojs/sitemap": "^3.3.1", "@astrojs/sitemap": "^3.3.1",
"astro": "^5.2.5", "astro": "^5.2.5",

752
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,6 @@ import FediverseComments from "./helper/comments/Fediverse.astro";
const method = siteConfig.comments.type const method = siteConfig.comments.type
const ArtalkConfig = siteConfig.comments.artalk const ArtalkConfig = siteConfig.comments.artalk
const giscusConfig = siteConfig.comments.giscus const giscusConfig = siteConfig.comments.giscus
const FediverseConfig = siteConfig.comments.fediverse
interface Props { interface Props {
path?: string; path?: string;
} }
@ -49,6 +48,4 @@ let { path='/' } = Astro.props;
async async
></script> ></script>
)} )}
<!-- if prerender === true is set then render from client --> {method === 'fediverse' && <FediverseComments path={path} /> }
{(method === 'fediverse' && !FediverseConfig.renderOnServer ) && <FediverseComments path={path} /> }
{(method === 'fediverse' && FediverseConfig.renderOnServer ) && <FediverseComments server:defer path={path} ><p>Loading comments...</p></FediverseComments> }

View file

@ -1,155 +1,47 @@
--- ---
import { siteConfig } from "../../../config"; import { siteConfig } from "../../../config";
const fediverseConfig = siteConfig.comments.fediverse; const fediverseConfig = siteConfig.comments.fediverse;
const { const {
renderOnServer, instanceDomain,
instanceDomain, useV2api,
useV2api, token,
token, useReverseProxy,
useReverseProxy, reverseProxyUrl,
reverseProxyUrl, accountId
accountId } = fediverseConfig;
} = fediverseConfig;
const serverRender = fediverseConfig.renderOnServer interface Props {
path: string;
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: string;
if (useReverseProxy && reverseProxyUrl) {
searchEndpoint = reverseProxyUrl;
} else {
const apiVersion = useV2api ? 'v2' : 'v1';
searchEndpoint = `https://${instanceDomain}/api/${apiVersion}/search`;
}
// Prepare default variables
let commentData: any = null;
let replies: any[] = [];
if (serverRender) {
// Server-side rendering - fetch data during build
// Prepare URL
const params = new URLSearchParams();
params.append('q', postUrl);
params.append('type', 'statuses');
if (accountId) {
params.append('account_id', accountId);
}
const url = `${searchEndpoint}?${params.toString()}`;
// Prepare fetch options
const options: RequestInit = {
method: 'GET',
headers: {
'Accept': 'application/json',
}
};
// Add authorization if token is provided
if (token) {
(options.headers as Record<string, string>)['Authorization'] = `Bearer ${token}`;
} }
// Fetch data const { path } = Astro.props;
commentData = await fetch(url, options).then(r => r.json());
// Extract original post and get replies if available // Create the full URL to search for
const statuses = commentData.statuses || []; const fullSiteUrl = Astro.url.host;
if (statuses.length > 0) { const postUrl = `https://${fullSiteUrl}${path.startsWith('/') ? path : '/' + path}`;
const originalPost = statuses[0];
// Fetch replies // Define the search API endpoint based on configuration
if (originalPost) { let searchEndpoint;
const statusId = originalPost.id; if (useReverseProxy && reverseProxyUrl) {
const contextUrl = `https://${instanceDomain}/api/v1/statuses/${statusId}/context`; searchEndpoint = reverseProxyUrl;
const contextResponse = await fetch(contextUrl, options); } else {
if (contextResponse.ok) { const apiVersion = useV2api ? 'v2' : 'v1';
const contextData = await contextResponse.json(); searchEndpoint = `https://${instanceDomain}/api/${apiVersion}/search`;
replies = contextData.descendants || [];
}
}
} }
} ---
---
<div class="fediverse-comments"> <div class="fediverse-comments">
<div class="fediverse-status"> <div class="fediverse-status">
{serverRender ? ( <div id="loading-message">Loading comments from the Fediverse...</div>
<> <div id="error-message" style="display: none;"></div>
{!commentData || !commentData.statuses || commentData.statuses.length === 0 ? ( <div id="no-posts-message" style="display: none;">
<div>No discussions found for this post on the Fediverse yet.</div> No discussions found for this post on the Fediverse yet.
) : null}
{commentData && commentData.statuses && commentData.statuses.length > 0 ? (
<div class="original-post">
<div class="post-header">
<img src={commentData.statuses[0].account.avatar} alt={commentData.statuses[0].account.display_name} class="avatar" />
<div class="post-meta">
<div class="post-author">{commentData.statuses[0].account.display_name}</div>
<div class="post-username">@{commentData.statuses[0].account.acct}</div>
</div>
</div>
<div class="post-content" set:html={commentData.statuses[0].content} />
<div class="post-stats">
<span>🔁 {commentData.statuses[0].reblogs_count || 0}</span>
<span>⭐ {commentData.statuses[0].favourites_count || 0}</span>
</div>
<div class="post-link">
<a href={commentData.statuses[0].url} target="_blank" rel="noopener noreferrer">View post</a>
</div>
</div>
) : null}
</>
) : (
<>
<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">
{!serverRender ? (
<div class="replies-container">
{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" set:html={reply.content} />
<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>
))}
</div> </div>
) : null} </div>
<div id="fediverse-replies"></div>
</div> </div>
</div>
{!serverRender && (
<script define:vars={{ <script define:vars={{
searchEndpoint, searchEndpoint,
postUrl, postUrl,
@ -210,7 +102,7 @@ if (serverRender) {
} }
// Find the original post (by checking for our URL) // Find the original post (by checking for our URL)
const originalPost = statuses[0]; const originalPost = statuses[0]
if (!originalPost) { if (!originalPost) {
loadingEl.style.display = 'none'; loadingEl.style.display = 'none';
@ -218,31 +110,6 @@ if (serverRender) {
return; return;
} }
// Render the original post
const originalPostHtml = `
<div class="original-post">
<div class="post-header">
<img src="${originalPost.account.avatar}" alt="${originalPost.account.display_name}" class="avatar">
<div class="post-meta">
<div class="post-author">${originalPost.account.display_name}</div>
<div class="post-username">@${originalPost.account.acct}</div>
</div>
</div>
<div class="post-content">
${originalPost.content}
</div>
<div class="post-stats">
<span>🔁 ${originalPost.reblogs_count || 0}</span>
<span>⭐ ${originalPost.favourites_count || 0}</span>
</div>
<div class="post-link">
<a href="${originalPost.url}" target="_blank" rel="noopener noreferrer">View post</a>
</div>
</div>
`;
document.querySelector('.fediverse-status').innerHTML = originalPostHtml;
// Fetch the status and its context (replies) // Fetch the status and its context (replies)
const statusId = originalPost.id; const statusId = originalPost.id;
const contextUrl = `https://${instanceDomain}/api/v1/statuses/${statusId}/context`; const contextUrl = `https://${instanceDomain}/api/v1/statuses/${statusId}/context`;
@ -279,12 +146,15 @@ if (serverRender) {
</div> </div>
`).join(''); `).join('');
repliesContainer.innerHTML = ` repliesContainer.innerHTML += `
<div class="replies-container"> <div class="replies-container">
${repliesHtml} ${repliesHtml}
</div> </div>
`; `;
} }
loadingEl.style.display = 'none';
} catch (error) { } catch (error) {
console.error('Error fetching Fediverse comments:', error); console.error('Error fetching Fediverse comments:', error);
loadingEl.style.display = 'none'; loadingEl.style.display = 'none';
@ -296,85 +166,84 @@ if (serverRender) {
// Call the fetch function when the component is loaded // Call the fetch function when the component is loaded
document.addEventListener('DOMContentLoaded', fetchFediverseComments); document.addEventListener('DOMContentLoaded', fetchFediverseComments);
</script> </script>
)}
<style> <style>
.fediverse-comments { .fediverse-comments {
margin-top: 2rem; margin-top: 2rem;
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
} }
.fediverse-status { .fediverse-status {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
#error-message { #error-message {
color: #e74c3c; color: #e74c3c;
padding: 0.5rem; padding: 0.5rem;
border-left: 3px solid #e74c3c; border-left: 3px solid #e74c3c;
} }
.original-post, .reply { .original-post, .reply {
border: 1px solid var(--border-color, #ddd); border: 1px solid var(--border-color, #ddd);
border-radius: 8px; border-radius: 8px;
padding: 1rem; padding: 1rem;
margin-bottom: 1rem; margin-bottom: 1rem;
background-color: var(--comment-bg, rgba(0, 0, 0, 0.02)); background-color: var(--comment-bg, rgba(0, 0, 0, 0.02));
} }
.post-header, .reply-header { .post-header, .reply-header {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.avatar { .avatar {
width: 48px; width: 48px;
height: 48px; height: 48px;
border-radius: 50%; border-radius: 50%;
margin-right: 0.75rem; margin-right: 0.75rem;
} }
.post-author { .post-author {
font-weight: bold; font-weight: bold;
} }
.post-username { .post-username {
opacity: 0.7; opacity: 0.7;
font-size: 0.9rem; font-size: 0.9rem;
} }
.post-content, .reply-content { .post-content, .reply-content {
margin: 0.75rem 0; margin: 0.75rem 0;
overflow-wrap: break-word; overflow-wrap: break-word;
} }
.post-stats { .post-stats {
display: flex; display: flex;
gap: 1rem; gap: 1rem;
margin-top: 0.5rem; margin-top: 0.5rem;
font-size: 0.9rem; font-size: 0.9rem;
color: var(--text-secondary, #666); color: var(--text-secondary, #666);
} }
.post-link { .post-link {
margin-top: 0.5rem; margin-top: 0.5rem;
font-size: 0.9rem; font-size: 0.9rem;
} }
.post-link a { .post-link a {
color: var(--link-color, #0366d6); color: var(--link-color, #0366d6);
text-decoration: none; text-decoration: none;
} }
.post-link a:hover { .post-link a:hover {
text-decoration: underline; text-decoration: underline;
} }
.replies-container { .replies-container {
margin-top: 1rem; margin-top: 1rem;
margin-left: 1rem; margin-left: 1rem;
border-left: 2px solid var(--border-color, #ddd); border-left: 2px solid var(--border-color, #ddd);
padding-left: 1rem; padding-left: 1rem;
} }
</style> </style>

View file

@ -24,19 +24,17 @@ export const siteConfig = {
fediverse: { fediverse: {
// use Mastodon (compatible) api to search posts and parse replies // use Mastodon (compatible) api to search posts and parse replies
// it will search for the post's link by default // it will search for the post's link by default
renderOnServer: false, // render comments on server-side or client-side, may different from the astro config // the comments are rendered at the client side (by now)
// the comments are rendered at the client side by default // a reverse proxy is required in pure client-side rendering mode to get the posts from the fediverse instance
// but if you want to deploy site on Cloudflare pages or so you can set it to true. useReverseProxy: true,
// (but in pure SSG mode, the comments will be rendered at build time, which mean delayed updates,maybe?)
// a reverse proxy is recommended in pure client-side rendering mode to get the posts from the fediverse instance
// that requires to be authorized to use search api the instance
useReverseProxy: false,
reverseProxyUrl: '', // the url of the reverse proxy, usually a cloudflare worker proxying the search api 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: // 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/search?q={query}&type=statuses&account_id=12345678
// GET /api/v1/statuses/12345678/context // GET /api/v1/statuses/12345678/context
// response body should be returned from the origin (fediverse instance) as-is. // 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} 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 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 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 token: process.env.MASTODON_API_TOKEN, // the token to use to authenticate with the fediverse instance, usually a read:search-only token