Appearance
靜態部落格最大的優點是簡單:不用資料庫、不需要使用者系統,也不太需要維護後端。但只要想加上留言功能,事情就會立刻變複雜。留言需要儲存、登入、審核、通知、防垃圾訊息,這些都不是單純貼一個表單就能結束的事情。
這篇記錄如何在 VitePress 部落格中接入 giscus,使用 GitHub Discussions 作為留言資料來源,並把它封裝成一個乾淨的 Vue 元件。
常見的靜態網站留言方案大致有幾類:
這個部落格本身就是技術內容為主,讀者大多能接受 GitHub 登入,因此 giscus 是最省維護成本的選擇。
它的核心模式是:
文章頁面 → giscus widget → GitHub Discussions → 每篇文章對應一個 discussion
我們不需要自己建立留言 API,也不需要自己設計資料表。GitHub 會處理登入、留言儲存、通知和基本管理。
先到部落格的 GitHub repository:
GitHub repo → Settings → General → Features → Discussions
勾選 Discussions 後,repository 上方會出現 Discussions 分頁。
Discussions
第一次打開 Discussions 時,GitHub 可能會自動建立一篇歡迎討論串,這是正常的。
接著安裝 giscus App:
https://github.com/apps/giscus
權限選擇建議:
Only select repositories → 選擇部落格 repository
不要直接授權所有 repositories。giscus 只需要讀取 metadata 和管理 discussions,授權單一 repo 就足夠。
打開:
https://giscus.app/
填入 repository:
kelvinLin9/hirimu-blog
如果 repository、Discussions、App 權限都設定正確,giscus 會顯示通過。
頁面與 discussion 的對應方式建議選:
Discussion 的標題包含頁面的路徑名稱
也就是:
data-mapping="pathname"
原因是文章標題可能會調整,但文章網址通常比較穩定。例如:
/posts/binary-data/05-FormData混合上傳.html
如果用 title 或 og:title,之後改標題就有可能找不到原本的 discussion。
giscus 建議選擇 Announcements 類型的分類,讓新的 discussion 只能由 repository 維護者和 giscus 建立。
Announcements
本次設定如下:
data-category="Announcements" data-category-id="DIC_kwDORhnLs84C8yir"
並啟用:
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" 可以避免留言區太早載入,對文章頁效能比較友善。
data-loading="lazy"
giscus 會產生一段 <script>:
<script>
<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 元件。
新增檔案:
.vitepress/theme/components/GiscusComments.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。
onMounted
document
第二,路由切換時要清空舊留言,再重新載入。VitePress 是 SPA,如果不處理 route watch,從文章 A 切到文章 B 時,留言區可能仍然停在前一篇。
第三,使用 frontmatter.comments === false 讓單篇文章可以關閉留言:
frontmatter.comments === false
--- title: 某篇文章 comments: false ---
VitePress 預設主題提供 doc-after slot,位置在文章內容與文章頁尾之間,非常適合放留言區。
doc-after
在:
.vitepress/theme/Layout.vue
引入元件:
import GiscusComments from './components/GiscusComments.vue'
然後掛到 DefaultTheme.Layout 裡:
DefaultTheme.Layout
<template #doc-after> <GiscusComments /> </template>
這樣所有文件頁都會經過留言元件,但是否真正顯示,交給 shouldShowComments 判斷。
shouldShowComments
目前顯示規則是:
✅ /posts/... 實際文章頁 ❌ /posts/ 文章列表頁 ❌ /posts/*/index.html 系列首頁 ❌ 首頁 ❌ 後台頁 ❌ 贊助頁
這樣留言區只會出現在真正需要討論的文章底部,不會污染工具頁或列表頁。
留言區只需要保留簡單分隔:
.comments-section { margin-top: 3rem; padding-top: 2rem; border-top: 1px solid var(--color-border-subtle); }
giscus 內部 iframe 會自己處理主題。這裡使用:
data-theme="preferred_color_scheme"
讓它跟隨使用者系統偏好。
如果網站有自訂深色模式切換,之後可以再監聽 VitePress theme 狀態,透過 postMessage 動態切換 giscus theme。
postMessage
完成後打開任一文章頁:
http://localhost:5183/posts/binary-data/05-FormData混合上傳.html
應該會在文章底部看到 giscus 留言區。
最後跑一次 build:
npm run build
如果 build 通過,代表 SSR 階段沒有被 document 或外部 script 影響。
這次整合的關鍵不是「把 giscus script 貼上去」,而是把留言功能放進 VitePress 的生命週期裡:
pathname
對靜態技術部落格來說,這是一個很平衡的方案:不用自建留言系統,又能保留足夠的管理能力。
在 VitePress 部落格加入 giscus 留言系統
靜態部落格最大的優點是簡單:不用資料庫、不需要使用者系統,也不太需要維護後端。但只要想加上留言功能,事情就會立刻變複雜。留言需要儲存、登入、審核、通知、防垃圾訊息,這些都不是單純貼一個表單就能結束的事情。
這篇記錄如何在 VitePress 部落格中接入 giscus,使用 GitHub Discussions 作為留言資料來源,並把它封裝成一個乾淨的 Vue 元件。
一、為什麼選 giscus
常見的靜態網站留言方案大致有幾類:
這個部落格本身就是技術內容為主,讀者大多能接受 GitHub 登入,因此 giscus 是最省維護成本的選擇。
它的核心模式是:
我們不需要自己建立留言 API,也不需要自己設計資料表。GitHub 會處理登入、留言儲存、通知和基本管理。
二、開啟 GitHub Discussions
先到部落格的 GitHub repository:
勾選 Discussions 後,repository 上方會出現
Discussions分頁。第一次打開 Discussions 時,GitHub 可能會自動建立一篇歡迎討論串,這是正常的。
三、安裝 giscus GitHub App
接著安裝 giscus App:
權限選擇建議:
不要直接授權所有 repositories。giscus 只需要讀取 metadata 和管理 discussions,授權單一 repo 就足夠。
四、產生 giscus 設定
打開:
填入 repository:
如果 repository、Discussions、App 權限都設定正確,giscus 會顯示通過。
4.1 Mapping 選 pathname
頁面與 discussion 的對應方式建議選:
也就是:
原因是文章標題可能會調整,但文章網址通常比較穩定。例如:
如果用 title 或 og:title,之後改標題就有可能找不到原本的 discussion。
4.2 Category 選 Announcements
giscus 建議選擇
Announcements類型的分類,讓新的 discussion 只能由 repository 維護者和 giscus 建立。本次設定如下:
並啟用:
其中
data-loading="lazy"可以避免留言區太早載入,對文章頁效能比較友善。五、不要直接把 script 貼進 Markdown
giscus 會產生一段
<script>:如果只是單頁使用,直接貼也能工作。但在 VitePress 裡,我們希望所有文章頁自動出現留言區,且首頁、後台、文章列表不要出現。因此比較好的做法是封裝成 Vue 元件。
六、封裝 GiscusComments 元件
新增檔案:
核心邏輯如下:
這裡有幾個重點。
第一,script 只在
onMounted後建立,避免 SSR 階段碰到document。第二,路由切換時要清空舊留言,再重新載入。VitePress 是 SPA,如果不處理 route watch,從文章 A 切到文章 B 時,留言區可能仍然停在前一篇。
第三,使用
frontmatter.comments === false讓單篇文章可以關閉留言:七、接到 VitePress Layout
VitePress 預設主題提供
doc-afterslot,位置在文章內容與文章頁尾之間,非常適合放留言區。在:
引入元件:
然後掛到
DefaultTheme.Layout裡:這樣所有文件頁都會經過留言元件,但是否真正顯示,交給
shouldShowComments判斷。八、顯示規則
目前顯示規則是:
這樣留言區只會出現在真正需要討論的文章底部,不會污染工具頁或列表頁。
九、樣式微調
留言區只需要保留簡單分隔:
giscus 內部 iframe 會自己處理主題。這裡使用:
讓它跟隨使用者系統偏好。
如果網站有自訂深色模式切換,之後可以再監聽 VitePress theme 狀態,透過
postMessage動態切換 giscus theme。十、驗證
完成後打開任一文章頁:
應該會在文章底部看到 giscus 留言區。
最後跑一次 build:
如果 build 通過,代表 SSR 階段沒有被
document或外部 script 影響。總結
這次整合的關鍵不是「把 giscus script 貼上去」,而是把留言功能放進 VitePress 的生命週期裡:
pathname負責穩定對應文章doc-afterslot 負責插入文章底部對靜態技術部落格來說,這是一個很平衡的方案:不用自建留言系統,又能保留足夠的管理能力。