DEV Community

loading...
Cover image for How I built Notion Playgrounds using Notion API and some Nextjs magic ✨

How I built Notion Playgrounds using Notion API and some Nextjs magic ✨

jj_ranalli
Full stack maker. Creator of Nightwind (nightwindcss.com)
Originally published at jacoporanalli.com ・4 min read

Now that the Notion API is in beta, nothing could stop me anymore from trying it out. This quickly ended up in a weird experiment called Notion Playgrounds.

Here's how I built it, using Nextjs, TailwindCSS and Nightwind to automatically generate the dark mode.


Interacting with Notion API

First of all, I created an api route to handle my Notion GET requests. In this case I was interested in retrieving the contents of a Notion page, so I needed the Retrieve block children endpoint.

I knew I wanted to build more than one page, so I built a dynamic API route that accepts a pageId as the query parameter, and returns me the corresponding object from the Notion API.

// api/notion/[pageId].ts
export default async function handler(req, res) {
  const { pageId } = req.query
  const endpointBlocks = `https://api.notion.com/v1/blocks/${pageId}/children`

  const fetcher = (url, options?) =>
    fetch(url, options).then((res) => res.json())

  const data = {
    headers: {
      Authorization: `Bearer ${process.env.NOTION_KEY}`,
      "Content-Type": "application/json",
      "Notion-Version": "2021-05-13",
    },
  }

  const blocks = await fetcher(endpointBlocks, data)
  res.status(200).json({ blocks })
}
Enter fullscreen mode Exit fullscreen mode

Rendering the page & the swr magic ✨

There were 3 things I wanted:

  1. Generate the pages statically, to make everything lightning-fast and SEO friendly
  2. Have dynamic routes, so I could have all pages under the paths /playground/1, /playground/2, etc.
  3. Fetching data from Notion in real-time, with a stale-while-revalidate pattern.

Dynamic routes & Static Generation

Static generation in Nextjs happens through the getStaticPaths and getStaticProps functions.

GetStaticPaths is used to get the actual paths of the page, while getStaticProps returns the props to each page – in this case the slug and the pageId props.

// pages/playground/[slug].tsx

export async function getStaticPaths() {
  return {
    paths: [
      {
        params: {
          slug: "1",
        },
      },
      {
        params: {
          slug: "2",
        },
      },
      {
        params: {
          slug: "3",
        },
      },
    ],
    fallback: false,
  }
}

export async function getStaticProps(context) {
  const slug = context.params.slug
  const pageIdArray = [
    "38fc182c459340b294fca3c99b88faae",
    "9e569a521efc4f0fa2087de12fca5e81",
    "59a5889031314f73a2c1bd268a486dff",
  ]
  const pageId = pageIdArray[slug - 1]
  return {
    props: {
      slug,
      pageId,
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

Rendering the data on page

Getting the data to display on website is trivial when using Nextjs and the (amazing) SWR hook.

I simply have to make an API call to the API route I built in the first step, using SWR and a fetcher function. This will return me the data to display on page, while caching and revalidating automatically.

export default function Play({ slug, pageId }) {
  const fetcher = (url, options?) =>
    fetch(url, options).then((res) => res.json())

  const { data, error } = useSWR(`/api/notion/${pageId}`, fetcher)

  return (
    // Return page using data returned from the api
  )
}
Enter fullscreen mode Exit fullscreen mode

At this point I just had to render what Notion was returning from my request.

In this case, that would be an array of block objects, each containing an array of Rich text objects containing the styling properties.

Note: Some blocks appear to be unsupported at this time, and others have children blocks that would've needed to be retrieved recursively. I didn't build this feature yet, so I made sure such block returned either a line element or null.

// ...
<div className="prose">
  {!error && data ? (
    data.blocks.results.map((line, key) => {
      if (line.paragraph) {
        return (
          <p key={key}>
            {line.paragraph.text.map((t, k) => {
              return <NotionSpan key={k} t={t} />
            })}
          </p>
        )
      } else if (line.heading_1) {
        return (
          <h1 key={key}>
            {line.heading_1.text.map((t, k) => {
              return <NotionSpan key={k} t={t} />
            })}
          </h1>
        )
      }
      // ... All elements that can be contained in the block objects
      else if (
        line.type === "unsupported" &&
        line.object === "block" &&
        !line.has_children
      ) {
        return <hr key={key} />
      } else {
        return null
      }
    })
  ) : (
    <p className="text-gray-500 text-center">Ready in 3, 2, 1...</p>
  )}
</div>
Enter fullscreen mode Exit fullscreen mode

To handle text styling I wanted to leverage the amazing TailwindCSS palette while automatically build a dark mode with Nightwind.

So I made a <NotionSpan> component which returns the text content of the Rich text object, wrapped in a <span> element with styling applied depending on the properties of the Rich text object.

I don't have to think about styling the dark mode because Nightwind does it automatically for me.

import { FC } from "react"

export interface NotionSpanProps {
  t: any;
}

const colors = {
  text: {
    gray: "text-gray-600",
    brown: "text-amber-700",
    orange: "text-orange-600",
    yellow: "text-yellow-600",
    green: "text-green-600",
    blue: "text-blue-600",
    purple: "text-purple-500",
    pink: "text-pink-600",
    red: "text-red-500",
  },
  bg: {
    gray: "bg-gray-200",
    brown: "bg-amber-200",
    orange: "bg-orange-200",
    yellow: "bg-yellow-200",
    green: "bg-green-200",
    blue: "bg-blue-200",
    purple: "bg-purple-200",
    pink: "bg-pink-200",
    red: "bg-red-200",
  },
}

const NotionSpan: FC<NotionSpanProps> = ({ t }) => {
  return (
    <span
      className={`
      ${t.annotations.bold ? "font-bold " : ""}
      ${
        t.annotations.code
          ? "font-medium overflow-hidden shadow-md rounded-sm py-2.5 px-5 bg-gray-100 text-indigo-600"
          : ""
      }
      ${
        t.annotations.color !== "default"
          ? t.annotations.color.includes("background")
            ? `${colors.bg[t.annotations.color.split("_")[0]]} text-gray-900 `
            : colors.text[t.annotations.color]
          : ""
      }
      ${t.annotations.italic ? "italic " : ""}
      ${t.annotations.strikethrough ? "line-through " : ""}
      ${t.annotations.underline ? "underline " : ""}`}
    >
      {t.text.link ? (
        <a href={t.text.link.url} target="_blank" rel="noopener">
          {t.text.content}
        </a>
      ) : (
        `${t.text.content}`
      )}
    </span>
  )
}

export default NotionSpan
Enter fullscreen mode Exit fullscreen mode

Wrapping up

And that's it! Notion API makes it really easy to get the data of a Notion page (and databases seem to be even more powerful, can't wait to try that too!), while Nextjs + swr spectacularly handle all the complex parts giving you the best possible experience.


If you liked this post or have any question, feel free to let me know on Twitter!

And if you like this whole experiment, consider leaving a note on the Notion Playgrounds and upvoting it on Product Hunt. Thanks! 🍩

Discussion (0)