Next.js Blog using Typescript and Notion API
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. <!--more--> 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
- Prerequisites
- Overview
- Setting up the database on Notion
- Setting up an integration on Notion
- Setting up the Next.js application
- Querying multiple posts
- Querying single post
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.
Navigate to the first default page, page 1
, and click on it. Change the title as you prefer.
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.
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:
Add dummy body to the post body.
Add an image. Select an image from Unsplash.
Append some conclusions.
Click outside the modal when done. The post should now be listed as shown below:
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:
Create a new integration. To do so, click on Develop your Integration
. Then click the plus button to set up a new integration:
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.
Go back to the workspace created in the previous step, where you created the pages, i.e., Latest posts
, and click on Share
:
Click on Invite. On the resulting modal, you should be able to see the integration just created:
Click on the blog_app_integration
integration, and then Invite
to add your pages to this new notion 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:
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.
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:
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