How to setup Notion as a CMS with NextJS 14
Notion
NextJs
Blogging
Project
Tech
March 13, 2024
Hello readers,
I'm a coder who got to know about Notion in 2020. Since then, it has become my go-to note-taking application. I've experimented with various page layouts, automated daily habits, and more. Essentially, you could say Notion is like my second brain, capturing everything from book summaries to my personal experiences and learning.
What I particularly appreciate about Notion is its simplicity and ease of use. In fact, I've become so accustomed to it that I want to manage everything through Notion.
While working on my blog page, I wondered if it would be possible to create blogs using Notion as a Content Management System (CMS), combined with a custom framework.
So, what exactly is Notion?
Notion is an all-in-one workspace where you can take notes, create documents, manage wikis, and oversee projects. Naturally, a blog is a fitting extension. Publishing a blog post can be as simple as adding a new row in a Notion database. This guide will show you how to do it using Next.js.
Tech Stack Used:
- NextJS 14
- TailwindCSS
- Notion Public API
Prerequisites
To complete this tutorial, you will need
- Node.js, npm, and git installed
- a text editor such as Visual Studio Code or Atom
- a web browser such as Firefox or Chrome
- familiarity with Next.js. You can look at the official Next.js getting started tutorial to learn more.
Setting Up Notion
Before using Notion as your blog's CMS, you need to create a new integration and share it with the Notion database containing the blog posts.
Creating a New Notion Integration
A Notion integration enables you to connect your Notion account via the Notion API. After creating the integration, you'll receive a Notion token, which will provide access to your blog data.
Start by signing up for a free Notion account if you don't already have one.
Next, follow the instructions in the getting started guide to create a new Notion integration.
Finally, copy the Notion token to the .env file.
Creating a Notion Database
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}
Populate the Notion Database
After creating the database, you need to populate it. First things first, create the blog schema by adding the following fields:
- Name - the title of the blog post
- Slug - the URL of the post
- Description - a list of users that wrote the post
- Tags - the URL of the meta image for a post
- Published - indicates whether the post is published or not; only the published posts will be displayed on the blog
- Date- the date the post was published
- CoverImg - the cover image of the blogpost
Why NextJs?
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.
- Next.js provides a built-in server-side rendering feature, which optimizes your site for search engine optimization.
- It has an automatic static optimization feature which allows pages without data fetching methods to be pre-rendered into static HTML.
- Next.js supports Incremental Static Regeneration, allowing developers to use static generation on a per-page basis and update them later as needed.
- It simplifies the process of deploying a web application. You do not need to worry about setting up a server, as Vercel, the company behind Next.js, provides a seamless deployment platform.
- It includes built-in routing, which makes it easy to create complex applications with shared components across different routes.
Setting Up Next.js
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 /app/page.tsx to look like this:
Fetch All Published 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 all the dependencies required to make notion blog.
npm install @notionhq/client notion-to-md react-markdown
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 NOTION_DB_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.
// getting all the blogs export const getAllPublished = async () => { const posts = await notion.databases.query({ database_id: process.env.NEXT_PUBLIC_NOTION_DB_ID, filter: { property: "Published", checkbox: { equals: true, }, }, sorts: [ { property: "Date", direction: "descending", }, ], }); const allPosts = posts.results; return allPosts.map((post: any) => { return getPageMetaData(post); }); };
The getPageMetaData
function extracts only the necessary data from the returned results.
const getPageMetaData = (post: any) => { const getTags = (tags: tagsType[]) => { const allTags = tags.map((tag) => { return tag.name; }); return allTags; }; return { id: post?.id, coverImg: post?.properties?.coverImg?.files[0]?.external?.url, 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: string) { 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.
Creating the Blog Overview Page
For better performance, we are using NextJs 14 which comes with the power of server side components.
Modify /app/blogs/page.tsx
,
import React from "react"; import { getAllPublished } from "@/app/lib/notion"; import BlogsCards from "./BlogsCards"; import { BlogCardData } from "@/shared/types"; export const dynamic = "force-dynamic"; const BlogsPage = async () => { const posts = await getAllPublished(); return ( <> <div className="grid grid-cols-1 md:grid-cols-2 gap-6 pb-8"> {posts.map((post: BlogCardData) => ( <div key={post.id} className="border rounded-lg hover:scale-105 transition-all duration-300" > <BlogsCards {...post} /> </div> ))} </div> </> ); }; export default BlogsPage;
As NextJs caches the pages in build time, but to make a page dynamic we will be adding this line for making that page dynamic. This enables this page to be dynamic which helps to constanly monitor the changes from notion to nextjs blogs page
export const dynamic = "force-dynamic";
Fetch a Single Blog Post From Notion
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.
To use notion-to-md, first give it the Notion client as an option.
// /lib/notion.ts const { NotionToMarkdown } = require("notion-to-md"); const n2m = new NotionToMarkdown({ notionClient: notion }); // getting post by the slug export const getSingleBlogPostBySlug = async (slug: string) => { const response = await notion.databases.query({ database_id: process.env.NEXT_PUBLIC_NOTION_DB_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.
Displaying a Single Post
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 /app/blogs/[slug].ts
and add the following code.
import { getSingleBlogPostBySlug } from "@/app/lib/notion"; const postPage = async ({ params }: paramsType) => { const post = await getSingleBlogPostBySlug(params.slug); return ( <article className="max-w-screen-md mx-auto py-8 px-4"> {/* Title of Blog */} <h1 className="text-3xl font-bold py-0 md:py-8 text-center"> {post.metadata.title} </h1> </article> ); };
This will show the title of the blogs, now we can render markdown we are receiving with ReactMarkdown. Using “markdown prose max-w-none” for parsing the markdown with styles.
import { getSingleBlogPostBySlug } from "@/app/lib/notion"; const postPage = async ({ params }: paramsType) => { const post = await getSingleBlogPostBySlug(params.slug); return ( <article className="max-w-screen-md mx-auto py-8 px-4"> {/* Title of Blog */} <h1 className="text-3xl font-bold py-0 md:py-8 text-center"> {post.metadata.title} </h1> <div className="markdown prose max-w-none"> {/* rendering code or markdown */} <ReactMarkdown> {post.markdown.parent} </ReactMarkdown> </article> ); };
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.
Styling the Blog
For the styling, make changes in global.css
@tailwind base; @tailwind components; @tailwind utilities; @layer components { .markdown h1 { @apply text-3xl font-bold mb-4; } .markdown h2 { @apply text-2xl font-semibold mb-4; } .markdown h3 { @apply text-xl font-semibold mb-3; } .markdown h4 { @apply text-lg font-semibold mb-3; } .markdown h5 { @apply text-base font-semibold mb-2; } .markdown h6 { @apply text-sm font-semibold mb-2; } .markdown p { @apply text-base text-gray-700 mb-4; } .markdown a { @apply text-blue-500 hover:text-blue-600 underline; } .markdown strong { @apply font-bold; } .markdown em { @apply italic; } .markdown ol { @apply list-decimal list-inside mb-4; } .markdown ul { @apply list-disc list-inside mb-4; } .markdown blockquote { @apply border-l-4 border-gray-200 pl-4 italic text-gray-600 mb-4; } /* .markdown code { @apply bg-gray-100 text-red-500 font-mono text-sm p-1 rounded; } */ .markdown pre { @apply bg-gray-900 text-white font-mono text-sm p-3 rounded overflow-x-auto; } .markdown img { @apply mx-auto my-4; } .markdown table { @apply min-w-full divide-y divide-gray-200 table-auto mb-4; } .markdown thead { @apply bg-gray-100; } .markdown tbody tr { @apply even:bg-gray-50 odd:bg-white; } .markdown th, .markdown td { @apply text-sm p-2 text-left; } }
Highlight 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 /app/blogs/[slug].ts
.
import { getSingleBlogPostBySlug } from "@/app/lib/notion"; import ReactMarkdown from "react-markdown"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { vscDarkPlus } from "react-syntax-highlighter/dist/cjs/styles/prism"; import Image from "next/image"; interface CodeBlockProps { language: string; codestring: string; } const CodeBlock = ({ language, codestring }: CodeBlockProps) => { return ( <SyntaxHighlighter language={language} style={vscDarkPlus} PreTag="div"> {codestring} </SyntaxHighlighter> ); }; interface paramsType { params: { slug: string; }; } const postPage = async ({ params }: paramsType) => { const post = await getSingleBlogPostBySlug(params.slug); return ( <article className="max-w-screen-md mx-auto py-8 px-4"> {/* cover Img */} <Image src={post.metadata.coverImg} className="w-full my-12 md:my-4 md:h-full object-cover rounded-2xl h-44" width={1000} height={1000} alt="blogs-cover" /> {/* Title of Blog */} <h1 className="text-3xl font-bold py-0 md:py-8 text-center"> {post.metadata.title} </h1> {/* Tags and Date of publish */} <div className="flex justify-between items-center py-4 flex-col-reverse md:flex-row"> <div className="flex gap-2 flex-wrap justify-center"> {post.metadata.tags.map((tag: string, index: number) => ( <p key={index} className="bg-gray-100 p-1 px-2 w-fit rounded-md break-words" > {tag} </p> ))} </div> <p>{post.metadata.date}</p> </div> <div className="markdown prose max-w-none"> {/* rendering code or markdown */} <ReactMarkdown components={{ code({ node, className, children, ...props }) { const match = /language-(\w+)/.exec(className || ""); return match ? ( <CodeBlock codestring={String(children).replace(/\n$/, "")} language={match[1]} /> ) : ( <code className={className} {...props}> {children} </code> ); }, }} > {post.markdown.parent} </ReactMarkdown> </div> </article> ); }; export default postPage;
Rendering Images in blog?
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.
So in this tutorial we will be uploading images files to imgur and get the image link.
Then adding the link in this way in notion.
And remember when you are uploading image to coverImg, use external link like imgur.
Deploy the Blog to Vercel
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.
Conclusion
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.
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 in realtime.