Knowledge Hub
For manager
Headless CMSFor developer
Web FrameworksStatic Site GeneratorsServerless DatabasesMonoreposHostingPIMLearn
GuidesTutorialsAdopt
Case studiesNotion is an all-in-one workspace for taking notes, creating docs, wikis, and managing projects. A blog is a natural extension. Publishing a blog post will be as simple as creating a new row in a Notion database. Here is the guide on how to do it using Next.js.
To complete this tutorial, you will need
Before using Notion as a CMS for your blog, you must create a new integration and share it with the Notion database with the blog posts.
A Notion integration allows you to connect to a Notion account via the Notion API. Once you create it, you should get a Notion token that’ll give you access to the blog data.
Begin by signing up for a free Notion account if you don’t have one yet.
Then, follow the instructions in the getting started guide to create a new Notion integration.
Lastly, copy the Notion token to the .env file.
Follow these steps to create a Notion database that will hold the blog posts. You must also share the integration with the database to ensure you can access it using the token.
Additionally, you need the ID of the Notion database. It acts as an identifier for the database you want to connect to.
Retrieve it from the database URL by copying the part corresponding to the database_id in the example below.
https://www.notion.so/{workspace_name}/{database_id}?v={view_id}
After creating the database, you need to populate it. First things first, create the blog schema by adding the following fields:
Each row in the database is a blog post page containing the slug, description, tags, date, and published status.
You can add extra fields, like an author field, and customize the blog schema according to your preference.
Next.js is a React framework for production. It comes with built-in features and optimizations for creating fast applications without extra configuration. It is perfect for a static blog as you can pre-render pages and configure the app to routinely update the site through the incremental static regeneration (ISR) feature.
Start setting up a new Next.js project by running the create-next-app command on the terminal.
npx create-next-app@latest
The command will prompt you for a project name. Type your preferred name and hit enter.
After the installation completes, open the folder using a text editor and clean up /pages/index.js to look like this:
import Head from 'next/head'
import styles from '../styles/Home.module.css'
export default function Home() {
return (
<div className={styles.container}>
<Head>
<title>My Blog</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
</main>
</div>
)
}
You can now start creating the blog overview page but first fetch the blog posts from Notion.
You’ll create a function to fetch all the published posts metadata using the notionhq/client package, a JavaScript client that simplifies making requests to the Notion API.
So, run the following command in the terminal to install it.
npm install @notionhq/client
All the functions that query Notion will go in the /lib/notion.js file. Create it and add the following code to initialize the Notion client.
const { Client } = require("@notionhq/client")
const notion = new Client({
auth: process.env.NOTION_TOKEN,
})
Remember to add the NOTION_TOKEN
and the DATABASE_ID
to the .env file.
Next, create a new function called getAllPublished. It uses the notion client to query the database for published posts only using the filter option. It also sorts the posts in descending order based on the date they were created.
export const getAllPublished = async () => {
const posts = await notion.databases.query({
database_id: process.env.DATABASE_ID,
filter: {
property: "Published",
checkbox: {
equals: true,
},
},
sorts: [
{
property: "Date",
direction: "descending",
},
],
});
const allPosts = posts.results;
return allPosts.map((post) => {
return getPageMetaData(post);
});
};
The getPageMetaData function extracts only the necessary data from the returned results.
const getPageMetaData = (post) => {
const getTags = (tags) => {
const allTags = tags.map((tag) => {
return tag.name;
});
return allTags;
};
return {
id: post.id,
title: post.properties.Name.title[0].plain_text,
tags: getTags(post.properties.Tags.multi_select),
description: post.properties.Description.rich_text[0].plain_text,
date: getToday(post.properties.Date.last_edited_time),
slug: post.properties.Slug.rich_text[0].plain_text,
};
};
You are also using the getToday function to format the date from Notion to a more human-readable format.
function getToday (datestring) {
const months = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
let date = new Date();
if (datestring) {
date = new Date(datestring);
}
const day = date.getDate();
const month = months[date.getMonth()];
const year = date.getFullYear();
let today = `${month} ${day}, ${year}`;
return today;
};
Now, you are ready to create the blog overview page.
For better performance, you will fetch the blog posts at build time using getStaticProps and pre-render the page.
Modify /pages/index.js
, to use getStaticProps where you will fetch the posts by calling the getAllPublishedPosts function.
import Head from 'next/head'
import Link from 'next/link';
import { getAllPublished } from '../lib/notion';
import styles from '../styles/Home.module.css'
export default function Home({posts}) {
if(!posts) return <h1>No posts</h1>
return (
<div className={styles.container}>
<Head>
<title>My Blog</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<h1>Blog</h1>
{posts.map((post, index) => (
<section key={index}>
<div>
<h2>
<Link href={post.slug}>
<a>{post.title}</a>
</Link>
</h2>
<div>
<div>{post.date}</div>
<p>{post.description}</p>
</section>
))}
</main>
</div>
)
}
export const getStaticProps = async () => {
const data = await getAllPublished()
return {
props: {
posts: data,
},
revalidate: 60
};
};
The getStaticProps function returns the posts data and makes it available to the Home component through props.
Once the Home component receives the props, it uses the map function to iterate over the array of posts and renders them on the page.
Using revalidate with a value of 60 in getStaticProps tells Next.js to attempt to regenerate the page every minute, which in turn ensures the blog stays up to date.
Before creating the individual blog pages, you must fetch the content of each post from Notion.
For this, you will use the notionhq/client package you installed earlier and the notion-to-md package to convert Notion blocks to markdown.
Install notion-to-md by running this command.
npm install notion-to-md
To use notion-to-md, first give it the Notion client as an option.
const { NotionToMarkdown } = require("notion-to-md");
const n2m = new NotionToMarkdown({ notionClient: notion });
// Then, create a new function called getSinglePost in /lib/notion.js.
export const getSinglePost = async (slug) => {
const response = await notion.databases.query({
database_id: process.env.DATABASE_ID,
filter: {
property: "Slug",
formula: {
string: {
equals: slug,
},
},
},
});
const page = response.results[0];
const metadata = getPageMetaData(page);
const mdblocks = await n2m.pageToMarkdown(page.id);
const mdString = n2m.toMarkdownString(mdblocks);
return {
metadata,
markdown: mdString,
};
}
For a given slug, this function retrieves a post from Notion and converts the page to markdown. It returns the markdown and page metadata in an object.
It's not feasible to manually create a page for each post. It's easier to create a dynamic route that renders the pages based on their corresponding id, slug, or other parameters. In Next.js, you create the dynamic route by adding a bracket around the page name.
Create a new dynamic page called /pages/posts/[slug].js
and add the following code.
const Post = () => {
return (
<h1>Post</h1>
)
}
/*
Similarly to the blog overview page, you will be pre-rendering each post page.
In /pages/posts/[slug].js, add the getStaticProps() function after the Post component and call the getSingleBlogPostBySlug function to fetch the blog post from Notion.
*/
export const getStaticProps = async ({ params }) => {
const post = await getSingleBlogPostBySlug(params.slug)
return {
props: {
post,
},
revalidate: 60
};
};
getStaticProps uses the slug parameter from the path URL to identify the blog post that should be rendered. Remember you named the page [slug].js, and that’s why you are using params.slug.
Note the revalidate option is set to 60 in the return statement. Like in the blog overview page, this tells Next.js to try fetching the data and updating the page after a minute.
getStaticProps is not enough to pre-render the list of posts. You need to define the paths using getStaticPaths, so add the following to the /pages/posts/[slug].js
.
export const getStaticPaths = async () => {
const posts = await getAllPublished();
const paths = posts.map(({ slug }) => ({ params: { slug } }));
return {
paths,
fallback: "blocking",
};
};
Here, the paths are generated from the slug of each post returned from Notion.
After fetching the post, the next step is to display it. Having converted the Notion blocks to markdown using notion-to-md, you now need to convert the markdown to HTM to render it on the browser. For this, you can use the React Markdown package. Install it by running this command.
npm install react-markdown
Next, import react-markdown and modify the Post component in /pages/posts/[slug].js
like this:
import ReactMarkdown from 'react-markdown'
import { getAllPublished, getSingleBlogPostBySlug } from "../../lib/notion"
const Post = ({ post }) => {
return (
<section>
<h2>{post.metadata.title}</h2>
<span>{post.metadata.date}</span>
<p>{post.metadata.tags.join(', ')}</p>
<ReactMarkdown>{post.markdown}</ReactMarkdown>
</section>
);
};
That’s it! You have successfully created a static blog using Notion as a CMS.
To make it more visually appealing, add syntax highlighting to code blocks.
There are many tools you can use to highlight code blocks. One of them is the react-syntax-highlighter component. It is easy to use and comes with out-of-the-box styling.
Run this command in the terminal to install the react-syntax-highlighter component.
npm install react-syntax-highlighter
Then, modify the Post component in /pages/posts/[slug].js
.
import ReactMarkdown from 'react-markdown'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism'
import { getAllPublished, getSingleBlogPostBySlug } from "../../lib/notion";
const CodeBlock = ({ language, codestring }) => {
return (
<SyntaxHighlighter language={language} style={vscDarkPlus} PreTag="div">
{codestring}
</SyntaxHighlighter>
)
}
const Post = ({ post }) => {
return (
<section>
<h2>{post.metadata.title}</h2>
<span>{post.metadata.date}</span>
<p style={{color: "gray"}}>{post.metadata.tags.join(', ')}</p>
<ReactMarkdown
components={{
code({node, inline, className, children, ...props}) {
const match = /language-(\w+)/.exec(className || '')
return !inline && match ? (
<CodeBlock
codestring={String(children).replace(/\n$/, '')}
language={match[1]}
/>
) : (
<code className={className} {...props}>
{children}
</code>
)
}
}}>
{post.markdown}
</ReactMarkdown>
</section>
);
};
Note you are using the vscDarkPlus theme to highlight the code. You can, however, choose another theme if you like.
Copy the following styles in Home.module.css to style the blog.
.container {
padding: 0 2rem;
}
.main {
padding: 4rem;
}
.card {
margin: 1rem 0;
padding: 1.5rem;
border: 1px solid #eaeaea;
border-radius: 10px;
position: relative;
}
.card:hover {
border-color: blue;
}
.card a {
text-decoration: none;
cursor: pointer;
border: 2px solid transparent;
transition: border-color 200ms;
}
.card a::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
These styles are just enough to give the blog structure, so feel free to change them as you see fit.
One of the easiest ways to deploy the blog is to use Vercel. Start by pushing your code to GitHub, then follow the Vercel for Git instructions from this guide, and your site will be available online. Remember to add the Notion token and database ID as environmental variables while deploying your site.
Apart from Vercel, you can also deploy the blog for free to the Netlify platform from Git. Push your code to GitHub or a supported version-control tool of your choice, then follow these instructions to deploy to Netlify. Like deploying to Vercel, you should include the Notion token and database ID as environment variables.
A Notion-powered blog is easy to maintain. You only need to edit or create rows in the database to update the blog’s content. If you are already a Notion user, publishing content fits easily into your daily workflow.
Notion is free for individual use. And if you end up hosting your blog on a platform like Vercel or Netlify, the site will be 100% cost-free.
However, there are some challenges to rendering pages from Notion in Next.js.
Notion returns short-lived image URLs that expire after an hour. For statically rendered pages, this means users experience a bunch of 404 errors. A workaround would be to host your images yourself instead of relying on Notion.
Note that if you want to render markdown tables from the Notion page in Next.js using react-markdown, you must use the remark-gfm plugin. Otherwise, any table in the blog post will not render correctly. It is because react-markdown follows the CommonMark standard that doesn’t have tables.
In this tutorial, you created a Next.js blog powered by Notion. To update or create new posts, edit the Notion database, and the blog will update every minute. For the entire code, go to the GitHub repo or see the deployed blog. You can also duplicate this sample Notion database as a starting point.