跳至主要內容
Skip to content

在 VitePress 部落格加入 giscus 留言系統

靜態部落格最大的優點是簡單:不用資料庫、不需要使用者系統,也不太需要維護後端。但只要想加上留言功能,事情就會立刻變複雜。留言需要儲存、登入、審核、通知、防垃圾訊息,這些都不是單純貼一個表單就能結束的事情。

這篇記錄如何在 VitePress 部落格中接入 giscus,使用 GitHub Discussions 作為留言資料來源,並把它封裝成一個乾淨的 Vue 元件。


一、為什麼選 giscus

常見的靜態網站留言方案大致有幾類:

方案資料儲存適合情境
giscusGitHub Discussions技術部落格、開源專案文件、讀者多半有 GitHub 帳號
utterancesGitHub Issues較舊的 GitHub-based 留言方案
Cusdis自家服務或雲端服務希望讀者不用 GitHub 登入
自建留言系統自己的資料庫需要會員、審核後台、通知、黑名單

這個部落格本身就是技術內容為主,讀者大多能接受 GitHub 登入,因此 giscus 是最省維護成本的選擇。

它的核心模式是:

text
文章頁面
→ giscus widget
→ GitHub Discussions
→ 每篇文章對應一個 discussion

我們不需要自己建立留言 API,也不需要自己設計資料表。GitHub 會處理登入、留言儲存、通知和基本管理。


二、開啟 GitHub Discussions

先到部落格的 GitHub repository:

text
GitHub repo
→ Settings
→ General
→ Features
→ Discussions

勾選 Discussions 後,repository 上方會出現 Discussions 分頁。

第一次打開 Discussions 時,GitHub 可能會自動建立一篇歡迎討論串,這是正常的。


三、安裝 giscus GitHub App

接著安裝 giscus App:

text
https://github.com/apps/giscus

權限選擇建議:

text
Only select repositories
→ 選擇部落格 repository

不要直接授權所有 repositories。giscus 只需要讀取 metadata 和管理 discussions,授權單一 repo 就足夠。


四、產生 giscus 設定

打開:

text
https://giscus.app/

填入 repository:

text
kelvinLin9/hirimu-blog

如果 repository、Discussions、App 權限都設定正確,giscus 會顯示通過。

4.1 Mapping 選 pathname

頁面與 discussion 的對應方式建議選:

text
Discussion 的標題包含頁面的路徑名稱

也就是:

html
data-mapping="pathname"

原因是文章標題可能會調整,但文章網址通常比較穩定。例如:

text
/posts/binary-data/05-FormData混合上傳.html

如果用 title 或 og:title,之後改標題就有可能找不到原本的 discussion。

4.2 Category 選 Announcements

giscus 建議選擇 Announcements 類型的分類,讓新的 discussion 只能由 repository 維護者和 giscus 建立。

本次設定如下:

html
data-category="Announcements"
data-category-id="DIC_kwDORhnLs84C8yir"

並啟用:

html
data-reactions-enabled="1"
data-emit-metadata="1"
data-input-position="bottom"
data-theme="preferred_color_scheme"
data-lang="zh-TW"
data-loading="lazy"

其中 data-loading="lazy" 可以避免留言區太早載入,對文章頁效能比較友善。


五、不要直接把 script 貼進 Markdown

giscus 會產生一段 <script>

html
<script
  src="https://giscus.app/client.js"
  data-repo="kelvinLin9/hirimu-blog"
  data-repo-id="R_kgDORhnLsw"
  data-category="Announcements"
  data-category-id="DIC_kwDORhnLs84C8yir"
  data-mapping="pathname"
  data-strict="0"
  data-reactions-enabled="1"
  data-emit-metadata="1"
  data-input-position="bottom"
  data-theme="preferred_color_scheme"
  data-lang="zh-TW"
  data-loading="lazy"
  crossorigin="anonymous"
  async
></script>

如果只是單頁使用,直接貼也能工作。但在 VitePress 裡,我們希望所有文章頁自動出現留言區,且首頁、後台、文章列表不要出現。因此比較好的做法是封裝成 Vue 元件。


六、封裝 GiscusComments 元件

新增檔案:

text
.vitepress/theme/components/GiscusComments.vue

核心邏輯如下:

vue
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useData, useRoute } from 'vitepress'

const { frontmatter } = useData()
const route = useRoute()
const container = ref<HTMLElement | null>(null)

const shouldShowComments = computed(() => {
  if (frontmatter.value.comments === false) return false
  if (!route.path.startsWith('/posts/')) return false
  if (route.path === '/posts/' || route.path.endsWith('/index.html')) return false
  return true
})

const clearComments = () => {
  if (container.value) container.value.innerHTML = ''
}

const loadComments = async () => {
  clearComments()
  if (!shouldShowComments.value) return

  await nextTick()
  if (!container.value) return

  const script = document.createElement('script')
  script.src = 'https://giscus.app/client.js'
  script.async = true
  script.crossOrigin = 'anonymous'
  script.setAttribute('data-repo', 'kelvinLin9/hirimu-blog')
  script.setAttribute('data-repo-id', 'R_kgDORhnLsw')
  script.setAttribute('data-category', 'Announcements')
  script.setAttribute('data-category-id', 'DIC_kwDORhnLs84C8yir')
  script.setAttribute('data-mapping', 'pathname')
  script.setAttribute('data-strict', '0')
  script.setAttribute('data-reactions-enabled', '1')
  script.setAttribute('data-emit-metadata', '1')
  script.setAttribute('data-input-position', 'bottom')
  script.setAttribute('data-theme', 'preferred_color_scheme')
  script.setAttribute('data-lang', 'zh-TW')
  script.setAttribute('data-loading', 'lazy')

  container.value.appendChild(script)
}

onMounted(loadComments)
onBeforeUnmount(clearComments)

watch(() => route.path, loadComments)
</script>

<template>
  <section v-if="shouldShowComments" class="comments-section" aria-label="留言">
    <div ref="container"></div>
  </section>
</template>

這裡有幾個重點。

第一,script 只在 onMounted 後建立,避免 SSR 階段碰到 document

第二,路由切換時要清空舊留言,再重新載入。VitePress 是 SPA,如果不處理 route watch,從文章 A 切到文章 B 時,留言區可能仍然停在前一篇。

第三,使用 frontmatter.comments === false 讓單篇文章可以關閉留言:

yaml
---
title: 某篇文章
comments: false
---

七、接到 VitePress Layout

VitePress 預設主題提供 doc-after slot,位置在文章內容與文章頁尾之間,非常適合放留言區。

在:

text
.vitepress/theme/Layout.vue

引入元件:

ts
import GiscusComments from './components/GiscusComments.vue'

然後掛到 DefaultTheme.Layout 裡:

vue
<template #doc-after>
  <GiscusComments />
</template>

這樣所有文件頁都會經過留言元件,但是否真正顯示,交給 shouldShowComments 判斷。


八、顯示規則

目前顯示規則是:

text
✅ /posts/... 實際文章頁
❌ /posts/ 文章列表頁
❌ /posts/*/index.html 系列首頁
❌ 首頁
❌ 後台頁
❌ 贊助頁

這樣留言區只會出現在真正需要討論的文章底部,不會污染工具頁或列表頁。


九、樣式微調

留言區只需要保留簡單分隔:

css
.comments-section {
  margin-top: 3rem;
  padding-top: 2rem;
  border-top: 1px solid var(--color-border-subtle);
}

giscus 內部 iframe 會自己處理主題。這裡使用:

html
data-theme="preferred_color_scheme"

讓它跟隨使用者系統偏好。

如果網站有自訂深色模式切換,之後可以再監聽 VitePress theme 狀態,透過 postMessage 動態切換 giscus theme。


十、驗證

完成後打開任一文章頁:

text
http://localhost:5183/posts/binary-data/05-FormData混合上傳.html

應該會在文章底部看到 giscus 留言區。

最後跑一次 build:

bash
npm run build

如果 build 通過,代表 SSR 階段沒有被 document 或外部 script 影響。


總結

這次整合的關鍵不是「把 giscus script 貼上去」,而是把留言功能放進 VitePress 的生命週期裡:

  • GitHub Discussions 負責留言資料
  • giscus App 負責建立與讀寫 discussion
  • pathname 負責穩定對應文章
  • Vue 元件負責在客戶端載入 script
  • doc-after slot 負責插入文章底部
  • frontmatter 負責單篇開關

對靜態技術部落格來說,這是一個很平衡的方案:不用自建留言系統,又能保留足夠的管理能力。