DEV Community

Cover image for Building a blog with Notions public API
Samuel Kraft
Samuel Kraft

Posted on • Originally published at samuelkraft.com

Building a blog with Notions public API

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 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. 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. 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

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

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

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).

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;
};
Enter fullscreen mode Exit fullscreen mode

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 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 Incremental Static Generation.

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
  };
};
Enter fullscreen mode Exit fullscreen mode

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 slugify.

Uhm, what is that <Text /> component you might ask? Notion has a lot of rich text tools allowing you to format text like this, so instead of just returning a plain text value each text-like field returns an array of Rich text objects like this:

{
  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"
}
Enter fullscreen mode Exit fullscreen mode

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

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>
    );
  });
};
Enter fullscreen mode Exit fullscreen mode
.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;
}
Enter fullscreen mode Exit fullscreen mode

Showing a specific post

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

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,
  };
};
Enter fullscreen mode Exit fullscreen mode

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. 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.

switch (type) {
  case "paragraph":
    return (
      <p>
        <Text text={value.text} />
      </p>
    );
  case "heading_1":
    return (
      <h1>
        <Text text={value.text} />
      </h1>
    );
  case "heading_2":
        
Enter fullscreen mode Exit fullscreen mode

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.

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

Notion blog

Top comments (2)

Collapse
 
latz profile image
Latz

Great idea, I have to try this!

Collapse
 
samuelkraft profile image
Samuel Kraft

Let me know if you build something cool 👍