# Fumadocs (Framework Mode, TanStack Start): フィードバック
URL: /ja/docs/tanstack-start/integrations/feedback
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/[framework]/integrations/feedback.ja.mdx

ユーザーからのフィードバックを受け取ります

## 概要 [#overview]

フィードバックを集めると、読者がドキュメントをどう評価しているか、どのページに改善が必要かを把握できます。
Fumadocs には、ページ単位やコンテンツのブロック単位でフィードバックを収集するためのコンポーネントが用意されています。

## インストール [#installation]

**Fumadocs CLI** を使ってインストールします。ドキュメントページが見つかった場合は、そのページにコンポーネントも自動で追加されます。

```npm
npx @fumadocs/cli feature feedback
```

または、コンポーネントのみをインストールすることもできます。

```npm
npx @fumadocs/cli@latest add feedback
```

### ページフィードバック [#page-feedback]

ドキュメントページの最下部に `<Feedback />` を追加します。`onSendAction` コールバックは、読者がフィードバックを送信したときに実行されます。

フィードバックはサーバー関数で処理します。たとえば、`on_rate_docs` イベントとして PostHog に送信できます。

```tsx
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>
  );
}
```

### ブロックフィードバック [#feedback-block]

ブロックフィードバックを使うと、読者はテキストを選択したときに開くポップオーバーから、コンテンツの特定のブロックにコメントできます。

各ブロックにIDを付与するremarkプラグイン`remarkBlockId`を追加します。

```ts tab="unified" title="source.config.ts"
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: [
      // [!code ++]
      [remarkBlockId, blockIdOptions],
    ],
  },
});
```

```ts tab="Sätteri" title="source.config.ts"
import { remarkBlockId } from '@fumadocs/satteri/remark-block-id';
import { defineConfig, defineDocs } from 'fumadocs-mdx/config';

export const docs = defineDocs({
  docs: {
    compiler: 'satteri',
  },
});

export default defineConfig({
  satteriOptions: {
    mdastPlugins: [
      // [!code ++]
      remarkBlockId({ addDataAttribute: 'feedback' }),
    ],
  },
});
```

次に、ページのコンテンツを`<FeedbackText />`でラップします。`onSendAction`コールバックは、読者がブロックに対するフィードバックを送信したときに実行されます。

```tsx
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>
  );
}
```

<Callout type="info" title="補足">
`remark-block-id`はブロックの内容とページ内での順序からブロックIDを生成するため、サードパーティのサービスでもブロックを追跡できます。
</Callout>

### GitHub Discussion との連携 [#integrating-with-github-discussion]

フィードバックを GitHub Discussion に送信するには、以下のファイルをコピーしてベースとして利用できます。

```ts title='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);
  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,
    };
  }
}
```

1. 独自の GitHub App を作成し、その App ID と秘密鍵を取得します。
2. 必要な環境変数を設定します。
3. `owner`、`repo`、`DocsCategory` などの定数を置き換えます。
4. `'use server'` ディレクティブを削除し、サーバー関数から `onPageFeedbackAction` と
   `onBlockFeedbackAction` を呼び出します。