This guide demonstrates how to use Notion as a dynamic CMS backend for your Next.js app. With this integration, any changes made in Notion will instantly update your app, enabling seamless real-time synchronization without the need for redeployment.
npm i @notionhq/client
npm i @notion-render/client
npm i @notion-render/hljs-plugin
notion.ts
in a new folder called lib
//notion.ts content
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_ARTICLES!,
filter: {
property: "Status",
status: {
equals: "Completed",
},
},
});
});
export const fetchPageBySlug = React.cache((slug: string) => {
return notion.databases
.query({
database_id: process.env.NOTION_DATABASE_ID_ARTICLES!,
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[]);
});
After entering Notion, create a new page and start a database by typing /Database-inline
or /Gallery view
.
Here is an example of a Notion page link, with the page ID highlighted in red:
https://www.notion.so/7707aa0a9b2644429e40635f8b14cca6?v=d0bcbb1785af4ec59dadc0b21fe135be
Slug
and Status
.Status:
Slug:
Note: Preserve the original case (uppercase or lowercase) when naming properties.
Finally, connect your page using your Notion token.
Create a .env
file and insert your Notion token and Notion page ID
NOTION_TOKEN="********************************************************"
NOTION_DATABASE_ID="********************************"
import React from "react";
import { fetchPages } from "@/lib/notion";
const Page = async () => {
const posts = await fetchPages();
return (
<div>Page</div>
)
};
export default Page;
The posts.results
array contains all the data, so using the map
function is essential to display it.
{posts.results.map((post: any) => {
//Title
<h2>{post.properties.Title.title[0].plain_text}</h2>
//Other properties, such as date or description, can also be added
//Date
<h3>{post.properties.Date.date.start}</h3>
//Description
<p>{post.properties.Description.rich_text[0].plain_text}</p>
})
In general, you can add any properties by adhering to the schema of the desired properties.
//Exaple : Logging posts.resutls[0] to the console.
{
object: 'page',
id: '73abc49c-8608-4a68-9348-a5b662ff8f34',
created_time: '2024-05-13T13:47:00.000Z',
last_edited_time: '2024-08-17T11:13:00.000Z',
created_by: { object: 'user', id: '0127936b-3791-47a7-ba36-063587d039b2' },
last_edited_by: { object: 'user', id: '0127936b-3791-47a7-ba36-063587d039b2' },
cover: null,
icon: null,
parent: {
type: 'database_id',
database_id: '7edf7558-91ad-432e-b868-6ef469cb3671'
},
archived: false,
in_trash: false,
//Next, we'll target the specific propertie: posts.resutls[0].properties
properties: {
Date: { id: '%3FqpY', type: 'date', date: [Object] },
Tags: { id: 'ZG%5EE', type: 'multi_select', multi_select: [Array] },
Link: {
id: '%5B_%5Cf',
type: 'url',
url: 'https://brahmihoussem.github.io/react-P_2_-chatMenu/'
},
Status: { id: 'elCU', type: 'status', status: [Object] },
Description: { id: 'u%3A%5Eb', type: 'rich_text', rich_text: [Array] },
//Next, we'll target the Title: posts.resutls[0].properties.Title.title[0].plain_text
Title: { id: 'title', type: 'title', title:
[
{
type: 'text',
text: { content: 'Chat app', link: null },
annotations: {
bold: false,
italic: false,
strikethrough: false,
underline: false,
code: false,
color: 'default'
},
plain_text: 'Chat app',
href: null
}
]
}
},
url: 'https://www.notion.so/Chat-app-73abc49c86084a689348a5b662ff8f34',
public_url: 'https://tungsten-liquid-d75.notion.site/Chat-app-73abc49c86084a689348a5b662ff8f34'
}
To handle the slug, paste this code into the [slug]
directory.
import { fetchPageBlocks, fetchPageBySlug, notion } from "@/lib/notion";
import React from "react";
import { NotionRenderer } from "@notion-render/client";
import hljsPlugin from "@notion-render/hljs-plugin";
const Page = async ({ params }: { params: { slug: string } }) => {
const post = await fetchPageBySlug(params.slug);
if (!post) {
return <div>Post not found</div>;
}
const blocks = await fetchPageBlocks(post.id);
const renderer = new NotionRenderer({
client: notion,
});
renderer.use(hljsPlugin({}));
const html = await renderer.render(...blocks);
return (
<>
<div
className="mx-auto "
dangerouslySetInnerHTML={{ __html: html }}
></div>
</>
);
};
export default Page;
Finally, for styling, use the inspector tool to identify the desired class and then edit and style it in global.css
.