diff --git a/src/components/LatestComments.astro b/src/components/LatestComments.astro
index 4566eba..4ad0a7b 100644
--- a/src/components/LatestComments.astro
+++ b/src/components/LatestComments.astro
@@ -75,6 +75,110 @@ const heading = lang === "zh" ? "最新评论" : "Latest comments";
return item?.user?.name || (lang === "zh" ? "匿名用户" : "Anonymous");
}
+ function normalizePath(url) {
+ if (!url) return "";
+
+ try {
+ const u = new URL(url, window.location.origin);
+ let path = u.pathname;
+
+ if (!path.startsWith("/")) path = "/" + path;
+ if (!path.endsWith("/")) path += "/";
+
+ return path;
+ } catch {
+ let path = String(url);
+
+ path = path.replace(/^https?:\/\/[^/]+/i, "");
+ if (!path.startsWith("/")) path = "/" + path;
+ if (!path.endsWith("/")) path += "/";
+
+ return path;
+ }
+ }
+
+ function buildUrlCandidates(rawUrl) {
+ const path = normalizePath(rawUrl);
+ if (!path) return [];
+
+ const noSlash = path.endsWith("/") ? path.slice(0, -1) : path;
+
+ const candidates = [
+ path,
+ noSlash,
+ `/zh${path}`,
+ `/zh${noSlash}`,
+ `/en${path}`,
+ `/en${noSlash}`,
+ `${window.location.origin}${path}`,
+ `${window.location.origin}${noSlash}`,
+ ];
+
+ return [...new Set(candidates.filter(Boolean))];
+ }
+
+ async function fetchPostComments(host, siteId, rawUrl) {
+ const candidates = buildUrlCandidates(rawUrl);
+
+ for (const candidate of candidates) {
+ try {
+ const res = await fetch(
+ `${host}/api/v1/find?site=${encodeURIComponent(siteId)}&url=${encodeURIComponent(candidate)}&format=plain`,
+ { credentials: "omit" },
+ );
+
+ if (!res.ok) continue;
+
+ const data = await res.json();
+
+ const comments = Array.isArray(data)
+ ? data
+ : Array.isArray(data?.comments)
+ ? data.comments
+ : [];
+
+ if (comments.length > 0) {
+ return comments;
+ }
+ } catch {}
+ }
+
+ return [];
+ }
+
+ async function enrichReplyTargets(host, siteId, comments) {
+ const urls = [
+ ...new Set(
+ comments.map((item) => item?.locator?.url).filter(Boolean),
+ ),
+ ];
+
+ const postMap = new Map();
+
+ await Promise.all(
+ urls.map(async (url) => {
+ const postComments = await fetchPostComments(host, siteId, url);
+ postMap.set(url, postComments);
+ }),
+ );
+
+ return comments.map((item) => {
+ if (!item?.pid) {
+ return { ...item, replyToName: "" };
+ }
+
+ const postComments = postMap.get(item?.locator?.url) || [];
+ const parent = postComments.find(
+ (c) => String(c?.id) === String(item?.pid),
+ );
+
+ return {
+ ...item,
+ replyToName: parent?.user?.name || "",
+ };
+ });
+ }
+
function renderComments(root, comments, lang) {
const list = root.querySelector(".latest-comments-list");
const loading = root.querySelector(".comments-loading");
@@ -90,20 +194,35 @@ const heading = lang === "zh" ? "最新评论" : "Latest comments";
list.innerHTML = comments
.map((item) => {
- const author = escapeHtml(getUserName(item, lang));
+ const rawAuthor = getUserName(item, lang);
+ const author = escapeHtml(rawAuthor);
+ const fallbackInitial = escapeHtml(
+ rawAuthor.slice(0, 1).toUpperCase(),
+ );
+
const avatar = getAvatar(item);
const safeAvatar = escapeHtml(avatar || "");
+
const title = escapeHtml(
item?.title ||
(lang === "zh" ? "未命名文章" : "Untitled post"),
);
const href = rewritePostUrl(item?.locator?.url || "", lang);
+
+ // Remark42 返回的 text 是处理后的 HTML,适合直接渲染
const html = item?.text || "";
const time = formatTime(item?.time, lang);
+ const replyTo = escapeHtml(item?.replyToName || "");
+ const authorLine = replyTo
+ ? ``
+ : ``;
+
const avatarHtml = avatar
? ``
- : `
`;
+ : ``;
return `