Notion
Pull pages from Notion and render them with Fumapress.
The @fumapress/notion package provides both a dynamic content source and the adapter plugin needed to render Notion blocks. It uses the official Notion API and does not ship a client-side renderer.
Installation
Install the integration and the official Notion client:
npm i @fumapress/notion @notionhq/clientImport the renderer's Tailwind source in your global CSS after the Fumadocs UI styles:
@import "@fumapress/notion/css/preset.css";Notion Setup
- Create a Notion integration with permission to read content.
- Connect the database containing your pages to the integration.
- Store the integration token and the database's data source ID in server-only environment variables.
NOTION_TOKEN=ntn_...
NOTION_DATA_SOURCE_ID=...Use a data source ID, not the database ID shown in the database URL. In the current Notion API, a database is a container and each query targets one of its data sources. See Notion's data source upgrade guide.
Configuration
For a runnable project, see examples/notion.
Create the shared integration once, use it as the Fumapress content source, and pass the same object to notionPlugin():
import { Client } from "@notionhq/client";
import { fumapressNotion, notionPlugin } from "@fumapress/notion";
import { defineConfig } from "fumapress";
const notion = fumapressNotion({
client: new Client({ auth: process.env.NOTION_TOKEN }),
dataSourceId: process.env.NOTION_DATA_SOURCE_ID!,
});
export default defineConfig({
content: notion.dynamicSource({
properties: {
title: "Name",
slug: "Slug",
description: "Description",
},
query: {
filter: {
property: "Published",
checkbox: { equals: true },
},
},
}),
loaderOptions: {
alwaysRevalidate: true,
},
mode: "dynamic",
}).plugins(notionPlugin(notion));The source paginates through every matching data-source row and recursively pulls nested page blocks. Calls to load the same page body are deduplicated until the content loader is invalidated.
alwaysRevalidate is the simplest way to reflect Notion edits immediately. For a higher-traffic site, leave it disabled and call revalidateLoader() from a trusted webhook or revalidation route instead.
Page Properties
When properties is omitted, the source:
- Uses the first Notion
titleproperty as the page title. - Uses a property named
Slugfor the URL, falling back to a slug generated from the title. - Uses a property named
Descriptionwhen present.
A slug can contain / to create nested routes. Duplicate or unsafe virtual paths fail with a descriptive error instead of silently replacing a page.
Query and Paths
Pass Notion filters and sorts through query. Use baseDir to place the virtual pages under a directory, or generatePath for complete control over the virtual file path.
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`;
},
});Prop
Type
Prop
Type
Rendering
notionPlugin() contributes renderers for common block types, you can add or override the default renderers.
notionPlugin(notion, {
components: {
callout({ block, children, renderRichText }) {
return (
<aside className="rounded-lg bg-fd-muted p-4">
{renderRichText(block.callout.rich_text)}
{children}
</aside>
);
},
},
});Notion Files
Notion-hosted file URLs expire after one hour. By default, notionPlugin() renders internal assets through /api/notion/file, which retrieves a fresh signed URL and verifies that the requested block belongs to a page in the configured data source.
The proxy needs a dynamic deployment. For a static export, disable it only when your content uses durable external URLs or when a custom renderer copies assets to durable storage:
notionPlugin(notion, {
fileProxy: false,
});Prop
Type
Source-only Import
Code that only needs to pull Notion pages can avoid the React renderer entry:
import { fumapressNotion } from "@fumapress/notion/source";This entry keeps the Notion SDK external and contains no React imports.
Last updated on
