Notion
Publish a Notion database as a docs site.
Pages render on demand, so the site needs a server, unless you prerender them.
For a working project, see examples/notion.
Setup
Create an Integration
Create a Notion integration with permission to read content, then connect your database to it.
Store the Credentials
Store the token and the data source ID in server-only environment variables.
NOTION_TOKEN=ntn_...
NOTION_DATA_SOURCE_ID=...Data source ID
It is not the database ID in the URL. See the Notion upgrade guide.
Install the Packages
npm i @fumadocs/notion @notionhq/clientAdd the Styles
Add this after the imports in your stylesheet:
@import "@fumadocs/notion/css/preset.css";Create the Source
Call createNotion() once. Use the result as the content source, and pass it to the plugin:
import { createNotion } from "@fumadocs/notion";
import notionPlugin from "@fumadocs/notion/press";
import { Client } from "@notionhq/client";
const notion = createNotion({
client: new Client({ auth: process.env.NOTION_TOKEN }),
dataSourceId: process.env.NOTION_DATA_SOURCE_ID!,
});
export default createPress(
notion.dynamicSource({
properties: {
title: "Name",
slug: "Slug",
description: "Description",
},
query: {
filter: {
property: "Published",
checkbox: { equals: true },
},
},
}),
{ alwaysRevalidate: true },
).plugins(notionPlugin({ notion }));alwaysRevalidate shows Notion edits right away. To refresh from a webhook instead, see Refreshing Content.
Customization
Page Properties
Without properties, the source reads:
- The first Notion
titleproperty as the page title. - A property named
Slugas the URL, or a slug generated from the title. - A property named
Description, when there is one.
A slug can contain / for nested URLs. Two pages with the same path fail with an error.
URLs
Use baseDir to place pages under a prefix, or generatePath to set each 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`;
},
});See the Fumadocs Notion integration for all source options.
Custom Blocks
Add or replace block renderers with components:
notionPlugin({
notion,
components: {
callout({ block, children, renderRichText }) {
return (
<aside className="rounded-lg bg-fd-muted p-4">
{renderRichText(block.callout.rich_text)}
{children}
</aside>
);
},
},
});File URLs
Notion file URLs expire after one hour, so the plugin serves them through your site. Turn off this proxy with fileProxy: false only when your content uses permanent external URLs, or a custom renderer copies files to permanent storage.
Prerendering Pages
To deploy to a static host, set mode to force-static. The build reads the database once and prerenders its pages, so edits appear on the next build.
export default createPress(notion.dynamicSource(), {
mode: "force-static",
}).plugins(notionPlugin({ notion, fileProxy: false }));The file proxy needs a server, so turn it off. Files hosted by Notion then expire one hour after the build.
Refreshing Content
The source is read once, so new content doesn't appear on its own. Choose when to read it again:
createPress(source, {
alwaysRevalidate: true,
});Reading on every request works for low traffic. On a busier site, call the webhook when your content changes.
Options
Prop
Type
Last updated on