6ch

Full stack developer, video game designer.

Plays shooters, fighters, and rhythm games.

The number of times this site has been accessed.

[← Go back]
Building a Dynamic Blog Site with Notion as CMS
#306 JUL 2026Last edited 06 JUL 2026
#
Preface

Before I got started building this site, I was casually scrolling through lists of recommended blog frameworks on Google. I don’t want a static site generator that I have to push and deploy every time I make a change, nor do I want to self host any backends. Then my mind came to our old friend, Notion. Notion is a notetaking service that allows us to write stuff in a format similar to Markdown (it’s not actually Markdown).

Note: I, nor this site, is affiliated with Notion. I am not advertising for Notion; however, I enjoy Notion so much that I strongly advise you to check it out.

Notion has a developer API that allows us to pull pages. We could write out blog posts on Notion, fetch them in the backend, and somehow render it as HTML on the frontend. This would save us tons of hassle. We won’t even have to host any images and large media anymore, we can pull them straight from Notion.

And the best of all, Notion is free.

#
Preparation
  • A Notion account.
  • A Notion API key.
  • A Notion database to store our posts.
  • Some sort of backend (I’m using NextJS, a hybrid between backend and frontend, for this site).
  • NodeJS libraries:
    • @notionhq/client: Fetches databases and pages from Notion.
    • notion-to-md: Converts Notion blocks to a Markdown document.
    • react-markdown: Renders a Markdown document as HTML.
    • shiki: Syntax highlighting for code blocks.
#
Setting Up Notion

We start by creating a database to contain your posts. Each post will be an item in the database. Databases are easier to deal with than regular pages, and you can also fetch database items with custom filter options.

Give it an ID column, a title column, and a checkbox column named Published. We want to be able to control what pages we show to the public; the Published checkbox is that control.

You could also add additional columns to fit your needs.

image.png

Create a page and name it something like “test post”. Check the Published checkbox to make it visible to the public.

##
Getting an API Key

You’ll need to get a Notion API Key with access to your newly created database page. Learn how to get one here:

https://developers.notion.com/guides/get-started/overview[↗]

##
Getting Your Database’s ID

You’ll need your database’s ID to interact with it using the Notion client library.

Notion separates Database Views with Data Sources. A database view is a specific way in which a data source is displayed. There may be multiple views to the same data source. We need the data source’s ID, not the view’s ID.

To get your data source’s ID, click on the settings button located to the left of the big blue New button. Choose Manage Data Sources, select your database, then select Copy data source ID.

image.png

Keep this ID somewhere safe on the server. For instance, in a .env file, along with your notion API key:

[Copy]plain
NOTION_API_KEY=xxxxxxxx
NOTION_BLOG_DATASOURCE_ID=184b68b99b7b4acd8c46663451e2a881
#
Listing Posts

We would want a “Blog Posts” page that lists all of our posts. To do this, we use the Notion client library to fetch all our rows from our database.

First, create a Notion client with your API key:

[Copy]typescript
"use server";
import {
    Client,
    QueryDataSourceResponse,
    PageObjectResponse,
    RichTextItemResponse,
    RichTextPropertyItemObjectResponse, CheckboxPropertyItemObjectResponse, UrlPropertyItemObjectResponse
} from "@notionhq/client";

const NOTION_API_KEY = process.env.NOTION_API_KEY || "";
const NOTION_BLOG_DATABASE_ID = process.env.NOTION_BLOG_DATABASE_ID || "";

const client = new Client({
    auth: NOTION_API_KEY,
})

We then query our database using its id. Remember to add a filter for the Published column to only select posts we set to be visible.

[Copy]typescript
export async function ListBlogPosts(): Promise<BlogPostInfo[]> {
    const res = await client.dataSources.query({
        data_source_id: NOTION_BLOG_DATABASE_ID,
        filter: {
            and: [
                {
                    property: "Published",
                    checkbox: {
                        equals: true,
                    }
                }
            ]
        }
    });
}

This spits out a result object with a result array, containing our posts. Each item in the array represents a page (since Notion database items are also pages). They look something like this (UUIDs in this example have been randomized):

[Copy]json
{
    "object": "page",
    "id": "a0500c47-3eb2-4113-be2f-cf2dadeefaaa",
    "created_time": "2026-07-06T01:48:00.000Z",
    "last_edited_time": "2026-07-06T02:21:00.000Z",
    "created_by": {
        "object": "user",
        "id": "914e320a-82c0-465e-83fa-b60d41f54d2a"
    },
    "last_edited_by": {
        "object": "user",
        "id": "be7af06d-ffc3-4bdd-a98f-0a05f06e24db"
    },
    "cover": null,
    "icon": null,
    "parent": {
        "type": "data_source_id",
        "data_source_id": "fd7248c7-5234-469f-9c27-8a5b4ffa82e8",
        "database_id": "43a209c4-b8ec-4910-b098-7749405779a8"
    },
    "in_trash": false,
    "is_archived": false,
    "is_locked": false,
    "properties": {
        "Last edited time": {
            "id": "A%5EvH",
            "type": "last_edited_time",
            "last_edited_time": "2026-07-06T02:21:00.000Z"
        },
        "ID": {
            "id": "OTF%5C",
            "type": "unique_id",
            "unique_id": {
                "prefix": null,
                "number": 3
            }
        },
        "Thumbnail": {
            "id": "P%5BXw",
            "type": "files",
            "files": []
        },
        "Created time": {
            "id": "TDfQ",
            "type": "created_time",
            "created_time": "2026-07-06T01:48:00.000Z"
        },
        "Published": {
            "id": "csb_",
            "type": "checkbox",
            "checkbox": true
        },
        "Title": {
            "id": "title",
            "type": "title",
            "title": [
                {
                    "type": "text",
                    "text": {
                        "content": "Building a Dynamic Blog Site with Notion as CMS",
                        "link": null
                    },
                    "annotations": {
                        "bold": false,
                        "italic": false,
                        "strikethrough": false,
                        "underline": false,
                        "code": false,
                        "color": "default"
                    },
                    "plain_text": "Building a Dynamic Blog Site with Notion as CMS",
                    "href": null
                }
            ]
        }
    },
    "url": "https://app.notion.com/p/Building-a-Dynamic-Blog-Site-with-Notion-as-CMS-xxxx",
    "public_url": null,
    "archived": false
}

The properties array contains values for each column in our database. We could then use these properties to display our post listing.

#
Rendering A Post

To render a post, we first need to find the post in the database, fetch the page’s contents, then render it as HTML.

We could either take the post’s ID (the unique ID column we’ve added), or use the post’s page ID directly. I chose the former approach, which includes the additional step of finding the page in the database by it’s ID; if you choose the latter, you could skip straight to fetching the page’s contents.

##
Fetching Page Metadata

To fetch the page’s metadata by its numeric ID, we simply perform another database query with a filter set for its numeric ID.

[Copy]typescript
const res = await client.dataSources.query({
    data_source_id: NOTION_BLOG_DATABASE_ID,
    filter: {
        and: [
            {
                property: "Published",
                checkbox: {
                    equals: true,
                }
            },
            {
                property: "ID",
                unique_id: {
                    equals: pageId,
                }
            }
        ]
    },
});

The id field in our response object is the page’s real ID. We then use this ID to fetch the page’s content.

##
Fetching Page Content

The notion-to-md library provides a function to fetch a page and convert it to Markdown in a single step. We need to create a NotionToMarkdown instance first, and pass it our Notion client.

[Copy]typescript
const n2m = new NotionToMarkdown({
    notionClient: client,
});

// ...

async function GetNotionPage(pageId: string) {
    const blocks = await n2m.pageToMarkdown(pageId);
    return n2m.toMarkdownString(blocks);
}
##
Rendering Markdown

Now that we have our page in Markdown, we use react-markdown to render it. toMarkdownString returns a MdStringObject, and we can access the Markdown in raw string format with its parent field.

[Copy]typescript
function Markdown(props: {
    children: string;
}) {
    return (
        <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} components={{
            h1: props => (
                <div className={"flex gap-2 items-center"}>
                    <span className={"text-muted-foreground"}>#</span>
                    <header className={"tracking-widest font-bold text-base"}>{props.children}</header>
                </div>
            ),
            h2: props => (
                <div className={"flex gap-2 items-center"}>
                    <span className={"text-muted-foreground"}>##</span>
                    <header className={"tracking-widest font-bold"}>{props.children}</header>
                </div>
            ),
            h3: props => (
                <div className={"flex gap-2 items-center"}>
                    <span className={"text-muted-foreground"}>###</span>
                    <header className={"tracking-widest"}>{props.children}</header>
                </div>
            ),
            ul: props => (<ul className={"list-disc list-inside"}>{props.children}</ul>),
            a: props => (<CommonLink href={props.href || ""} target={"_blank"}>{props.children}</CommonLink>),
        }}>
            {props.children}
        </ReactMarkdown>
    )
}

And there we go! We have successfully rendered a blog post from Notion in our website.

#
Optimization

Obviously, the above demonstration is barebones and lacks optimization. Making a call to the Notion API every time somebody loads up a blog post is obviously not idiomatic.

This section will be left incomplete as of right now. I am still experimenting with optimization strategies for this blog; once I have gathered enough experience, I’ll come back and finish this section.

##
Caching
##
Access Speed