コンテンツへスキップ
Fumadocs

Notion

Notion からページを取得し、Fumadocs でレンダリングします。

はじめに

@fumadocs/notion を使うと、Notion データベースを Fumadocs のコンテンツソースとして利用できます。公式の Notion API 経由でページを取得する動的なコンテンツソースと、Notion のブロックを Fumadocs UI でレンダリングするサーバーレンダラーを提供します。

クライアントサイドのレンダラーは含まれておらず、コンテンツを MDX に変換することもありません。ブロックはサーバー上でレンダリングされるため、バンドラーや型生成のステップは不要です。

セットアップ

インテグレーションと公式の Notion クライアントをインストールします。

npm install @fumadocs/notion @notionhq/client

Fumadocs UI のスタイルの後に、レンダラーの Tailwind CSS ソースをインポートしてください:

Tailwind CSS
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
@import '@fumadocs/notion/css/preset.css';

Notion データベース

  1. コンテンツの読み取り権限を付与した Notion インテグレーションを作成します。
  2. ページを格納しているデータベースをインテグレーションに接続します。
  3. インテグレーショントークンとデータベースの データソース ID を、サーバー専用の環境変数に保存します。
.env.local
NOTION_TOKEN=ntn_...
NOTION_DATA_SOURCE_ID=...

データベースの URL に表示されるデータベース ID ではなく、データソース ID を使用してください。現在の Notion API では、データベースはコンテナであり、各クエリはその中のいずれかのデータソースを対象とします。詳しくは Notion のデータソースアップグレードガイドを参照してください。

Notion インテグレーション

インテグレーションは一度だけ作成し、コンテンツソースとファイルルートで共有します。

lib/notion.ts
import { Client } from '@notionhq/client';
import { createNotion } from '@fumadocs/notion';

export const notion = createNotion({
  client: new Client({ auth: process.env.NOTION_TOKEN }),
  dataSourceId: process.env.NOTION_DATA_SOURCE_ID!,
});

コンテンツローダー

notion.dynamicSource() を dynamicLoader() に渡し、getSource() からローダーを公開します。

lib/source.ts
import { dynamicLoader } from 'fumadocs-core/source';
import { notion } from '@/lib/notion';

const source = dynamicLoader(
  notion.dynamicSource({
    // Notion のプロパティをページのフィールドにマッピングします
    properties: {
      title: 'Name',
      slug: 'Slug',
      description: 'Description',
    },
    // フィルタリングや並べ替えなど、クエリを自由にカスタマイズします
    query: {
      filter: {
        property: 'Published',
        checkbox: { equals: true },
      },
    },
  }),
  {
    baseUrl: '/docs',
  },
);

export async function getSource() {
  return source.get();
}

このソースは、条件に一致するデータソースの行をすべてページネーションで取得し、各ページのネストされたブロックを遅延読み込みします。同じページ本文を読み込む呼び出しは、ローダーが無効化されるまで重複排除されます。

ページをレンダリングする

レイアウトでローダーを使用してページツリーを構築します。ページのブロックを読み込み、NotionRenderer でレンダリングします。目次は blocksToTableOfContents() で生成します。

import type { ReactNode } from 'react';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { getSource } from '@/lib/source';

export default async function Layout({ children }: { children: ReactNode }) {
  const source = await getSource();

  return <DocsLayout tree={source.getPageTree()}>{children}</DocsLayout>;
}

Notion ファイル

Notion でホストされているファイル(画像、動画、PDF)の URL は約 1 時間で失効するため、キャッシュされるページや静的に配信されるページに直接埋め込むことはできません。

createNotionFileUrlResolver() は、これらの内部アセットをファイルハンドラー経由でルーティングし、リクエストのたびに新しい署名付き URL を取得します。ハンドラーは createNotionFileHandler() でマウントしてください。このハンドラーは、リクエストされたブロックがデータソース内のページに属しているかを検証してからリダイレクトします。

src/pages/_api/api/notion/file.ts
import { createNotionFileHandler } from '@fumadocs/notion';
import { notion } from '@/lib/notion';

export const GET = createNotionFileHandler(notion);

デフォルトのルートは /api/notion/file です。別の場所にマウントする場合は、両方の関数に同じ path を渡してください。

createNotionFileUrlResolver({ pageId: page.data.id, path: '/notion/file' });

静的エクスポートを行う場合は、アセットに恒久的な外部 URL を使用し、getFileUrl を省略してください。この場合、レンダラーは Notion の URL を直接リンクします。ファイルハンドラーには動的なランタイムが必要です。

完了

データベースにページを追加して公開済みに設定すると、docs ルート配下に表示されます。

再検証

ローダーは、キャッシュを無効化するまでページをキャッシュし続けます。Notion での編集内容を反映するには、信頼できる Webhook または再検証用のルートから revalidate() を呼び出してください。

lib/source.ts
export function revalidateNotion() {
  return source.revalidate();
}

すべてのリクエストで最新のコンテンツを反映させるプレビュー向けの構成では、読み込む前にキャッシュを無効化してください。

lib/source.ts
export async function getSource() {
  source.invalidate(); 
  return source.get();
}

ページプロパティ

properties は、Notion のプロパティをページのフィールドにマッピングします。省略した場合、ソースは次のように動作します。

  • 最初の Notion title プロパティをページタイトルとして使用します。
  • Slug という名前のプロパティを URL に使用します。存在しない場合は、タイトルから生成したスラッグにフォールバックします。
  • Description という名前のプロパティがあれば、それを使用します。

スラッグに / を含めると、ネストされたルートを作成できます。仮想パスが重複している場合や安全でない場合は、ページを暗黙的に置き換えるのではなく、内容がわかるエラーを出して失敗します。

クエリとパス

Notion のフィルターとソートは query で渡します。仮想ページを特定のディレクトリ配下に配置するには baseDir を、仮想ファイルのパスを完全に制御するには generatePath を使用します。

notion.dynamicSource({
  baseDir: 'docs',
  query: {
    sorts: [{ timestamp: 'last_edited_time', direction: 'descending' }],
  },
  generatePath(page, info) {
    return info.slugs.length > 0 ? `${info.slugs.join('/')}.mdx` : `${page.id}.mdx`;
  },
});

レンダリングのカスタマイズ

NotionRenderer には、一般的なブロックタイプ用のレンダラーが組み込まれています。components を渡すと、レンダラーを追加または上書きできます。上書きするレンダラーには、それぞれ対応するブロックに応じた型が付きます。

import { NotionRenderer } from '@fumadocs/notion';

<NotionRenderer
  blocks={blocks}
  components={{
    callout({ block, children, renderRichText }) {
      return (
        <aside className="rounded-lg bg-fd-muted p-4">
          {renderRichText(block.callout.rich_text)}
          {children}
        </aside>
      );
    },
  }}
/>;

コードブロックは、デフォルトでサーバー側で Shiki によってハイライトされます。ハイライトせずにプレーンなコードとしてレンダリングするには highlightCode={false} を、独自のハイライターを組み込むには関数を渡してください。

ソースのみのインポート

検索インデクサーや llms.txt ルートなど、Notion ページを取得するだけのコードでは、@fumadocs/notion/source からインポートすることで、React レンダラーのエントリーを読み込まずに済みます:

import { createNotion } from '@fumadocs/notion/source';

このエントリーポイントでは Notion SDK を外部依存として扱い、React のインポートは一切含みません。blocksToPlainText()、blocksToStructuredData()、blocksToTableOfContents() もこちらから利用できます。

How is this guide?

最終更新日