How to use React and Notion for blog setup?

February 10, 2021

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.

 
notion image
 

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 or pages/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 with false 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.

notion image
 

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 and getServerSideProps
  • 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.

Hey, did you like the post?

2021