Notion
Notion からページを取得し、Fumadocs でレンダリングします。
はじめに
@fumadocs/notion を使うと、Notion データベースを Fumadocs のコンテンツソースとして利用できます。公式の Notion API 経由でページを取得する動的なコンテンツソースと、Notion のブロックを Fumadocs UI でレンダリングするサーバーレンダラーを提供します。
クライアントサイドのレンダラーは含まれておらず、コンテンツを MDX に変換することもありません。ブロックはサーバー上でレンダリングされるため、バンドラーや型生成のステップは不要です。
セットアップ
インテグレーションと公式の Notion クライアントをインストールします。
npm install @fumadocs/notion @notionhq/clientFumadocs UI のスタイルの後に、レンダラーの Tailwind CSS ソースをインポートしてください:
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
@import '@fumadocs/notion/css/preset.css';Notion データベース
- コンテンツの読み取り権限を付与した Notion インテグレーションを作成します。
- ページを格納しているデータベースをインテグレーションに接続します。
- インテグレーショントークンとデータベースの データソース ID を、サーバー専用の環境変数に保存します。
NOTION_TOKEN=ntn_...
NOTION_DATA_SOURCE_ID=...データベースの URL に表示されるデータベース ID ではなく、データソース ID を使用してください。現在の Notion API では、データベースはコンテナであり、各クエリはその中のいずれかのデータソースを対象とします。詳しくは Notion のデータソースアップグレードガイドを参照してください。
Notion インテグレーション
インテグレーションは一度だけ作成し、コンテンツソースとファイルルートで共有します。
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() からローダーを公開します。
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 { getSource } from '@/lib/source';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
export default async function Layout({ children }: LayoutProps<'/docs'>) {
const source = await getSource();
return <DocsLayout tree={source.getPageTree()}>{children}</DocsLayout>;
}Notion ファイル
Notion でホストされているファイル(画像、動画、PDF)の URL は約 1 時間で失効するため、キャッシュされるページや静的に配信されるページに直接埋め込むことはできません。
createNotionFileUrlResolver() は、これらの内部アセットをファイルハンドラー経由でルーティングし、リクエストのたびに新しい署名付き URL を取得します。ハンドラーは createNotionFileHandler() でマウントしてください。このハンドラーは、リクエストされたブロックがデータソース内のページに属しているかを検証してからリダイレクトします。
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() を呼び出してください。
export function revalidateNotion() {
return source.revalidate();
}すべてのリクエストで最新のコンテンツを反映させるプレビュー向けの構成では、読み込む前にキャッシュを無効化してください。
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?
最終更新日
