フィードバック
ユーザーからのフィードバックを受け取ります
概要
フィードバックを集めると、読者がドキュメントをどう評価しているか、どのページに改善が必要かを把握できます。 Fumadocs には、ページ単位やコンテンツのブロック単位でフィードバックを収集するためのコンポーネントが用意されています。
インストール
Fumadocs CLI を使ってインストールします。ドキュメントページが見つかった場合は、そのページにコンポーネントも自動で追加されます。
npx @fumadocs/cli feature feedbackまたは、コンポーネントのみをインストールすることもできます。
npx @fumadocs/cli@latest add feedbackページフィードバック
ドキュメントページの最下部に <Feedback /> を追加します。onSendAction コールバックは、読者がフィードバックを送信したときに実行されます。
フィードバックをアプリ内のエンドポイントに送信します。
import { DocsPage } from 'fumadocs-ui/layouts/docs/page';
import { Feedback } from '@/components/feedback/client';
function Content() {
return (
<DocsPage>
{/* ページの最下部に配置 */}
<Feedback
onSendAction={async (feedback) => {
const res = await fetch('/api/feedback', {
method: 'POST',
body: JSON.stringify(feedback),
});
return res.json();
}}
/>
</DocsPage>
);
}フィードバックはエンドポイントで処理します。たとえば、on_rate_docs イベントとして PostHog に送信できます。このエンドポイントはリクエストごとにレンダリングされるため、プロジェクトにアダプターが必要です。
import type { APIRoute } from 'astro';
import { pageFeedback } from '@/components/feedback/schema';
export const prerender = false;
export const POST: APIRoute = async ({ request }) => {
const feedback = pageFeedback.parse(await request.json());
console.log(feedback);
return Response.json({});
};ブロックフィードバック
ブロックフィードバックを使うと、読者はテキストを選択したときに開くポップオーバーから、コンテンツの特定のブロックにコメントできます。
各ブロックにIDを付与するremarkプラグインremarkBlockIdを追加します。
import { defineConfig } from 'astro/config';
import fumadocs from '@fumadocs/astro';
import { remarkBlockId } from '@fumadocs/satteri/remark-block-id';
export default defineConfig({
integrations: [
fumadocs({
mdastPlugins: [remarkBlockId({ addDataAttribute: 'feedback' })],
}),
],
});次に、ページのコンテンツを<FeedbackText />でラップします。onSendActionコールバックは、読者がブロックに対するフィードバックを送信したときに実行されます。
import { DocsPage } from 'fumadocs-ui/layouts/docs/page';
import { FeedbackText } from '@/components/feedback/client';
function Content() {
return (
<DocsPage>
<FeedbackText
onSendAction={async (feedback) => {
const res = await fetch('/api/feedback/block', {
method: 'POST',
body: JSON.stringify(feedback),
});
return res.json();
}}
>
{/* ページのコンテンツ */}
</FeedbackText>
</DocsPage>
);
}ページフィードバックと同様に別のエンドポイントで処理します。ただし、リクエストボディはblockFeedbackでパースしてください。
補足
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?
最終更新日
