评论区改为自托管 twikoo 前端,并适配主题

- 前端脚本从 jsDelivr 改为自托管 public/assets/js/twikoo.min.js,
  版本与后端对齐(1.7.20)。国内访问 jsDelivr 实测 3 秒以上且常被墙,
  是评论区空白的根因
- 新增 public/assets/css/twikoo-custom.css 主题适配层:选择器加 #twikoo
  前缀以覆盖运行时注入的样式,颜色引用主题变量,自动跟随亮/暗色
- 修复首屏重复初始化:astro:page-load 在首屏也会触发,用 dataset.mounted 去重
- 评论配置移除已无用的 version 字段;README 与操作指南同步更新
This commit is contained in:
dsh
2026-09-14 20:58:37 +08:00
parent 686b1caa7d
commit 0b99eabfbe
6 changed files with 320 additions and 87 deletions
+98 -74
View File
@@ -6,86 +6,110 @@ const envId =
const isEnabled = Boolean(envId)
---
{isEnabled && (
<section class="mt-8 pt-0 pb-12">
<div class="px-6">
<div class="max-w-3xl mx-auto">
<div
id="twikoo"
data-env-id={envId}
data-lang={commentConfig.lang}
data-version={commentConfig.version}
data-strip-trailing-slash={commentConfig.stripTrailingSlash ? "true" : "false"}
></div>
<script is:inline>
(() => {
const SCRIPT_ID = "twikoo-script"
{
isEnabled && (
<section class="mt-12 pb-12" data-comments>
{/* twikoo 的主样式由脚本运行时注入,这里只加主题适配层 */}
<link rel="stylesheet" href="/assets/css/twikoo-custom.css" />
const loadScript = (version) => {
if (window.twikoo) return Promise.resolve()
<div class="px-6">
<div class="max-w-3xl mx-auto">
<h2 class="mb-6 flex items-center gap-3 text-2xl font-semibold text-foreground">
<span class="h-6 w-1 rounded-full bg-primary" />
评论
</h2>
const existing = document.getElementById(SCRIPT_ID)
if (existing) {
if (existing.dataset.loaded === "true") return Promise.resolve()
return new Promise((resolve, reject) => {
existing.addEventListener("load", () => resolve(), { once: true })
existing.addEventListener("error", () => {
reject(new Error("Failed to load twikoo"))
}, { once: true })
})
}
<div
id="twikoo"
data-env-id={envId}
data-lang={commentConfig.lang}
data-strip-trailing-slash={commentConfig.stripTrailingSlash ? "true" : "false"}
/>
return new Promise((resolve, reject) => {
const script = document.createElement("script")
script.id = SCRIPT_ID
script.src = `https://cdn.jsdelivr.net/npm/twikoo@${version}/dist/twikoo.min.js`
script.async = true
script.onload = () => {
script.dataset.loaded = "true"
resolve()
<script is:inline>
;(() => {
const SCRIPT_SRC = "/assets/js/twikoo.min.js"
const loadScript = () => {
if (window.twikoo) return Promise.resolve()
const existing = document.querySelector("script[data-twikoo]")
if (existing) {
if (existing.dataset.loaded === "true") return Promise.resolve()
return new Promise((resolve, reject) => {
existing.addEventListener("load", () => resolve(), { once: true })
existing.addEventListener(
"error",
() => reject(new Error("twikoo 脚本加载失败")),
{ once: true },
)
})
}
script.onerror = () => reject(new Error("Failed to load twikoo"))
document.head.appendChild(script)
})
}
// 评论归属的路径。去掉结尾斜杠,与站点历史评论数据保持一致。
const getCommentPath = (stripTrailingSlash) => {
const pathname = window.location.pathname
if (stripTrailingSlash && pathname.length > 1 && pathname.endsWith("/")) {
return pathname.slice(0, -1)
}
return pathname
}
const mountComments = async () => {
const container = document.getElementById("twikoo")
if (!container) return
const envId = container.dataset.envId
if (!envId) return
try {
await loadScript(container.dataset.version)
// 切页时容器已被换成新节点,清空可避免重复渲染
container.innerHTML = ""
await window.twikoo.init({
envId,
el: "#twikoo",
lang: container.dataset.lang || "zh-CN",
path: getCommentPath(container.dataset.stripTrailingSlash === "true"),
return new Promise((resolve, reject) => {
const script = document.createElement("script")
script.src = SCRIPT_SRC
script.async = true
script.dataset.twikoo = "true"
script.onload = () => {
script.dataset.loaded = "true"
resolve()
}
script.onerror = () => reject(new Error("twikoo 脚本加载失败: " + SCRIPT_SRC))
document.head.appendChild(script)
})
} catch (error) {
console.error("[twikoo] 初始化失败:", error)
}
}
mountComments()
// 本站启用了 Astro 的 ClientRouter,切页后需要重新挂载
document.addEventListener("astro:page-load", mountComments)
})()
</script>
/**
* 评论归属路径。本站 trailingSlash 为 "always",但评论 key 统一去掉
* 结尾斜杠(/posts/xxx),与历史数据保持一致。
*/
const getCommentPath = (strip) => {
const pathname = window.location.pathname
if (strip && pathname.length > 1 && pathname.endsWith("/")) {
return pathname.slice(0, -1)
}
return pathname
}
const mount = async () => {
const container = document.getElementById("twikoo")
if (!container) return
// astro:page-load 在首屏也会触发,用标记避免重复初始化;
// 切页时容器是新节点,没有这个标记,所以会正常重新挂载。
if (container.dataset.mounted === "true") return
const envId = container.dataset.envId
if (!envId) return
container.dataset.mounted = "true"
try {
await loadScript()
await window.twikoo.init({
envId,
el: "#twikoo",
lang: container.dataset.lang || "zh-CN",
path: getCommentPath(container.dataset.stripTrailingSlash === "true"),
})
} catch (error) {
// 失败时解除标记,允许下次切页重试
container.dataset.mounted = "false"
console.error("[twikoo] 初始化失败:", error)
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", mount)
} else {
mount()
}
document.addEventListener("astro:page-load", mount)
})()
</script>
</div>
</div>
</div>
</section>
)}
</section>
)
}