mirror of
https://github.com/ClovertaTheTrilobita/SanYeCao-blog.git
synced 2026-07-03 15:41:26 +00:00
231 lines
No EOL
6.2 KiB
TypeScript
231 lines
No EOL
6.2 KiB
TypeScript
import type { APIRoute } from "astro";
|
|
import { getCollection } from "astro:content";
|
|
|
|
const token = import.meta.env.GITHUB_TOKEN;
|
|
const owner = import.meta.env.GISCUS_REPO_OWNER ?? "ClovertaTheTrilobita";
|
|
const name = import.meta.env.GISCUS_REPO_NAME ?? "SanYeCao-blog";
|
|
const categoryId = import.meta.env.GISCUS_CATEGORY_ID ?? "DIC_kwDORvuVpM4C5MDE";
|
|
|
|
export function getStaticPaths() {
|
|
return [{ params: { lang: "zh" } }, { params: { lang: "en" } }];
|
|
}
|
|
|
|
type CommentAuthor = {
|
|
login?: string;
|
|
avatarUrl?: string;
|
|
url?: string;
|
|
};
|
|
|
|
type RawReply = {
|
|
id: string;
|
|
body?: string;
|
|
updatedAt: string;
|
|
author?: CommentAuthor | null;
|
|
};
|
|
|
|
type RawComment = {
|
|
id: string;
|
|
body?: string;
|
|
updatedAt: string;
|
|
author?: CommentAuthor | null;
|
|
replies?: {
|
|
nodes?: RawReply[];
|
|
};
|
|
};
|
|
|
|
type DiscussionNode = {
|
|
title: string;
|
|
comments?: {
|
|
nodes?: RawComment[];
|
|
};
|
|
};
|
|
|
|
type LatestCommentItem = {
|
|
id: string;
|
|
body?: string;
|
|
updatedAt: string;
|
|
author?: CommentAuthor | null;
|
|
postId: string;
|
|
postTitle: string;
|
|
localUrl: string;
|
|
isReply: boolean;
|
|
replyTo?: string;
|
|
};
|
|
|
|
function normalizePath(path: string) {
|
|
return path.replace(/^\/+|\/+$/g, "");
|
|
}
|
|
|
|
function excerpt(text = "", max = 110) {
|
|
const plain = text.replace(/\s+/g, " ").trim();
|
|
if (plain.length <= max) return plain;
|
|
return plain.slice(0, max) + "…";
|
|
}
|
|
|
|
async function fetchLatestComments(lang: string, limit: number): Promise<LatestCommentItem[]> {
|
|
if (!token) return [];
|
|
|
|
const allPosts = await getCollection("blog");
|
|
|
|
const query = `
|
|
query($owner: String!, $name: String!, $categoryId: ID!) {
|
|
repository(owner: $owner, name: $name) {
|
|
discussions(
|
|
first: 20
|
|
categoryId: $categoryId
|
|
orderBy: { field: UPDATED_AT, direction: DESC }
|
|
) {
|
|
nodes {
|
|
title
|
|
comments(last: 10) {
|
|
nodes {
|
|
id
|
|
body
|
|
updatedAt
|
|
author {
|
|
... on User {
|
|
login
|
|
avatarUrl
|
|
url
|
|
}
|
|
... on Organization {
|
|
login
|
|
avatarUrl
|
|
url
|
|
}
|
|
}
|
|
replies(first: 10) {
|
|
nodes {
|
|
id
|
|
body
|
|
updatedAt
|
|
author {
|
|
... on User {
|
|
login
|
|
avatarUrl
|
|
url
|
|
}
|
|
... on Organization {
|
|
login
|
|
avatarUrl
|
|
url
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
const res = await fetch("https://api.github.com/graphql", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify({
|
|
query,
|
|
variables: { owner, name, categoryId },
|
|
}),
|
|
});
|
|
|
|
const json = await res.json();
|
|
const discussions = (json?.data?.repository?.discussions?.nodes ?? []) as DiscussionNode[];
|
|
|
|
const postMap = new Map(
|
|
allPosts.map((post: any) => [
|
|
normalizePath(post.id),
|
|
{
|
|
title: post.data.title,
|
|
url: `/${lang}/posts/${post.id}/#comments`,
|
|
},
|
|
]),
|
|
);
|
|
|
|
const comments: LatestCommentItem[] = [];
|
|
|
|
for (const discussion of discussions) {
|
|
const discussionKey = normalizePath(discussion.title);
|
|
|
|
let matchedPostId: string | undefined;
|
|
|
|
if (postMap.has(discussionKey)) {
|
|
matchedPostId = discussionKey;
|
|
} else {
|
|
matchedPostId = [...postMap.keys()].find(
|
|
(postId) =>
|
|
discussionKey === postId ||
|
|
discussionKey.includes(postId) ||
|
|
postId.includes(discussionKey),
|
|
);
|
|
}
|
|
|
|
if (!matchedPostId) continue;
|
|
|
|
const postInfo = postMap.get(matchedPostId);
|
|
if (!postInfo) continue;
|
|
|
|
for (const comment of discussion.comments?.nodes ?? []) {
|
|
comments.push({
|
|
id: comment.id,
|
|
body: excerpt(comment.body),
|
|
updatedAt: comment.updatedAt,
|
|
author: comment.author,
|
|
postId: matchedPostId,
|
|
postTitle: postInfo.title,
|
|
localUrl: postInfo.url,
|
|
isReply: false,
|
|
});
|
|
|
|
for (const reply of comment.replies?.nodes ?? []) {
|
|
comments.push({
|
|
id: reply.id,
|
|
body: excerpt(reply.body),
|
|
updatedAt: reply.updatedAt,
|
|
author: reply.author,
|
|
postId: matchedPostId,
|
|
postTitle: postInfo.title,
|
|
localUrl: postInfo.url,
|
|
isReply: true,
|
|
replyTo: comment.author?.login ?? "Unknown",
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return comments
|
|
.sort(
|
|
(a, b) =>
|
|
new Date(b.updatedAt).getTime() -
|
|
new Date(a.updatedAt).getTime(),
|
|
)
|
|
.slice(0, limit);
|
|
}
|
|
|
|
export const GET: APIRoute = async ({ params, url }) => {
|
|
try {
|
|
const lang = params.lang === "en" ? "en" : "zh";
|
|
const limit = Number(url.searchParams.get("limit") ?? 5);
|
|
|
|
const comments = await fetchLatestComments(lang, limit);
|
|
|
|
return new Response(JSON.stringify({ comments }), {
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": "application/json; charset=utf-8",
|
|
"Cache-Control": "public, max-age=300",
|
|
},
|
|
});
|
|
} catch (error) {
|
|
return new Response(JSON.stringify({ comments: [], error: "Failed to load comments" }), {
|
|
status: 500,
|
|
headers: {
|
|
"Content-Type": "application/json; charset=utf-8",
|
|
},
|
|
});
|
|
}
|
|
}; |