跳至主要内容

留言/評論功能

Docusaurus本身並沒有留言功能,所以當時在找第三方留言系統,意外發現有滿多Docusaurus相關教學,我主要考量的點是可以匿名留言和不要廣告。

一開始,是想說可能還是會有人想要聯繫我,所以用Google Forms製作聯絡表單,但收到的訊息多是廣告訊息,想了想,相較花時間打長篇大論的聯絡表單,不如在每個頁面下放置留言區,更方便和即時互動。

社群帳號登入的留言系統

本身沒有使用社群媒體,另外基於社群帳號登入的評論系統,我個人覺得有蒐集數據之嫌疑,所以最後沒有選擇這類的留言系統。

基於社群帳號登入的留言系統,比較有名就是Facebook留言外掛程式和Disqus,話說Docusaurus是Meta的開源項目,但卻沒有將Facebook留言外掛程式納入;而Disqus是老牌、知名、常見的留言系統,免費版本有廣告,似乎有個資的問題,可用多種社群媒體登入留言。

Disqus

Waline

github登入的留言系統

沒有考慮過這個選項,因為會看這個網站的人沒有github帳號應該是遠遠高於擁有的比例,但會用靜態網頁產生器架站的人,有github帳號比例非常高,所以相關的文章反而是最多的

Gitalk

愧怍-Docusaurus配置Gitalk评论插件

Giscus

utterances

不須登入的留言系統

Cusdis

選擇Cusdis,一款開源、輕量、隱私的評論系統,因為它可以完全匿名留言。

優點就是樣式簡潔,留言不需用戶登入,不需輸入信箱,即可留言,完全匿名,提供免費托管服務及自行部屬兩種方式,我當然就是用官方免費提供的dashbord。

留言採事前審核機制,決定留言是否發布、直接回覆或刪除留言,可設定新留言電子信箱通知,並在信件內直接審核或回覆留言,另外,React有cusdis官方組件,剛好Docusaurus本身也是用React。

缺點就是沒有垃圾留言過濾,所有的留言都需要自己逐筆審核。

添加Cusdis留言功能

官方的教學是用Swizzle組件,因為我不懂程式,老實說看不太懂,所以是將檔案直接複製到src/theme下修改。

  1. 安裝 react-cusdis
npm i react-cusdis
  1. 檔案位置
  • 創建src/theme資料夾,用來放複製模板檔案。
  • 文檔的部分原始檔的目錄位置 @docusaurus\theme-classic\src\theme\DocItem\DocItem
  • 部落格的部分原始檔的目錄位置 @docusaurus\theme-classic\src\theme\BlogPostPage\index.tsx
  1. 在複製的模板檔案添加Cusdis
src/theme/DocItem/index.tsx
import React from 'react';
import {HtmlClassNameProvider} from '@docusaurus/theme-common';
import {DocProvider} from '@docusaurus/theme-common/internal';
import DocItemMetadata from '@theme/DocItem/Metadata';
import DocItemLayout from '@theme/DocItem/Layout';
import type {Props} from '@theme/DocItem';
import { ReactCusdis } from 'react-cusdis'

export default function DocItem(props: Props): JSX.Element {
const docHtmlClassName = `docs-doc-id-${props.content.metadata.unversionedId}`;
const MDXComponent = props.content;
return (
<DocProvider content={props.content}>
<HtmlClassNameProvider className={docHtmlClassName}>
<DocItemMetadata />
<DocItemLayout>
<MDXComponent />
<ReactCusdis
lang="zh-tw" //繁體中文
attrs={{
host: 'https://cusdis.com',
appId: 'data-app-id',
pageId: MDXComponent.metadata.permalink,
pageTitle: MDXComponent.metadata.title,
pageUrl: 'https://from8to8.com/'+MDXComponent.metadata.permalink,
}}
/>
</DocItemLayout>
</HtmlClassNameProvider>
</DocProvider>
);
}
src/theme/BlogPostPage\index.tsx
import React, {type ReactNode} from 'react';
import clsx from 'clsx';
import {HtmlClassNameProvider, ThemeClassNames} from '@docusaurus/theme-common';
import {BlogPostProvider, useBlogPost} from '@docusaurus/theme-common/internal';
import BlogLayout from '@theme/BlogLayout';
import BlogPostItem from '@theme/BlogPostItem';
import BlogPostPaginator from '@theme/BlogPostPaginator';
import BlogPostPageMetadata from '@theme/BlogPostPage/Metadata';
import TOC from '@theme/TOC';
import type {Props} from '@theme/BlogPostPage';
import type {BlogSidebar} from '@docusaurus/plugin-content-blog';
import { ReactCusdis } from 'react-cusdis'

function BlogPostPageContent({
sidebar,
children,
}: {
sidebar: BlogSidebar;
children: ReactNode;
}): JSX.Element {
const {metadata, toc} = useBlogPost();
const {nextItem, prevItem, frontMatter} = metadata;
const {
hide_table_of_contents: hideTableOfContents,
toc_min_heading_level: tocMinHeadingLevel,
toc_max_heading_level: tocMaxHeadingLevel,
} = frontMatter;
return (
<BlogLayout
sidebar={sidebar}
toc={
!hideTableOfContents && toc.length > 0 ? (
<TOC
toc={toc}
minHeadingLevel={tocMinHeadingLevel}
maxHeadingLevel={tocMaxHeadingLevel}
/>
) : undefined
}
>
<BlogPostItem>{children}</BlogPostItem>
<ReactCusdis
lang="zh-tw"
attrs={{
host: 'https://cusdis.com',
appId: 'data-app-id',
pageId: metadata.permalink,
pageTitle: metadata.title,
pageUrl: 'https://from8to8.com'+metadata.permalink,
}}
/>

{(nextItem || prevItem) && (
<BlogPostPaginator nextItem={nextItem} prevItem={prevItem} />
)}
</BlogLayout>
);
}

export default function BlogPostPage(props: Props): JSX.Element {
const BlogPostContent = props.content;
return (
<BlogPostProvider content={props.content} isBlogPostPage>
<HtmlClassNameProvider className={clsx(ThemeClassNames.wrapper.blogPages, ThemeClassNames.page.blogPostPage)}>
<BlogPostPageMetadata />
<BlogPostPageContent sidebar={props.sidebar}>
<BlogPostContent />
</BlogPostPageContent>
</HtmlClassNameProvider>
</BlogPostProvider>
);
}