意見回饋
收集使用者的意見回饋
概覽
透過意見回饋,您可以了解讀者對文件的看法,以及哪些頁面需要改進。 Fumadocs 提供多種元件,可針對頁面及內容區塊收集意見回饋。
安裝
請使用 Fumadocs CLI 安裝。若偵測到您的文件頁面,CLI 也會自動將此元件加入該頁面:
npx @fumadocs/cli feature feedback或者,您也可以只安裝此元件:
npx @fumadocs/cli@latest add feedback頁面意見回饋
在文件頁面底部加入 <Feedback />。讀者送出意見回饋時,會執行 onSendAction 回呼函式。
請在 server action 中處理意見回饋。例如,以 on_rate_docs 事件回報至 PostHog:
import { DocsPage } from 'fumadocs-ui/layouts/docs/page';
import { Feedback } from '@/components/feedback/client';
export default async function Page() {
return (
<DocsPage>
{/* 放在頁面底部 */}
<Feedback
onSendAction={async (feedback) => {
'use server';
console.log(feedback);
return {};
}}
/>
</DocsPage>
);
}任何人都能以任意輸入呼叫 server action,因此請在儲存或傳送意見回饋前先進行驗證。
區塊意見回饋
區塊意見回饋可讓讀者針對特定內容區塊提供意見:讀者選取文字時,會開啟一個彈出視窗。
加入 remarkBlockId,這是一個為每個區塊指定 ID 的 remark 外掛:
import {
remarkBlockId,
type RemarkBlockIdOptions,
} from 'fumadocs-core/mdx-plugins/remark-block-id';
import { defineConfig } from 'fumadocs-mdx/config';
const blockIdOptions: RemarkBlockIdOptions = {
addDataAttribute: 'feedback',
};
export default defineConfig({
mdxOptions: {
remarkPlugins: [
[remarkBlockId, blockIdOptions],
],
},
});接著用 <FeedbackText /> 包住您的頁面內容。讀者針對某個區塊送出意見回饋時,便會執行 onSendAction 回呼函式。
import { DocsPage } from 'fumadocs-ui/layouts/docs/page';
import { FeedbackText } from '@/components/feedback/client';
export default async function Page() {
return (
<DocsPage>
<FeedbackText
onSendAction={async (feedback) => {
'use server';
console.log(feedback);
return {};
}}
>
{/* 頁面內容 */}
</FeedbackText>
</DocsPage>
);
}小提示
remark-block-id 會根據區塊的內容及其在頁面中的順序產生區塊 ID,因此第三方服務也能追蹤這些區塊。
與 GitHub Discussion 整合
若要將意見回饋回報至 GitHub Discussion,您可以複製此檔案作為起點:
import { App, Octokit } from 'octokit';
import {
blockFeedback,
BlockFeedback,
pageFeedback,
type ActionResponse,
type PageFeedback,
} from '@/components/feedback/schema';
export const repo = 'fumadocs';
export const owner = 'fuma-nama';
export const DocsCategory = 'Docs Feedback';
let instance: Octokit | undefined;
async function getOctokit(): Promise<Octokit> {
if (instance) return instance;
const appId = process.env.GITHUB_APP_ID;
const privateKey = process.env.GITHUB_APP_PRIVATE_KEY;
if (!appId || !privateKey) {
throw new Error('No GitHub keys provided for Github app, docs feedback feature will not work.');
}
const app = new App({
appId,
privateKey,
});
const { data } = await app.octokit.request('GET /repos/{owner}/{repo}/installation', {
owner,
repo,
headers: {
'X-GitHub-Api-Version': '2022-11-28',
},
});
instance = await app.getInstallationOctokit(data.id);
return instance;
}
interface RepositoryInfo {
id: string;
discussionCategories: {
nodes: {
id: string;
name: string;
}[];
};
}
let cachedDestination: RepositoryInfo | undefined;
async function getFeedbackDestination() {
if (cachedDestination) return cachedDestination;
const octokit = await getOctokit();
const {
repository,
}: {
repository: RepositoryInfo;
} = await octokit.graphql(`
query {
repository(owner: "${owner}", name: "${repo}") {
id
discussionCategories(first: 25) {
nodes { id name }
}
}
}
`);
return (cachedDestination = repository);
}
export async function onPageFeedbackAction(feedback: PageFeedback): Promise<ActionResponse> {
'use server';
feedback = pageFeedback.parse(feedback);
const url = new URL(feedback.url);
return createDiscussionThread(
url.pathname,
`[${feedback.opinion}] ${feedback.message}\n\n> Forwarded from user feedback.`,
);
}
export async function onBlockFeedbackAction(feedback: BlockFeedback): Promise<ActionResponse> {
'use server';
feedback = blockFeedback.parse(feedback);
const url = new URL(feedback.url);
url.hash = feedback.blockId;
return createDiscussionThread(
url.pathname,
`> ${feedback.blockBody}\n\n${feedback.message}\n\n> [Forwarded from user feedback](${url.href}).`,
);
}
async function createDiscussionThread(pageId: string, body: string) {
const octokit = await getOctokit();
const destination = await getFeedbackDestination();
const category = destination.discussionCategories.nodes.find(
(category) => category.name === DocsCategory,
);
if (!category) throw new Error(`Please create a "${DocsCategory}" category in GitHub Discussion`);
const title = `Feedback for ${pageId}`;
const queryResult: {
search: {
nodes: { id: string; title: string; url: string }[];
};
} = await octokit.graphql(`
query {
search(type: DISCUSSION, query: ${JSON.stringify(`"${title}" in:title repo:${owner}/${repo} author:@me`)}, first: 10) {
nodes {
... on Discussion { id, title, url }
}
}
}`);
const discussion = queryResult.search.nodes.find((item) => item.title === title);
if (discussion) {
const result: {
addDiscussionComment: {
comment: { id: string; url: string };
};
} = await octokit.graphql(`
mutation {
addDiscussionComment(input: { body: ${JSON.stringify(body)}, discussionId: "${discussion.id}" }) {
comment { id, url }
}
}`);
return {
githubUrl: result.addDiscussionComment.comment.url,
};
} else {
const result: {
createDiscussion: {
discussion: { id: string; url: string };
};
} = await octokit.graphql(`
mutation {
createDiscussion(input: { repositoryId: "${destination.id}", categoryId: "${category.id}", body: ${JSON.stringify(body)}, title: ${JSON.stringify(title)} }) {
discussion { id, url }
}
}`);
return {
githubUrl: result.createDiscussion.discussion.url,
};
}
}
- 建立您自己的 GitHub App,並取得其 App ID 與私密金鑰。
- 填入必要的環境變數。
- 替換
owner、repo與DocsCategory等常數。 -
在您的意見回饋元件中使用
onPageFeedbackAction與onBlockFeedbackAction。
How is this guide?
最後更新於
