DEV Community

Cover image for Use Notion as a database for your Next.JS Blog
Martin PAUCOT
Martin PAUCOT

Posted on • Updated on

Use Notion as a database for your Next.JS Blog

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.

Image description

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.

Image description

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:

$ 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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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[]);
});

Enter fullscreen mode Exit fullscreen mode

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 <></>;
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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 (29)

Collapse
 
huytq085 profile image
Huy Tran

@notion-render/hljs-plugin A plugin to fetch website metadata to render bookmarks
need to be modified to:
@notion-render/bookmark-plugin

Collapse
 
alvarolorentedev profile image
Alvaro

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.

Collapse
 
martinp profile image
Martin PAUCOT • Edited

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!

Collapse
 
coffeegerm profile image
David Yarzebinski

Would love to help with this if there's a possibility of collaboration!

Collapse
 
itsjavidotcom profile image
Javi Aguilar

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? :)

Collapse
 
rmmgc profile image
Ramo Mujagic

Really nice idea.
Tnx for sharing.

Collapse
 
maxbrych profile image
MaxBrych

cool stuff

Collapse
 
ch3ber profile image
Eber Alejo

Looks so interesting, great job!

Collapse
 
hasanulhaquebanna profile image
Hasanul Haque Banna

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?

Collapse
 
martinp profile image
Martin PAUCOT

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

Collapse
 
karaca19 profile image
Bora Karaca

Interesting... Will try this out tomorrow or later

Collapse
 
madzimai profile image
Netsai

This is really awesome!

Collapse
 
jrrs1982 profile image
Jeremy Smith

Keen to try this tomorrow!!

Collapse
 
jonmccon profile image
jonmccon

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!

Collapse
 
martinp profile image
Martin PAUCOT

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.

Collapse
 
tilakkhatri profile image
Tilak Khatri

I am using app route of nextjs13 approute , this below api call showing me cors error how to solve it.

Image description

Image description

And calling it here.

Image description

Hoping for solving idea :)

Collapse
 
ekqt profile image
Hector Sosa • Edited

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

Collapse
 
thesonpb profile image
Nguyễn Thế Sơn • Edited

Hi Martin,

I used it in my React application, although i have block, the htmlTemp is still an empty string ("")

Image description

Here is my code:

                const block = await getBlogBlock(id);
                const renderer = new NotionRenderer({
                    client: notion,
                });
                renderer.use(hljsPlugin());
                renderer.use(bookmarkPlugin());

                const htmlTemp = await renderer.render(block);
                setHtml(htmlTemp);
Enter fullscreen mode Exit fullscreen mode

The block looks like this when i debug:

Image description

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!

Collapse
 
martinp profile image
Martin PAUCOT

Hi! The render functions uses rest parameters. Replacing renderer.render(block) by renderer.render(...block) should resolve your issue! (and you can plurialize block 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 array

Collapse
 
awaisalwaisy profile image
Alwaisy al-waisy

what is their geneorus limiT. Airtable can also do the same.

Collapse
 
martinp profile image
Martin PAUCOT

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!

Collapse
 
awaisalwaisy profile image
Alwaisy al-waisy

so simple. How about the limits. 1000 blcoks in free notion.

Thread Thread
 
martinp profile image
Martin PAUCOT

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

Collapse
 
kasperbuchholtz profile image
Kb

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...