notion-rsc
v0.0.5
Published
Turns Notion pages into React Server Components
Downloads
2
Readme
notion-rsc
Build your Next.js blog using Notion as a CMS with only two server components. It is even better with tailwindcss.
[!WARNING]
notion-rsc
is not affiliated with nor endorsed by Notion Labs, Inc. Also, these RSC are made as an experiment: I use them on my personal blog. If you decide to use them in production, it is at your own risk.
Getting Started
$ npm install notion-rsc
const { NotionPost, NotionPosts, getPost, getPosts } = createNotionComponents(
"Notion API key",
"Notion blog database id"
);
To allow the Notion API to access you database, you must create an Internal Integration.
It boils down to creating a new integration at notion.so/my-integrations and then add a connection with it on your database page.
The API key used by createNotionComponents
is the Internal Integration Secret you will find on the page of your integration.
Basic usage
If you don't give any more parameters to createNotionComponents
, it will use a default Post type, which is the following:
type Post = {
content: React.ReactNode[];
createdAt: string;
published: boolean;
slug: string;
title: string;
};
Thus, your Notion database should contain the appropriate fields: slug (main column), title (text), publish (checkbox), Created Time (default Created Time type). Content corresponds to the nested notion page you will create for each given database row.
You can duplicate this page as a template to start right away.
The NotionPost
and NotionPosts
components are used like that:
<NotionPosts renderPost={(post) => <p>{post.title}</p>} />
NotionPosts
is the component made to list your posts. It takes only one parameter: renderPost
. You keep the full control UI control.
<NotionPost
id={slug}
renderPost={(post) => <article>{post.content}</article>}
/>
[!NOTE]
As you probably noticed,content
is of typeReact.ReactNode[]
. Check out the What's under the hood section to see what it renders to and a great tip to make it a fully designed blog in seconds thanks to @tailwindcss/typography.
NotionPost
is the component made to render individual posts. It takes two parameters: id
and renderPost
. By default, the id
used is the post slug. And again, renderPost
gives you full UI control.
Finally, getPost
and getPosts
are exposed in case you would need it. Let's say you wanted to statically generate your blog routes at build time:
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
What's under the hood?
Under the hood, notion-rsc
:
- Fetches your posts from your notion database using @notionhq/client...
- ...while applying a filter to get only the ones marked as
published
and another filter to get the post corresponding to theid
you gave to<NotionPost />
(taking the first one, if ids are not unique in your database) - Parses each post/row of your database to get you back a proper
Post
object you can then make part of your UI using therenderPost
prop... - ...while parsing the
content
of your post (which is the Notion page nested inside each post/row of your database) to make it proper HTML. Example: Notion's# This is a heading 1
becomes<h1>This is a heading 1</h1>
[!TIP]
The very cool thing with getting your Notion page as HTML is that now you can leverage @tailwindcss/typography to instantly "add beautiful typographic defaults" to your blog. Instant dark theme out of the box as well. And fully customizable.
API Reference
createNotionComponents(auth, dbId, options)
It is the entrypoint of the package. It returns { NotionPost, NotionPosts, getPost, getPosts }
. It is a generic function. Its signature with type params is:
function createNotionComponents<T extends object = Post>(
auth: string,
dbId: string,
options?: {
parsePost?: (client: Client, page: PageObjectResponse) => Promise<T>;
postQueryFilter?: (id: string) => QueryDatabaseParameters["filter"];
postsQueryFilter?: QueryDatabaseParameters["filter"];
}
);
Check Client
type in Notion's code. Useful to use utility functions.
Check PageObjectReponse
type in Notion's code. You can check here all the page properties you can use.
Check QueryDatabaseParameters["filter"]
type in Notion's code. It is the type of a database filter, as Notion explains here.
auth
- type: string
See Getting started to know how to get it.
dbId
- type: string
Follow this section of Notion documentation to get it.
options.parsePost
(optional)
(client: Client, page: PageObjectResponse) => Promise<T>;
// Default:
const defaultPostParser = async (
client: Client,
page: PageObjectResponse
): Promise<Post> => ({
content: await defaultNotionPageParser(client, page),
slug: (page.properties.slug as any).title[0].plain_text,
title: (page.properties.title as any).rich_text[0].plain_text,
published: (page.properties.published as any).checkbox,
createdAt: page.created_time.split("T")[0],
});
Allows to turn pages to a js object with the shape you want.
You also have acces to the notion client
to leverage utility functions if you want to.
In the default, we use the client in defaultNotionPageParser
to iterate over each block in the page and parse them.
options.postQueryFilter
(optional)
(id: string) => QueryDatabaseParameters["filter"];
// Default:
const defaultPostQueryFilter = (id: string) => ({
and: [
{ property: "published", checkbox: { equals: true } },
{ property: "slug", title: { equals: id } },
],
});
Allows to filter posts to get the one you want, when using <NotionPost />
.
id
param corresponds to the id
you will give as a prop to <NotionPost />
.
The default queries posts that are published
and with the slug
corresponding to id
.
Note: if multiple posts come out from this query, the first matching one will be kept.
options.postsQueryFilter
(optional)
QueryDatabaseParameters["filter"];
// Default:
const const defaultPostsQueryFilter = {
property: "published",
checkbox: { equals: true },
});
Allows to filter posts to get the ones you want, when using <NotionPosts />
.
The default queries posts that are published
.
Utility functions
defaultNotionPageParser(client, page)
const defaultNotionPageParser = async (
client: Client,
page: PageObjectResponse
): Promise<React.ReactNode[]>;
Check createNotionComponents(auth, dbId, options) section to understand the types.
defaultNotionPageParser
enables you to parse the blocks of a page. It is used in the default options.parsePost
function, and you can use it as well if you want to create your own blog database without rewriting the block parsing.
defaultNotionBlockParser(block, notionPublicFolder)
const defaultNotionBlockParser = async (
block: BlockObjectResponse,
notionPublicFolder: string = `${process.cwd()}/public/notion-files`
): Promise<JSX.Element | undefined>;
Check BlockObjectResponse
type in Notion's code. It describes a Notion supported block.
defaultNotionBlockParser
enables you to turn a block into a JSX.Element
. This way, if you want to change the defaultNotionPageParser
logic, but want to keep the block to HTML logic, you can use this function.
Also, if a notion block is not supported yet, you can create your own function to wrap this one, and support the block you need.
Finally, you could create your own syntax in Notion and parse it here with some new logic.
block
is the Notion block to parse.
notionPublicFolder
is the folder where the Notion files and images found in the blocks will be saved. In the future, this might be replaced by something to handle file uploading to S3 (with AWS sdk or UploadThing).