DEV Community

loading...

Creating a Markdown blog with Notion, Tailwind & Next.js

Thomas Ledoux
Frontend Developer @ The Reference Passionate about React, Next.js and performance
・4 min read

Last week Notion announced that they are opening up their API to the public, after being in closed beta for a while.
For me that was great news, since I'm a big Notion fan and I was looking for a way to easily write my blogs in Markdown in a central place.

So the backend was decided! For the frontend I went with my usual stack: Next.js and Tailwind.

I started out by creating an integration, and then sharing my database with this integration. This is explained in detail here.

Once this part is set up, we can start querying our database in Notion!

There are 3 different API routes I used to create my blog:

In my pages/index.jsx I query the database to get back the pages in my database.

// fetcher function
async function fetcher(url, method = "GET") {
  return fetch(url, {
    method,
    headers: {
      Authorization: `Bearer ${process.env.NOTION_API_KEY}`
    }
  });
}

// getBlogs function
export async function getBlogs() {
  const res = await fetcher(
    `https://api.notion.com/v1/databases/${process.env.NOTION_DATABASE}/query`,
    "POST"
  );
  const database = await res.json();
  return database.results;
}

// in pages/index.js
export async function getStaticProps() {
  const blogs = await getBlogs();
  return {
    props: {
      blogs
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

So now we have passed the blogs to the props of the home page.
In the functional component I render the blogs, wrapped in a Link for internal routing:

{blogs.map(blog => (
  <Link passHref key={blog.id} href={`/blog/${blog.id}`}>
    <a>
      <article className="shadow-md hover:shadow-xl p-4">
        <h2>{blog.properties.Name.title[0].plain_text}</h2>
        <p>{new Date(blog.last_edited_time).toLocaleDateString()}</p>
      </article>
    </a>
  </Link>
))}
Enter fullscreen mode Exit fullscreen mode

Now we have the blog previews being shown on the homepage, we can now work on the actual blog page.
As you can see in the href of the Link, we will use /blog/[id] as the URL.
So in the /pages folder we create a folder /blog and create a file [id].jsx in there.

On the blog page, we need to fetch the pages again to generate our URL's, fetch the actual page and fetch the blocks out of which the page consists.

export async function getStaticPaths() {
  const blogs = await getBlogs();
  return {
    paths: blogs.map(el => ({
      params: {
        id: el.id
      }
    })),
  };
}

export async function getStaticProps(context) {
  const { id } = context.params;
  const blocks = await getBlocks(id);
  const page = await getBlog(id);
  return {
    props: {
      blocks,
      page
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

Now we have the blocks and page available in our component, we can render them to our page!
I'm going to focus on the blocks, because the page is just used for the title.
All the content comes from the blocks:

// components/block.jsx
import Text from "./text";

const Block = ({ block }) => {
  const { type } = block;
  const value = block[type];
  if (type === "paragraph") {
    return (
      <p className="mb-4">
        <Text text={value.text} />
      </p>
    );
  }
  if (type === "heading_1") {
    return (
      <h1 className="text-2xl font-bold md:text-4xl mb-4">
        <Text text={value.text} />
      </h1>
    );
  }
  if (type === "heading_2") {
    return (
      <h2 className="text-xl font-bold md:text-2xl mb-4">
        <Text text={value.text} />
      </h2>
    );
  }
  if (type === "heading_3") {
    return (
      <h3 className="text-lg font-bold md:text-xl mb-4">
        <Text text={value.text} />
      </h3>
    );
  }
  if (type === "bulleted_list_item" || type === "numbered_list_item") {
    return (
      <li className="mb-4">
        <Text text={value.text} />
      </li>
    );
  }
  return (
    <p className="bg-red-600 px-4 py-2 mb-4">Not supported yet by Notion API</p>
  );
};

export default Block;

// components/text.jsx
import classNames from "classnames";
import { v4 as uuid } from "uuid";
const Text = ({ text }) => {
  const colorMapper = {
    default: "text-current",
    yellow: "text-yellow-500",
    gray: "text-gray-500",
    brown: "text-brown-500",
    orange: "text-orange-500",
    green: "text-green-500",
    blue: "text-blue-500",
    purple: "text-purple-500",
    red: "text-red-500"
  };
  if (!text) {
    return null;
  }
  return text.map(value => {
    const {
      annotations: { bold, code, color, italic, strikethrough, underline },
      text
    } = value;
    const id = uuid();
    return (
      <span
        className={classNames(colorMapper[color], "break-words", {
          "font-bold": bold,
          italic: italic,
          "line-through": strikethrough,
          underline: underline,
          "bg-gray-300 px-2 py-1": code
        })}
        key={id}
      >
        {text.link ? (
          <a className="underline" href={text.link.url}>
            {text.content}
          </a>
        ) : (
          text.content
        )}
      </span>
    );
  });
};

export default Text;


// pages/blog/[id]
{blocks.map(block => (
  <Block key={block.id} block={block} />
))}
Enter fullscreen mode Exit fullscreen mode

Using the classes provided by Tailwind, we can easily map the Markdown to a fully styled page.

You can check the demo at https://notion-blog-ruby-kappa.vercel.app.
Source code can be found on https://github.com/thomasledoux1/notion-blog.
Some of the code was inspired by https://github.com/samuelkraft/notion-blog-nextjs, so shoutout to Samuel too.

Thanks for reading, I hope you learned something new today!

Discussion (3)

Collapse
lifund profile image
lifund

Hi Thomas,
I was looking for Notion's docs today for setting up my blog,
but some critical features were missing like

  • images
  • codeblocks
  • iframes and many else.

Do you happen to have some information on the future plan of Notion's api?
Will they ever support them?

Collapse
ezazhel profile image
Sjeanne

With react-notion-x you now have all this features !
github.com/NotionX/react-notion-x

Collapse
thomasledoux1 profile image
Thomas Ledoux Author

Hi,

I wish I could give you more information, but I don’t seem to find any more info about it either..
I guess as long as the API is in beta this will be the case…