フィードバック
ユーザーからのフィードバックを受け取ります
概要
フィードバックを集めると、読者がドキュメントをどう評価しているか、どのページに改善が必要かを把握できます。 Fumadocs には、ページ単位やコンテンツのブロック単位でフィードバックを収集するためのコンポーネントが用意されています。
インストール
Fumadocs CLI を使ってインストールします。ドキュメントページが見つかった場合は、そのページにコンポーネントも自動で追加されます。
npx @fumadocs/cli feature feedbackまたは、コンポーネントのみをインストールすることもできます。
npx @fumadocs/cli@latest add feedbackページフィードバック
ドキュメントページの最下部に <Feedback /> を追加します。onSendAction コールバックは、読者がフィードバックを送信したときに実行されます。
フィードバックはサーバー関数で処理します。たとえば、on_rate_docs イベントとして PostHog に送信できます。
import { createServerFn } from '@tanstack/react-start';
import { DocsPage } from 'fumadocs-ui/layouts/docs/page';
import { Feedback } from '@/components/feedback/client';
import { type PageFeedback, pageFeedback } from '@/components/feedback/schema';
const sendFeedback = createServerFn({ method: 'POST' })
.validator((feedback: PageFeedback) => pageFeedback.parse(feedback))
.handler(async ({ data: feedback }) => {
console.log(feedback);
return {};
});
function Content() {
return (
<DocsPage>
{/* ページの最下部に配置 */}
<Feedback onSendAction={(feedback) => sendFeedback({ data: feedback })} />
</DocsPage>
);
}ブロックフィードバック
ブロックフィードバックを使うと、読者はテキストを選択したときに開くポップオーバーから、コンテンツの特定のブロックにコメントできます。
各ブロックにIDを付与するremarkプラグインremarkBlockIdを追加します。
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 { createServerFn } from '@tanstack/react-start';
import { DocsPage } from 'fumadocs-ui/layouts/docs/page';
import { FeedbackText } from '@/components/feedback/client';
import { type BlockFeedback, blockFeedback } from '@/components/feedback/schema';
const sendBlockFeedback = createServerFn({ method: 'POST' })
.validator((feedback: BlockFeedback) => blockFeedback.parse(feedback))
.handler(async ({ data: feedback }) => {
console.log(feedback);
return {};
});
function Content() {
return (
<DocsPage>
<FeedbackText onSendAction={(feedback) => sendBlockFeedback({ data: feedback })}>
{/* ページのコンテンツ */}
</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などの定数を置き換えます。-
'use server'ディレクティブを削除し、サーバー関数からonPageFeedbackActionとonBlockFeedbackActionを呼び出します。
How is this guide?
最終更新日
