Notion is an extremely powerful tool to manage your content by creating a database you can even add properties to pages: publication date, tags, etc.
In this Post you will learn how to fetch pages from the Notion API and render their content to create a wonderful Next.JS Blog entirely managed with Notion.
1. Create a Notion Database
A Notion Database is a list of pages with defined properties, it provides features to easily manage your content with different type of views (table, calendar, etc).
For the purpose of this guide we will add the following properties:
- Title: The title of the post
- Date: The date of the post
- Status: The status of the post (Not started, Draft, Published)
- Created time: The creation date of the post
Do not forget to create your posts and write some content in them!
Feel free to add your own properties and tweak them to your needs. You could for example add a publication date to automaticaly publish at a certain date.
Find the database ID
Later in this guide you will need the ID of your database. You will find it in the URL: https://www.notion.so/myworkspace/50b6156388e445eaaca3a3599d6f7ade
2. Get a Notion Token
Create an Integration
In order to interact with the Notion API you will need an Internal Integration Token aka. Notion Token.
Head over the following link to create a new Notion Integration. In our case we will only read data, you should only add the Read capacity.
When your integration is created you will have an Internal Integration Token. Save it and keep it safe, it will be the "Notion token" that you will use to authenticate to the API.
Authorize the integration to your databases
You must explicitly give the permission to your integration to query your databases.
Click on the •••
in the top right corner of your database, then on Add connection and select your Integration.
To avoid giving access to each of your databases, you can add the integration to a parent page.
3. Setup the project
Let's install the required dependencies. We are going to use four libraries:
- @notionhq/client The official Notion Javascript SDK
- @notion-render/client A library to transform Notion Blocks (page content) into HTML
- @notion-render/hljs-plugin A plugin to highlight your code blocks
- @notion-render/bookmark-plugin A plugin to fetch website metadata to render bookmarks
$ yarn add @notionhq/client @notion-render/client @notion-render/hljs-plugin @notion-render/bookmark-plugin
# Or
$ npm install @notionhq/client @notion-render/client @notion-render/hljs-plugin @notion-render/bookmark-plugin
Then store your Internal Integration Token and the database Id into your .env.local
file so you can access it later.
NOTION_TOKEN="secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
NOTION_DATABASE_ID="xxxxxxxxxxxxxxxxxxxxxxx"
4. Create the Post Page
Create the Notion Client
Create a new file lib/notion.ts
we will add inside the functions we need to fetch our posts.
import "server-only";
import { Client } from "@notionhq/client";
import React from "react";
import {
BlockObjectResponse,
PageObjectResponse,
} from "@notionhq/client/build/src/api-endpoints";
export const notion = new Client({
auth: process.env.NOTION_TOKEN,
});
export const fetchPages = React.cache(() => {
return notion.databases.query({
database_id: process.env.NOTION_DATABASE_ID!,
filter: {
property: "Status",
select: {
equals: "Published",
},
},
});
});
export const fetchPageBySlug = React.cache((slug: string) => {
return notion.databases
.query({
database_id: process.env.NOTION_DATABASE_ID!,
filter: {
property: "Slug",
rich_text: {
equals: slug,
},
},
})
.then((res) => res.results[0] as PageObjectResponse | undefined);
});
export const fetchPageBlocks = React.cache((pageId: string) => {
return notion.blocks.children
.list({ block_id: pageId })
.then((res) => res.results as BlockObjectResponse[]);
});
You can notice two things:
- import 'server-only';
This line make sure that the file never get imported by the client to avoid leaking your Notion Token.
- React.cache
Next.JS provide an extremely good caching system with the fetch()
function but we can not benefit from it as we are using the Notion JS SDK.
Instead we can use React.cache
, a powerful method that will returns the same result if we call our function with the same parameters.
Create the page
Create a page with a dynamic segment [slug]
. Inside we will fetch our pages so it must be a Server Component:
// app/blog/[slug]/page.tsx
import { fetchPageBlocks, fetchPageBySlug } from "@/lib/notion";
import { notFound } from "next/navigation";
export default async function Page({ params }: { params: { slug: string } }) {
const post = await fetchPageBySlug(params.slug);
if (!post) notFound();
const content = await fetchPageBlocks(post.id);
return <></>;
}
Render the page content
import { fetchPageBlocks, fetchPageBySlug, notion } from "@/lib/notion";
import bookmarkPlugin from "@notion-render/bookmark-plugin";
import { NotionRenderer } from "@notion-render/client";
import hljsPlugin from "@notion-render/hljs-plugin";
import { notFound } from "next/navigation";
export default async function Page({ params }: { params: { slug: string } }) {
const post = await fetchPageBySlug(params.slug);
if (!post) notFound();
const blocks = await fetchPageBlocks(post.id);
const renderer = new NotionRenderer({
client: notion,
});
renderer.use(hljsPlugin());
renderer.use(bookmarkPlugin());
const html = await renderer.render(...blocks);
return <div dangerouslySetInnerHTML={{ __html: html }}></div>;
}
Next steps
- Import your favorite highlight.js theme
- Import the Notion theme from
@notion-render/client/sass/theme.scss
- Create a theme with your own branding
- Use
generateStaticParams
to generate pages at build time - Use
draftMode
to preview your Post not published yet
Top comments (33)
@notion-render/hljs-plugin A plugin to fetch website metadata to render bookmarks
need to be modified to:
@notion-render/bookmark-plugin
The question how can I decode from html/render? Because for doing SEO it is mandatory to implement where to put necessary content so how can I get according to SEO needs?
Hey! I do not entirely understand your needs. But when fetching the page
fetchPageBySlug(params.slug)
it returns the page properties aswell.You can use it inside generateMetadata to use the page here is how I do it on my blog (with dev.to articles).
You can aswell use generateStaticParams to statically generate all your blog pages at build time.
The result: martin-paucot.fr
for metadata instances there has, image property as well how can I get that using notion?
When fetching a page, you would have the properties as well.
developers.notion.com/reference/re...
I am getting this error;
If I remember well the plugin expect a configuration, even empty. So
hljsPlugin({})
I have also used it as an MVP for an API, i would recommend to put things behind a lambda or service to have a contract layer and not be tide to the query or response model from Notion.
The model sent to the Notion API is really complex for no reason (in my opinion).
This is why I am working on NotionX! A library that makes working with Notion extremely easy.
Feel free to follow the project and my Twitter to know when it gets published!
Would love to help with this if there's a possibility of collaboration!
I looked at the code of NotionX and looks much cleaner than the official SDK. Do you have plans to release it on npm to start trying it? :)
Looks so interesting, great job!
Really nice idea.
Tnx for sharing.
cool stuff
Keen to try this tomorrow!!
This is really awesome!
Interesting... Will try this out tomorrow or later
This is really awesome! I've pulled content with the notion api but it's quite tedious. Does your work support metadata or database views? Great work, thank you!
Hey! Thanks for the feedback! I am actually working on an improved fully typed Notion Client, currently it only supports fetching but in a near future the idea is to be able to use it like a library github.com/kerwanp/notionx.
If you talk about external website metadata like the Bookmark block, the Bookmark plugin will fetch them from the targeted website to render the block.
I never had to query database views before but it will be definitely implemented in the future library I'm working on.