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.
I am using app route of nextjs13 approute , this below api call showing me cors error how to solve it.
And calling it here.
Hoping for solving idea :)
This is a pretty cool concept! I experimented with it in the past too! Framing some of these screenshots would've been chef's kiss! @martinp I've built a simple OSS tool for creating engaging screenshots with ease. Check it out and let me know what you think! Cheers!
github.com/ekqt/screenshot
Hi Martin,
I used it in my React application, although i have block, the htmlTemp is still an empty string ("")
Here is my code:
The block looks like this when i debug:
Could you please provide some guidance on how to fix this or point me in the right direction? Any help would be greatly appreciated.
Thank you in advance!
Hi! The
render
functions uses rest parameters. Replacingrenderer.render(block)
byrenderer.render(...block)
should resolve your issue! (and you can plurializeblock
at the same occasion).Basically, a page content contains a list of blocks (paragraphs, bookmarks, quotes, etc). This is why your
getBlogBlock
function returns an arraywhat is their geneorus limiT. Airtable can also do the same.
Airtable is cool but Notion is mostly use by marketing and sales team.
It makes way easier for anyone to write content.
And in my case, sometimes when I have an idea I simply open the Notion App and start writing content and set the status of my page to "Draft".
And when it is ready to be published, I just drag the page within my Kanban view to "Published" and tada it appears on the website!
so simple. How about the limits. 1000 blcoks in free notion.
I have no problems of paying the tools I use daily, but if in your case using Airtable makes more sense, go for it!
If you are intersted, I also wrote an article on how to directly fetch your DEV.to posts for your blog Build a Blog using Next.JS and DEV.to
Hello, i have a question about showing the notion pages as a webpage in your blogpost it mentions the path blog/[slug] so the url would be /blog/my-first-post...
i can get the page to show but not any of the content...