Fumadocs

反馈

接收用户反馈

概览

🌐 Overview

反馈对于了解读者的想法至关重要,并能帮助你进一步改进文档内容。

🌐 Feedback is crucial for knowing what your reader thinks, and help you to further improve documentation content.

你可以将一个简单的反馈系统与 Fumadocs 集成。

🌐 You can integrate a simple feedback system with Fumadocs.

安装

🌐 Installation

使用 Fumadocs CLI 安装它。

🌐 Install it using Fumadocs CLI.

npx @fumadocs/cli@latest add feedback

页面反馈

🌐 Page Feedback

要创建页面级反馈界面,请将 <Feedback /> 组件添加到你的文档页面中:

🌐 To create a page-level feedback UI, add the <Feedback /> component to your docs page:

import { DocsPage } from 'fumadocs-ui/layout/docs/page';
import { Feedback } from '@/components/feedback/client';
import posthog from 'posthog-js';

export default async function Page() {
  return (
    <DocsPage>
      
      <Feedback
        onSendAction={async (feedback) => {
          'use server';

          await posthog.capture('on_rate_docs', feedback);
        }}
      />
    </DocsPage>
  );
}
  • onSendAction:当用户提交反馈时触发。

你可以指定一个服务器动作,或任何函数(在客户端组件中)来处理用户反馈。例如,将用户反馈作为 PostHog 上的 on_rate_docs 事件上报。

🌐 You can specify a server action, or any function (in client component) to handle the user feedback. For example, to report user feedback as a on_rate_docs event on PostHog.

反馈模块

🌐 Feedback Block

你还可以配置块级反馈(例如,为每个段落设置反馈弹出框)。

🌐 You can also configure block-level feedback (e.g. a feedback popover for each paragraph).

添加 remark-feedback-block Remark 插件:

🌐 Add the remark-feedback-block Remark plugin:

source.config.ts (Fumadocs MDX)
import { remarkFeedbackBlock } from 'fumadocs-core/mdx-plugins/remark-feedback-block';
import { defineConfig } from 'fumadocs-mdx/config';

export default defineConfig({
  mdxOptions: {
    remarkPlugins: [remarkFeedbackBlock],
  },
});

然后,定义 FeedbackBlock MDX 组件:

🌐 Then, define the FeedbackBlock MDX component:

mdx-components.tsx
import { FeedbackBlock } from '@/components/feedback/client';
import defaultComponents from 'fumadocs-ui/mdx';
import type { MDXComponents } from 'mdx/types';

export function getMDXComponents(components?: MDXComponents): MDXComponents {
  return {
    ...defaultComponents,
    FeedbackBlock: (props) => (
      <FeedbackBlock {...props} onSendAction={onBlockFeedbackAction}>
        {children}
      </FeedbackBlock>
    ),
    ...components,
  };
}
  • onSendAction:当用户提交反馈时触发。

知道了

remark-feedback-block 根据内容和页面中的顺序生成块 ID,因此也可以从第三方服务跟踪这些块。

与 GitHub 讨论集成

🌐 Integrating with GitHub Discussion

要将你的反馈报告给 GitHub 讨论,你可以将此文件复制作为起点:

🌐 To report your feedback to GitHub Discussion, you can copy this file as a starting point:

lib/github.ts
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);
  return createDiscussionThread(
    feedback.url,
    `[${feedback.opinion}] ${feedback.message}\n\n> Forwarded from user feedback.`,
  );
}

export async function onBlockFeedbackAction(feedback: BlockFeedback): Promise<ActionResponse> {
  'use server';
  feedback = blockFeedback.parse(feedback);
  return createDiscussionThread(
    feedback.url,
    `> ${feedback.blockBody ?? feedback.blockId}\n\n${feedback.message}\n\n> Forwarded from user feedback.`,
  );
}

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 {
    search: {
      nodes: [discussion],
    },
  }: {
    search: {
      nodes: { id: string; url: string }[];
    };
  } = await octokit.graphql(`
          query {
            search(type: DISCUSSION, query: ${JSON.stringify(`${title} in:title repo:${owner}/${repo} author:@me`)}, first: 1) {
              nodes {
                ... on Discussion { id, url }
              }
            }
          }`);

  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: {
      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.discussion.url,
    };
  }
}
  1. 创建你自己的 GitHub 应用并获取其应用 ID 和私钥。
  2. 填写必需的环境变量。
  3. 替换类似 ownerrepoDocsCategory 的常量。
  4. 在你的反馈组件中使用 onPageFeedbackActiononBlockFeedbackAction

Last updated on

On this page