How to use React and Notion for blog setup?
The last time I did the deployment of API and was able to download data from Notion using this API. Today I'm going to use this data in the front-end application. I will use Next.js and react-notion to display posts.
Next.js
In accordance with the MVP ideology, I try to use solutions that already exist to improve the process of product delivery. Next.js has a very extensive community and many initial application templates are available for immediate use. You can see it here. I have chosen with-typescript-eslint-jest which gives me Typescript, eslint and prettier out of the box. So on my computer, I need to run this command to download the template.
yarn create next-app --example with-typescript-eslint-jest with-typescript-eslint-jest-app
At the very beginning my application structure looks like this.
├── README.md
├── jest.config.js
├── next-env.d.ts
├── package.json
├── pages
│ ├── api
│ │ └── hello.ts
│ └── index.tsx
├── public
│ ├── favicon.ico
│ └── vercel.svg
├── tsconfig.json
└── yarn.lock
First thing first, I need to install react-notion package.
yarn add react-notion
Then, let's create _app.tsx
file in pages
directory. This is a Next.js way to override the default App
component. I need to do that to inject global CSS styles that are coming from 3rd party libraries.
import { AppProps } from "next/app";
// these styles are necessary to display notion data properly
import "react-notion/src/styles.css";
import "prismjs/themes/prism-tomorrow.css";
export default function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
I can move to index.tsx
. Let's start with a super simple HomePage
component that displays the list of all posts. I am going to use a mocked list of posts and then I will modify this component to fetch the posts asynchronously.
// index.tsx
const posts = [
{
id: "5b381ba5-baab-4c86-97fa-0a414e5d78fd",
slug: "first-post",
title: "First Post",
},
{
id: "3bd33382-0e08-4234-aaca-6c03816afc06",
slug: "second-post",
title: "Second Post",
},
];
const HomePage = () => {
return (
<div>
<h1>Welcome on my blog</h1>
<h2>These are my recent posts:</h2>
<ul>
{posts.map((post) => (
<li>{post.title}</li>
))}
</ul>
</div>
);
};
export default HomePage;
In Next.js there is a possibility to fetch data in different ways. The most popular ones are these three:
getStaticProps
- helpful when you want your site to be statically generated at build time (SSG)
getStaticPaths
- helpful when you want dynamically specify the routes on statically generated sites (SSG)
getServerSideProps
- helpful when you want you data to be fetch on every request (SSR)
More information about data fetching you can check on offical documentation.
In this case, I won't change the content so often so I want my blog to be generated at build time. Also, the paths will be based on the dynamic content so I need to take care of that as well. Let's see how does it look like in the code.
To keep the code clean it is always good to set some constants for values like tokens, API URLs, etc. API_URL
in this case is the endpoint I got after publishing the wrangler worker to Cloudflare. NOTION_BLOG_ID
on the other hand is the pageId
I got from the Notion's URL.
// index.tsx
const API_URL = "https://NAME_OF_WORKER.YOUR_CLOUDFLARE_NAME.workers.dev";
const NOTION_BLOG_ID = "ID_OF_YOUR_NOTION_TABLE_PAGE";
Having the above configuration separated, I can write some logic, which will be responsible for fetching the posts from my API.
// index.tsx
export const getStaticProps: GetStaticProps = async () => {
const response = await fetch(`${API_URL}/v1/table/${NOTION_BLOG_ID}`);
const posts = await response.json();
return {
props: {
posts,
},
};
};
Now, for the posts
that are being returned from the getStaticProps
function, I need to pass them as props to the component to make them visible on the page. So I get rid of previously mocked posts and use the once from the API. I also want to prepare the links now, so I don't have to go back to this file again. Next.js provides a Link
component, which helps me navigate between posts. It accepts href
prop, where I need to pass a string with [slug]
in order to create dynamic routes. Also, it accepts as
a prop, where I need to pass a string that will be displayed in the browser. I think it will become less complicated in a while.
Summing up, the finalindex.tsx
file looks like this.
// index.tsx
import { GetStaticProps } from "next";
import Link from "next/link";
export type Post = { id: string; slug: string; title: string };
const API_URL = "https://NAME_OF_WORKER.YOUR_CLOUDFLARE_NAME.workers.dev";
const NOTION_BLOG_ID = "ID_OF_YOUR_NOTION_TABLE_PAGE";
export const getStaticProps: GetStaticProps = async () => {
const response = await fetch(`${API_URL}/v1/table/${NOTION_BLOG_ID}`);
const posts = await response.json();
return {
props: {
posts,
},
};
};
const HomePage = ({ posts }: { posts: Post[] }) => {
return (
<div>
<h1>Welcome on my blog</h1>
<h2>These are my recent posts:</h2>
<ul>
{posts.map((post) => (
<li>
<Link href={"/blog/[slug]"} as={`/blog/${post.slug}`}>
<a>{post.title}</a>
</Link>
</li>
))}
</ul>
</div>
);
};
export default HomePage;
And this is how the blog in the browser looks like.

So far I am able to display the list of posts, however, when I click on the link, I will be redirected to the 404 page. That is the expected result for now, because, I didn't implement the post page itself. And this is what I am going to do now.
In Next.js it is important how you structure your files and directories because it has a direct impact on how the application navigation in your browser looks like. The mapping looks like this:
pages/index.tsx
⇒ mydomain.com
pages/blog.tsx
orpages/blog/index.tsx
⇒ mydomain.com/blog
pages/blog/[slug].tsx
⇒ mydomain.com/blog/[dynamic-post-slug]
I have created the first one. I am rendering the list of posts on my home page. For now, I won't create the blog
path, so I will skip the second example. I will go with the third one, which has a little bit of weird syntax. Well, this is how Next.js handles the dynamic routes.
Let's create pages/blog/[slug].tsx
file. I will start with super easy component like below.
// pages/blog/[slug].tsx
const BlogPost: React.FC = () => {
return <div>this is post</div>;
};
export default BlogPost;
It works, however for each post clicked it renders the same content. So, the first thing I need to add here is the getStaticPaths
function, which is always required for dynamic, server-side generated pages. Inside, I will fetch the list of all posts available on Notion. In the end, the function has to return the object containing two properties:
paths
- Array - list of paths that will be prerendered on build time
fallback
- Boolean - I will stick withfalse
for now (more in docs)
// pages/blog/[slug].tsx
export const getStaticPaths: GetStaticPaths = async () => {
const response = await fetch(`${API_URL}/v1/table/${NOTION_BLOG_ID}`);
const table = await response.json();
const paths = table.map((row) => ({ params: { slug: row.slug } }));
return {
paths,
fallback: false,
};
};
In addition to getStaticPath
, getStaticProps
must also be added to this file. When getStaticPath
is responsible for handling and rendering correctly dynamic routes, then getStaticProps
is responsible for feeding each individual dynamic rote with relevant data.
The URLs in the applications are constructed in a human-friendly, readable way, using slugs. However, I need the id
of a particular post to get all the content that will be displayed. Therefore, inside the function, I need to fetch all posts and find the proper one by slug. Then I need to call API once again to fetch the post content itself, which then will be passed down to the component with props.
// pages/blog/[slug].tsx
export const getStaticProps: GetStaticProps = async ({ params: { slug } }) => {
const tableResponse = await fetch(`${API_URL}/v1/table/${NOTION_BLOG_ID}`);
const table = await tableResponse.json();
const post = table.find((post) => post.slug === slug);
const blocksResponse = await fetch(`${API_URL}/v1/page/${post.id}`);
const blocks = await blocksResponse.json();
return {
props: {
blocks,
post,
},
};
};
I have one last thing left to do. The data that comes from API is in .json
format, but its structure is quite complicated. So I'll use the react-notion library to display the content in a simple and effective way. Just import it and use it in the BlogPost component.
// pages/blog/[slug].tsx
import { NotionRenderer, BlockMapType } from "react-notion";
const BlogPost: React.FC<{ post: Post; blocks: BlockMapType }> = ({
post,
blocks,
}) => (
<>
<h1>{post.title}</h1>
<NotionRenderer blockMap={blocks} />
</>
);
And that's it! Look what we have acomplished here.

Summary
Today I have done many interesting things. I learn:
- how to setup the Next.js project with Typescript very quickly
- what is the difference between
getStaticProps
,getStaticPaths
andgetServerSideProps
- how to display Notion content and serve it as a CMS with Next.js application
Of course, I went with the easiest approach in many things like code structure, CSS styles, etc. But that is alright. The product will be evolving constantly so it is good to deliver small chunks of it every small period of time.