diff --git a/public/fonts/subset/MapleMono-CN-Bold.ttf b/public/fonts/subset/MapleMono-CN-Bold.ttf index d508e1b..ded1522 100644 Binary files a/public/fonts/subset/MapleMono-CN-Bold.ttf and b/public/fonts/subset/MapleMono-CN-Bold.ttf differ diff --git a/public/fonts/subset/MapleMono-CN-Bold.woff2 b/public/fonts/subset/MapleMono-CN-Bold.woff2 index 3cf9e4a..d9f0604 100644 Binary files a/public/fonts/subset/MapleMono-CN-Bold.woff2 and b/public/fonts/subset/MapleMono-CN-Bold.woff2 differ diff --git a/public/fonts/subset/MapleMono-CN-Italic.ttf b/public/fonts/subset/MapleMono-CN-Italic.ttf index 04bb5ef..a732ea5 100644 Binary files a/public/fonts/subset/MapleMono-CN-Italic.ttf and b/public/fonts/subset/MapleMono-CN-Italic.ttf differ diff --git a/public/fonts/subset/MapleMono-CN-Italic.woff2 b/public/fonts/subset/MapleMono-CN-Italic.woff2 index 4774a55..a695900 100644 Binary files a/public/fonts/subset/MapleMono-CN-Italic.woff2 and b/public/fonts/subset/MapleMono-CN-Italic.woff2 differ diff --git a/public/fonts/subset/MapleMono-CN-Regular.ttf b/public/fonts/subset/MapleMono-CN-Regular.ttf index fa69e84..f4d0aac 100644 Binary files a/public/fonts/subset/MapleMono-CN-Regular.ttf and b/public/fonts/subset/MapleMono-CN-Regular.ttf differ diff --git a/public/fonts/subset/MapleMono-CN-Regular.woff2 b/public/fonts/subset/MapleMono-CN-Regular.woff2 index 9c49b60..6dbda69 100644 Binary files a/public/fonts/subset/MapleMono-CN-Regular.woff2 and b/public/fonts/subset/MapleMono-CN-Regular.woff2 differ diff --git a/src/components/Comments.astro b/src/components/Comments.astro new file mode 100644 index 0000000..3f46844 --- /dev/null +++ b/src/components/Comments.astro @@ -0,0 +1,407 @@ +--- +import { getCollection } from "astro:content"; +import { getLangFromUrl } from "@/i18n"; + +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"; +const limit = Number(Astro.props.limit ?? 5); + +const lang = getLangFromUrl(Astro.url); +const allPosts = await getCollection("blog"); + +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) + "…"; +} + +function formatDate(iso: string) { + return new Date(iso).toISOString().slice(0, 10); +} + +async function fetchLatestComments(): Promise { + if (!token) return []; + + 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[]; + + // 用 post.id 建本地文章索引 + 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: 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: 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); +} + +const comments = await fetchLatestComments(); +--- + +
+

{lang === "zh" ? "最新评论" : "Latest Comments"}

+ + { + comments.length === 0 ? ( +

+ {lang === "zh" ? "还没有评论。" : "No comments yet."} +

+ ) : ( + + ) + } +
+ + diff --git a/src/components/Posts/PostItem.astro b/src/components/Posts/PostItem.astro index 2d98c23..5b26a3b 100644 --- a/src/components/Posts/PostItem.astro +++ b/src/components/Posts/PostItem.astro @@ -4,7 +4,7 @@ const data = Astro.props; ---
  • - +
    {data.title} {data.description} diff --git a/src/components/Posts/PostList.astro b/src/components/Posts/PostList.astro index 96bd531..c6cb235 100644 --- a/src/components/Posts/PostList.astro +++ b/src/components/Posts/PostList.astro @@ -73,11 +73,18 @@ async function fetchDiscussionStats(): Promise { } const discussions = await fetchDiscussionStats(); + +const sortedPosts = [...allPosts].sort( + (a, b) => + new Date(b.data.pubDate).getTime() - new Date(a.data.pubDate).getTime(), +); + +const latestPosts = sortedPosts.slice(0, 5); ---
      { - allPosts.map((post: any) => { + latestPosts.map((post: any) => { const formattedDate = new Date(post.data.pubDate) .toISOString() .split("T")[0]; @@ -112,9 +119,35 @@ const discussions = await fetchDiscussionStats(); }
    +
    + diff --git a/src/pages/[lang]/index.astro b/src/pages/[lang]/index.astro index e78f444..ffd4d0d 100644 --- a/src/pages/[lang]/index.astro +++ b/src/pages/[lang]/index.astro @@ -1,6 +1,7 @@ --- import BaseLayout from "@/layouts/BaseLayout.astro"; import PostList from "@/components/Posts/PostList.astro"; +import Comments from "@/components/Comments.astro"; import { getLangFromUrl, getTranslations } from "@/i18n"; import "@/styles/global.css"; @@ -23,6 +24,10 @@ const pageTitle = t.home.title;
    + +
    + +