Building a blog with Notions public API
Tags: notion, nextjs, blog, javascript
Published: 2021-05-19 18:29:26

**Ever wanted to use Notion as a blog CMS? This guide will walk you through how to setup Notion for blogging and how to access the data using Notions public API.**

Notions public API is finally here! I put together [an example repo](https://github.com/samuelkraft/notion-blog-nextjs) on creating a blog using **Notion as a CMS** hours after the release and was blown away by the interest so here's a guide explaining how to set it up yourself using the new api & Next.js.

## Getting started

If you're new to the notion API the first thing you need to do in order to start using it is to retrieve a `NOTION_TOKEN` by following their [Getting Started guide](https://developers.notion.com/docs/getting-started). When following the guide create either a table or a list as the database for your content. After finishing you should have a `NOTION_TOKEN` as well as a database id.

Create a Next.js project by following the [Next.js app setup](https://nextjs.org/learn/basics/create-nextjs-app/setup). The neat thing about using Next.js is that you can statically render all blog posts, while still updating them with *incremental static generation* making your blog super snappy ⚡️.


__Note:__ This guide assumes you are using Next.js but you can use plain javascript or whatever framework you wish, the Notion code is the same.

Save your `NOTION_TOKEN` and `NOTION_TABLE_ID` to a `.env.local` file in your projects root directory, then install their javascript client by running `yarn add @notionhq/client` .

## Fetching data using Notions javascript client

Let's create a new file called `lib/notion.js` which will contain all our API usage. To fetch our data we first need to create a client and supply it with the token we received earlier

```jsx
import { Client } from "@notionhq/client";

const notion = new Client({
  auth: process.env.NOTION_TOKEN,
});
```

Then we need to add the functions we need. To get a list of our posts we need to be able to query a specific database (`getDatabase`), to get the information for a specific post we need to get a specific page (`getPage`), and to actually get the content of the page we need get its blocks (`getBlocks`).   

```jsx
import { Client } from "@notionhq/client";

const notion = new Client({
  auth: process.env.NOTION_TOKEN,
});

export const getDatabase = async (databaseId) => {
  const response = await notion.databases.query({
    database_id: databaseId,
  });
  return response.results;
};

export const getPage = async (pageId) => {
  const response = await notion.pages.retrieve({ page_id: pageId });
  return response;
};

export const getBlocks = async (blockId) => {
  const response = await notion.blocks.children.list({
    block_id: blockId,
    page_size: 50,
  });
  return response.results;
};
```

## Showing a list of posts

Add a function called `getInitialProps` to `pages/index.js` where you use the getDatabase function to fetch all posts. The response will contain a [Database Object](https://developers.notion.com/reference/database) containing information like the database title, when it was last edited as well as its properties (in our case blogposts).

__Note:__ If you're new to Next.js getStaticProps allows you to pre-render the page at build time using the props returned by the function. Combining this with the revalidate prop will allow the page to get updated in the background as new traffic comes in without the need to rebuild your entire site. Learn more about <a href="https://nextjs.org/docs/basic-features/data-fetching#incremental-static-regeneration">Incremental Static Generation</a>.

```jsx
import Link from "next/link";
import { getDatabase } from "../lib/notion";
import { Text } from "./[id].js";

export const databaseId = process.env.NOTION_DATABASE_ID

export default function Home({ posts }) {
  return (  
    <ol className={styles.posts}>
      {posts.map((post) => (
        <li key={post.id}>
          <Link href={`/${post.id}`}>
             <Text text={post.properties.Name.title} />
          </Link>
        </li>
      )}
    </ol> 
  );
}

export const getStaticProps = async () => {
  const database = await getDatabase(databaseId);

  return {
    props: {
      posts: database,
    },
		// Next.js will attempt to re-generate the page:
    // - When a request comes in
    // - At most once every second
    revalidate: 1, // In seconds
  };
};
```

__Note:__ We're using the post id as slug for sake of simplicity in this tutorial. For SEO purposes it would be better to use a prettier slug based on the title. You can do this by either manually adding a slug property to each post in Notion or generating a slug with a package like <a href="https://www.npmjs.com/package/slugify">slugify</a>.

Uhm, what is that `<Text />` component you might ask? Notion has a lot of rich text tools allowing you to **format** <ins>text</ins> ~~like~~ *this,* so instead of just returning a plain text value each text-like field returns an array of [Rich text objects](https://developers.notion.com/reference/rich-text) like this:

```jsx
{
  annotations: {
    bold: false,
    italic: false,
    strikethrough: false,
    underline: false,
    code: false,
    color: 'default'
  },
  href: null,
  plain_text: "Hello world!",
  text: {
    content: "Hello world!",
    link: null
  },
  type: "text"
}
```

In order to handle links and annotations let's create a component that we can re-use for all types of text:

```jsx
import styles from './text.module.css'

export const Text = ({ text }) => {
  if (!text) {
    return null;
  }
  return text.map((value) => {
    const {
      annotations: { bold, code, color, italic, strikethrough, underline },
      text,
    } = value;
    return (
      <span
        className={[
          bold ? styles.bold : "",
          code ? styles.code : "",
          italic ? styles.italic : "",
          strikethrough ? styles.strikethrough : "",
          underline ? styles.underline : "",
        ].join(" ")}
        style={color !== "default" ? { color } : {}}
      >
        {text.link ? <a href={text.link.url}>{text.content}</a> : text.content}
      </span>
    );
  });
};
```

```css
.bold {
  font-weight: bold;
}

.code {
  font-family: monospace;
  background-color: rgb(242, 242, 242);
  padding: 2px 4px;
  border-radius: 2px;
}

.italic {
  font-style: italic;
}

.strikethrough {
  text-decoration: line-through;
}

.underline {
  text-decoration: underline;
}
```

## Showing a specific post

Let's create a `pages/[id].js` page that will show the content of each blog post

```jsx
import { Fragment } from "react";
import { getDatabase, getPage, getBlocks } from "../lib/notion";
import { databaseId } from "./index.js";
import Text from 'components/text'

const renderBlock = (block) => {
  const { type, id } = block;
  const value = block[type];

  switch (type) {
    case "paragraph":
      return (
        <p>
          <Text text={value.text} />
        </p>
      );
    case "heading_1":
      return (
        <h1>
          <Text text={value.text} />
        </h1>
      );
    case "heading_2":
      return (
        <h2>
          <Text text={value.text} />
        </h2>
      );
    case "heading_3":
      return (
        <h3>
          <Text text={value.text} />
        </h3>
      );
    case "bulleted_list_item":
    case "numbered_list_item":
      return (
        <li>
          <Text text={value.text} />
        </li>
      );
    case "to_do":
      return (
        <div>
          <label htmlFor={id}>
            <input type="checkbox" id={id} defaultChecked={value.checked} />{" "}
            <Text text={value.text} />
          </label>
        </div>
      );
    case "toggle":
      return (
        <details>
          <summary>
            <Text text={value.text} />
          </summary>
          It's a toggle!
        </details>
      );
    case "child_page":
      return <p>{value.title}</p>;
    default:
      return `❌ Unsupported block (${
        type === "unsupported" ? "unsupported by Notion API" : type
      })`;
  }
};

export default function Post({ page, blocks }) {
  if (!page || !blocks) {
    return <div />;
  }
  return (   
    <article>
      <h1>
        <Text text={page.properties.Name.title} />
      </h1>
      <section>
        {blocks.map((block) => (
          <Fragment key={block.id}>{renderBlock(block)}</Fragment>
        ))}
      </section>
    </article>
  );
}

export const getStaticPaths = async () => {
  const database = await getDatabase(databaseId);
  return {
    paths: database.map((page) => ({ params: { id: page.id } })),
    fallback: true,
  };
};

export const getStaticProps = async (context) => {
  const { id } = context.params;
  const page = await getPage(id);
  const blocks = await getBlocks(id);

  return {
    props: {
      page,
      blocks,
    },
    revalidate: 1,
  };
};
```

That's a lot, so let's break it down starting from the bottom.

Just as on the index page we are using getStaticProps to get the data so the page can be statically rendered. In order to know which post to render we'll use the `context` parameter which contains route parameters for the current page. With the post id we can then use our two notion functions `getPage()` and `getBlocks()` and return the results as props to our component.

When using dynamic routes we also need to specify a list of availables routes and we'll do that with `getStaticPaths` by mapping over the pages and returning the page id's.

## Rendering Notion blocks

The `blocks` prop contains a list of all the blocks as [Block Objects](https://developers.notion.com/reference/block). We have created a `renderBlock()` function that will take care of displaying the different blocks. Simplifying this a bit, basically all blocks currently supported by Notions API is a "text-like" block. This means that we pretty much only have to use our Text component inside the respective tag needed.

```jsx
switch (type) {
  case "paragraph":
    return (
      <p>
        <Text text={value.text} />
      </p>
    );
  case "heading_1":
    return (
      <h1>
        <Text text={value.text} />
      </h1>
    );
  case "heading_2":
		…
```

Open up the index page in your browser and click a post and you should see a working page displaying your content!

## Notion API unsupported blocks

During the Notion API beta unfortunately not all block types are supported. The supported block types are:

- `paragraph`
- `heading_1`
- `heading_2`
- `heading_3`
- `bulleted_list_item`
- `numbered_list_item`
- `to_do`
- `toggle`
- `child_page`

If you use a block that is not supported the page will display `❌ Unsupported block`. At this time there's no way of getting around this other than using their private API which is not recommended and can change at any time. If you need more blocks than those available right now I would wait a little bit longer until all block types are available.

## Styling

This guide intentionally comes without any styles as you would probably just rip it out anyways. If you want inspiration I put together some very bare-bones styles [in my example repo at Github](https://github.com/samuelkraft/notion-blog-nextjs).

## That's it!

You now have a blog powered by Notion! If you add a new blogpost or edit a post then the page will automatically get updated on the second hit thanks to Next.js incremental static regeneration. Check out my [live example here](https://notion-blog-nextjs-coral.vercel.app)

![Notion blog](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ghl8u3fxhlqkk4u5teob.png)