EngEd Community

Section’s Engineering Education (EngEd) Program fosters a community of university students in Computer Science related fields of study to research and share topics that are relevant to engineers in the modern technology landscape. You can find more information and program guidelines in the GitHub repository. If you're currently enrolled in a Computer Science related field of study and are interested in participating in the program, please complete this form .

Next.js Blog using Typescript and Notion API

March 25, 2022

Next.js is used to create Server-side Rendering (SSR) and Static Site Generation (SSG) using JavaScript. The app fetches extra data from the server after the browser loads the website’s HTML page.

Technologies such as SSG often have to rebuild the app when data from the source is updated and render it to the user at build-time, making the site load even faster, creating a better user experience.

This guide will help the reader learn how to use Next.js with Notion API to create a blog app powered by Typescript code.

Prerequisites

To follow along with this article, it is helpful to have the following:

  • Node.js installed on your computer.
  • Basic knowledge working with Typescript and Next.js.

Overview

Setting up the database on Notion

First, create a notion account. If you already have an account, just login or register a new account.

Once you have created the account, hover over the Getting Started section of the dashboard page and click on the plus icon to add a new notion page.

Under Database, click on list on the resulting popup. A sample skeleton will be loaded. Go ahead and enter a project title.

list-database-skeleton

Navigate to the first default page, page 1, and click on it. Change the title as you prefer.

page-title-change

You can choose to change the icon and cover image using the free images from Unsplash. Hover over the page title and click the Add cover button to add the cover image.

cover_image_change

cover_image_changed

Every new page created is a blank canvas where you can add content such as plain text, lists, and images. To add content to a page, scroll down to the content section and add some prerequisites to your blog page as shown below:

page-content-section

prerequisites_section

Add dummy body to the post body.

body_section

Add an image. Select an image from Unsplash.

image_section

Append some conclusions.

conclusion_section

Click outside the modal when done. The post should now be listed as shown below:

database_posts

Repeat the same process for page 2 and page 3. You can add other posts the same way.

Setting up an integration on Notion

Navigate to the Settings & Members section of your notion dashboard page. Click Integrations under the Workspace section on the resulting modal. Then set the notion integration as shown in the following steps:

notion_integrations

Create a new integration. To do so, click on Develop your Integration. Then click the plus button to set up a new integration:

create_new_integrations

Name the integration blog_app_integration, then click Submit to set it up. Once done, you should be able to view the notion integrations settings, i.e., the integration token.

Scroll down and click save changes to reveal the notion-integration key to save this integration. Copy this key for use in connecting to Next.js.

notion_secrets

Go back to the workspace created in the previous step, where you created the pages, i.e., Latest posts, and click on Share:

notion_share

Click on Invite. On the resulting modal, you should be able to see the integration just created:

notion_integrations

Click on the blog_app_integration integration, and then Invite to add your pages to this new notion integration.

selected-integration

With that, you will be able to access your workspace using the integration-generated token.

Setting up the Next.js application

To set up a Next.js project, create a project folder, then run the following command to bootstrap the application inside the created directory:

npx create-next-app blog_app --ts

The --ts flag allows your app to run using Typescript.

This command will create a basic Next.js app inside the folder blog_app. Once the process is done, navigate to the blog_app folder using the command cd blog_app and install the notion client package:

npm install @notionhq/client

On the project root folder, create a .env file. This file will host the notion integration key that Next.js needs to access and connect with the notion API. Go ahead and add the two notion variables, the integration token key and the database id:

NOTION_KEY=""
NOTION_DATABASE=""

Paste the integration key copied earlier and add it to the NOTION_KEY value. If you did not copy this key, navigate to the integration page, under Secrets, click on Show and then Copy and paste it in the NOTION_KEY entry.

To get the NOTION_DATABASE ID, check your workspace page URL. Copy the first path parameter before the query parameter as shown in the illustration below:

notion-api

In this case the id would be 53905ad838f04731b48fb1e40c25766a. Let us say that your workspace URL is https://www.notion.so/your_database_id?v=some_long_hash. The parameter your_database_id should be the NOTION_DATABASE.

Start the development server to test the app.

npm run dev

Navigate to http://localhost:3000; you should be able to view the default Next.js page.

Querying multiple posts

On the project root folder, create a folder named lib. Inside lib, create a file notion.ts. Then add the following code to query multiple posts from the notion API.

Start by importing the notion client package:

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

Instantiate the notion client using your notion integration key:

const client = new Client({
   auth: process.env.NOTION_KEY,
});

Define a function to get the posts. This function processes and queries the list of posts from the notion database:

async function posts() {
   const myPosts = await client.databases.query({
     database_id: `${process.env.NOTION_DATABASE}`,
   });
   return myPosts;
}

Export an object with the function. This export will make the function accessible by other files inside the project:

export {
   posts
}

On pages/index.tsx, import the function you have defined above and the Next.js link dependencies:

import Link from 'next/link';
import {posts} from '../lib/notion'

Then, fetch the posts from the server-side using the Next.js getServerSideProps() function:

export async function getServerSideProps() {
   // Get the posts
   let { results } = await posts();
   // Return the result
   return {
     props: {
       posts: results
     }
   }
}

Define an interface for the props. This interface creates the structure of posts and holds the array of posts:

interface Props {
   posts: [any]
}

To show the list of posts, render the posts to the view that lists down the fetched posts from the server-side:

const Home: NextPage<Props> = (props ) => {
   return (
     <div className={styles.container}>
     <Head>
       <title>Latest posts</title>
     </Head>

     <main className={styles.main}>
       <h1 className={styles.title}>
       Latest posts
       </h1>
       {
         props.posts.map((result,index) => {
         return (
           <div className={styles.cardHolder} key={index}>
           <Link href={`/posts/${result.id}`}>
             <Image src={result.cover.external.url} width={300} height={200} />
           </Link>
           <div className={styles.cardContent}>
             <Link href={`/posts/${result.id}`}>
             <a className={styles.cardTitle}>{
             result.properties.Name.title[0].plain_text
             }</a>
             </Link>
           </div>
           </div>
           )
         })
       }
     </main>

     <footer className={styles.footer}>
       <p>Blog application</p>
     </footer>
     </div>
   )
}

For the Next.js application to load images, you must configure the image hostname/domain under images in your next.config.js.

In this example, you loaded images from unsplash.com. To add this domain, navigate to the next.config.js and configure the unsplash.com image source as shown below:

const nextConfig = {
  reactStrictMode: true,
  images: {
    domains: ['images.unsplash.com']
  }
}

Add the following styles to styles/Home.module.css. This will style the fetched posts:

.container {
  padding: 0 2rem;
}

.main {
  min-height: 100vh;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.cardHolder {
   display: flex;
   width: 40%;
   margin: 10px auto;
   justify-content: space-between;
   padding: 10px;
   border: 1px solid #d4d4d4;
}

.cardContent {
   width: 100%;
   display: flex;
   align-items: center;
   justify-content: center;
}

.footer {
  display: flex;
  flex: 1;
  padding: 2rem 0;
  border-top: 1px solid #eaeaea;
  justify-content: center;
  align-items: center;
}

.footer a {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-grow: 1;
}

To test this code, ensure the development server is up and running. You should now be able to view the posts on the home page.

posts_page

Querying single post

Let us create a function that will fetch a single post from the notion database.

Navigate to the lib/notion.ts file and add a function to get a single post based on the post id as shown below:

async function post(id: string) {
   const myPost = await client.pages.retrieve({
     page_id: id,
   });
   return myPost;
}

Add another function inside the lib/notion.ts file. A post has other properties such as prerequisites and conclusion, which can be referred to as the children’s properties of a particular post.

Create a function blocks() to get the children (blocks) of a particular post:

async function blocks(id: string) {
   const myBlocks = await client.blocks.children.list({
     block_id: id
   });
   return myBlocks;
}

Export the functions post() and blocks() to make them accessible by other files inside your project:

export {
   posts,
   post,
   blocks
} 

On the root folder, create a posts folder. Inside the folder, create an [id].tsx file. This file will serve the dynamic post based on the parameter id. In [id].tsx, add the following imports:

import { GetStaticProps, NextPage, GetStaticPaths } from 'next';
import Image from 'next/image';
import Head from 'next/head';
import Link from 'next/link';
import { ParsedUrlQuery } from 'querystring';
import { post, posts, blocks } from '../../lib/notion';
import styles from '../../styles/Home.module.css';

Next, implement an interface for this context. This interface will be applied when getting the dynamic id:

interface IParams extends ParsedUrlQuery {
   id: string
}

Get the dynamic post and the children properties from the server-side:

export const getStaticProps: GetStaticProps = async (ctx) => {
   let { id } = ctx.params as IParams; 
   // Get the dynamic id
   let page_result = await post(id); 
   // Fetch the post
   let { results } = await blocks(id); 
   // Get the children
   return {
     props: {
       id,
       post: page_result,
       blocks: results
     }
   }
}

Implement the paths for fetching all posts using getStaticPaths. Then map the results using the parameter id. This will help Next.js to go through every fetched post and display it based on its dynamic id:

export const getStaticPaths: GetStaticPaths = async () => {
   let { results } = await posts(); 
   // Get all posts
   return {
     paths: results.map((post) => { 
       // Go through every post
       return {
         params: { 
           // set a params object with an id in it
           id: post.id
         }
       }
     }),
     fallback: false
   }
} 

Implement an interface for the Props:

interface Props {
   id: string,
   post: any,
   blocks: [any]
}

Implement a function to render each child. For example, a single post has a heading, a hero image, the post content, and an unordered list of items. This function will help render them to from the server.

const renderBlock = (block: any) => {
   switch (block.type) {
     case 'heading_1': 
     // For a heading
       return <h1>{ block['heading_1'].text[0].plain_text } </h1> 
     case 'image': 
     // For an image
       return <Image src={ block['image'].external.url } width = { 650} height = { 400} />
       case 'bulleted_list_item': 
       // For an unordered list
       return <ul><li>{ block['bulleted_list_item'].text[0].plain_text } </li></ul >
       case 'paragraph': 
       // For a paragraph
       return <p>{ block['paragraph'].text[0]?.text?.content } </p>
     default: 
     // For an extra type
       return <p>Undefined type </p>
   }
}

Once the post has been rendered, create a view that will display the post to the user as shown below:

const Post:NextPage<Props> = ({id,post,blocks}) => {
   return (
     <div className={styles.blogPageHolder}>
       <Head>
         <title>
           {post.properties.Name.title[0].plain_text}
         </title>
       </Head>
       <div className={styles.blogPageNav}>
         <nav>
           <Link href="/">
             <a>Home</a>
           </Link>
         </nav>
       </div>
       {
         blocks.map((block,index) => {
           return (
             <div key={index} className={styles.blogPageContent}>
               {
                 renderBlock(block)
               }
             </div>
           )})
       }
     </div>
   )
}

Add the view export:

export default Post;

Next, add some style to the blogPageHolder class to format the rendered post:

.blogPageHolder {
   display: flex;
   flex-direction: column;
   justify-content: left;
   width: 50%;
   margin: 10px auto;
}

@media (max-width: 600px) {
  .grid {
    width: 100%;
    flex-direction: column;
  }
}

Ensure that the development server is running, and then click on any title of the posts on the home page:

single_post_page

Conclusion

This guide helped the reader set up a notion database. We then used the database with Next.js.

Check this project on this GitHub repository.

Happy coding!


Peer Review Contributions by: Jerim Kaura