Back to overview

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

PC with person drawing on paper behind it
| 4 min read
View count:

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, {
    headers: {
      Authorization: `Bearer ${process.env.NOTION_API_KEY}`,

// getBlogs function
export async function getBlogs() {
  const res = await fetcher(
  const database = await res.json();
  return database.results;

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

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:

{ => (
    <Link passHref key={} href={`/blog/${}`}>
        <article className="shadow-md hover:shadow-xl p-4">
          <p>{new Date(blog.last_edited_time).toLocaleDateString()}</p>

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: => ({
      params: {
export async function getStaticProps(context) {
  const { id } = context.params;
  const blocks = await getBlocks(id);
  const page = await getBlog(id);
  return {
    props: {

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:

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

export default Block;
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 => {
    const {
      annotations: { bold, code, color, italic, strikethrough, underline },
    } = value;
    const id = uuid();
    return (
        className={classNames(colorMapper[color], "break-words", {
          "font-bold": bold,
          italic: italic,
          "line-through": strikethrough,
          underline: underline,
          "bg-gray-300 px-2 py-1": code,
        { ? (
          <a className="underline" href={}>
        ) : (

export default Text;

// pages/blog/[id]
{ => <Block key={} block={block} />);

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

You can check the demo at Source code can be found on Some of the code was inspired by, so shoutout to Samuel too.

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

Did you like this post? Check out my latest blog posts:

Astral picture
29 Mar 2024

Astro DB: Migrating my analytics data from Vercel Postgres

5 min reading time

  • astro
  • database
  • vercel
  • postgres
  • libsql
Screenshot of an analytics dashboard
16 Jan 2024

Basic analytics with Vercel Postgres - Drizzle - Astro

10 min reading time

  • vercel
  • databases
  • typescript
  • astro
Page of a dictionary
15 Nov 2023

A minimal dependency-free translation system for Next.js

6 min reading time

  • nextjs
  • react
  • javascript