A simple blog with NextJS and Notion

General Overview

This app uses Static Site Generation, with NextJS. Which means that the HTML is generated at build time. When I run yarn build, NextJS will essentially fetch all my notion data and parse it into it’s corresponding HTML. This means that the page only has to be built once, and the server doesn’t have to render the page on every request (meaning it is super fast!)

The following outlines how I made this blog. Some prerequisites would be to have a Notion account and get the Notion API key from this link.

Making a Database of Pages

The first thing I did was make a ‘table of contents’ to create a centralised location for where all my blog pages will be.

https://s3.us-west-2.amazonaws.com/secure.notion-static.com/92ac9ecf-787f-4d42-895c-af22fc55d07e/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20230825%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20230825T121716Z&X-Amz-Expires=3600&X-Amz-Signature=cc3f85db25a61a406f5319a59aa56ef50513d0e03dcd09d65ec44f52a2ab0bec&X-Amz-SignedHeaders=host&x-id=GetObject

Then with this database we can use Notion’s npm module

yarn add @notionhq/client

We are then able to query the database like so

const notion = new Client({ auth: process.env.NOTION_TOKEN })
const response = await notion.databases.query({ database_id: process.env.FRONT_END_DATABASE_ID})

const parsedData: DbData[] = response.results.map((result: any) => {
    const description = result.properties.Description.rich_text[0].plain_text
    return {
      id: result.id,
      url: result.url,
      createdAt: result.created_time || null,
      title: result.properties['Page Title'].title[0].plain_text,
      description:  description || '',
    }
  })

Note that I parsed the response to my own type which I called DbData[].

Creating a blog page

Using the pages directory. I create a directory under pages where blogId corrensponds to the id of each row in the database fetch (under the field id)

/pages/[blogId]/index.tsx

This is where I create all the blog pages. This is where we use getStaticProps and getStaticPaths to get all the possible page ids.

Create all blog paths

Inside this index.tsx file, I used getStaticProps (

// Inside getStaticProps

const dbData = await getDbData(process.env.NOTION_FRONTEND_DATABASE_ID)

const paths = dbData.map((data) => ({
    params: { blogId: data.id },
}))

Get page data

After getStaticProps is called, getStaticPaths gets invoked. This is where we pass in a param object (which conceptually contains each pageId from the row). At this point we fetch the page contents via notion.blocks.children request.

export async function getStaticProps({ params }) {
  const pageData = await getPageData(params.blogId)
  const dbData = await getDbData(process.env.NOTION_FRONTEND_DATABASE_ID)
	
  return {
    props: { pageData: pageData, dbData: dbData },
  }
}

// inside getPageData
const notion = new Client({ auth: process.env.NOTION_TOKEN })
  const response = await notion.blocks.children.list({
    block_id: pageId,
    page_size: 50,
  })

After getStaticPaths is invoked, we pass down pageData to the main component that is being rendered in the index.tsx file.

export const Page = ({ pageData, dbData }) => {
  const router = useRouter()

  const title =
    dbData.find((data) => data.id === router.query.blogId).title || ''

  return <BlogPageLayout blockData={pageData} pageTitle={title} />
}

Blog Page Layout

Then inside a <BlogPageLayout /> I have a prose component which basically wraps the blog using Tailwind’s prose. This is a pretty cool thing provided by Tailwind which basically is a typography plugin. Anything that is in children will basically be rendered in a specific style from tailwind (this is mainly used for markdown or HTML that the user has no control over). But I am a bit lazy and kind of like the styling.

export function Prose({ children, className }) {
  return (
    <div className={clsx(className, 'prose prose-sm dark:prose-invert')}>{children}</div>
  )
}

The next step is to simply parse all these blocks into what we want it to be. All the different types of blocks that I have decided to use

export const supportedBlockTypes = [
  'paragraph',
  'heading_1',
  'heading_2',
  'heading_3',
  'bulleted_list_item',
  'numbered_list_item',
  'code',
  'image',
  'unsupported',
]

// convert above array to type
export type SupportedBlockTypes = (typeof supportedBlockTypes)[number]

Then I basically map the following blocks within this prose div. In the future I can just add my own styling if I want. Also for code syntax highlighting I use a library called react-syntax-highlighter for pretty code snippets.

const renderBlockData = (blockData: BlockData) => {
    if (blockData.type === 'heading_1') {
      return (
        <h2 key={blockData.blockId}>
          {content}
        </h2>
      )
    } else if (blockData.type === 'heading_2') {
      return (
        <h3 key={blockData.blockId}>
          {content}
        </h3>
      )
    } else if (blockData.type === 'code') {
      return (
        <SyntaxHighlighter
          language="javascript"
          style={style}
          key={blockData.blockId}
        >
          {content}
        </SyntaxHighlighter>
      )
    }
... and so on

Conclusion

This is how I basically made this blog! Admittedly it is pretty simple, and I could be rendering the pages as MDX in the future one day. But if you would like to make a quick blog with Notion and NextJS integration that will only take a weekend to make then I hope this helps 😊