Unlike most note-taking applications, Notion features a full suite of notation-like tools, from tools to track tasks to ones that help with journaling, all in one place. If you're an experienced user, you may have used some of their advanced concepts like tables as databases, custom templates, and table functions.
As of May 13, 2021, Notion released a beta version of their API client, opening up another range of possibilities for the app. Users of the client can query databases, create pages, update blocks, perform searches, and much more.
We'll look at how easy it is to get started with the client today.
Note: For this simple demonstration, we won't use authentication, but it's highly recommended no matter what type of app you decide to create.
Project Setup
We have a few options for building a scalable blog site. For this stack, we'll be using NextJS because it's relatively lightweight compared to most frameworks, and has several readily available features for a standard React app. But Remix works just as well.
In a folder of your choice, create the NextJS app with:
npx create-next-app@latest
or
yarn create next-app
Note: add --typescript
at the end to generate a TypeScript project.
I'm using the TypeScript project for this demo.
If you're unfamiliar with NextJS, I recommend their Getting Started Guide. In this new project folder, we have all the necessary things to run the app.
Install the necessary dependencies:
npm install -S @notionhq/client
Create the Notion Database
For this next step and the next, you'll need a Notion account to create databases, and an integration key.
Now, create a table for your blog posts in Notion. This will become the database for referencing posts for the site.
I'm making this generic developer blog database, but feel free to make your database specific to a target audience or subjects.
Notion Setup and Integration
Before using Notion's API to query any data, we need access via an integration key. Head over to the Notion's authorization guide and follow the instructions to create your key. I'm using the most basic setup for reading from my database.
Continue with the integration key guide up to Step 2 which references how to grant the intergration key rights to the database.
With the integration key and database ID handy, let's configure the app to use them.
Querying the database
In your favourite code editor, create an .env.local
file with the following:
NOTION_API_KEY=secret_xxxxxxxxxxxxxxxxxxxxxx
NOTION_DATABASE_ID=xxxxxxxxxxxxxxxxxxxxxx
Note: it won't matter if you wrap your values in quotations.
NextJS comes with the ability to read local environment variables. It also ignores versions of this file in its .gitignore
so we don't have to. If publishing an app to production, it's recommended to use environment variables.
Next, create a src/api/client.ts
file at the root of the project. This will contain an easily referenced version of our client for querying Notion.
import { Client } from '@notionhq/client';
const NOTION_API_KEY = process.env.NOTION_API_KEY ?? '';
export const notion = new Client({ auth: NOTION_API_KEY });
Follow that up with a src/api/query-database.ts
file:
import { notion } from './client';
export const queryDatabase = async () =>
await notion.databases.query({
database_id: process.env.NOTION_DATABASE_ID ?? '',
});
Parse the Query
Because the returned query object is so complex, we need to parse it. There are better ways to do this using more advanced techniques, but basic mapping will suffice.
In a new src/utils/parse-properties
file:
import { QueryDatabaseResponse } from '@notionhq/client/build/src/api-endpoints';
export type Post = {
id: string;
title: string;
};
export const parseProperties = (database: QueryDatabaseResponse): Post[] =>
database.results.map((row) => {
const id = row.id;
const titleCell = row.properties.Title.title;
const title = titleCell?.[0].plain_text;
return { id, title };
});
Now we can leverage NextJS server rendering feature via getStaticProps
to prefetch and render our Home page. In index.tsx
:
import type { NextPage } from 'next';
import Head from 'next/head';
import { queryDatabase } from '../src/api/query-database';
import styles from '../styles/Home.module.css';
import { parseProperties, Post } from '../src/utils/parse-properties';
type HomeProps = {
posts: Post[];
};
const Home: NextPage<HomeProps> = ({ posts }) => {
return (
<div className={styles.container}>
<Head>
<title>My Notion Blog</title>
<meta
name="description"
content="Notion blog generated from Notion API"
/>
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<h1>My Notion Blog</h1>
<ul>
{posts.map(({ id, title }) => (
<li key={id}>{title}</li>
))}
</ul>
</main>
</div>
);
};
export async function getStaticProps() {
const database = await queryDatabase();
const posts = parseProperties(database);
return {
props: {
posts,
},
};
}
export default Home;
We should now see our post titles loaded on the Home page when we run npm run dev
.
Finishing Up
With a few setup pieces, it's easy to setup a basic blog using Notion's API, but there are more possibilities and use cases for Notion as a CMS. Keep in mind that this may not be the best database to use in a production environment, but playing around with one of my favourite tools creates some new possibilies for non-Notion-tailored experiences.