Using Notion as a CMS for anything that reads markdown


As you might see from the fact that this blog resides on Mirror instead of Notion, I didn’t end up using this, but I reckon some might get usage out of the code that I wrote. :)

TL;DR? *The finished code can be found, forked and tinkered with behind this link: *


While looking out for fun new things to try out on my personal website, I thought of using an external CMS for my blog posts, music and photos. Notion works well for a blog, given its applications on multiple platforms allows you to write basically from anywhere with anything, but the free tier puts some limit on how widely it can be used. I also looked into Contentful, but it’s marketed for businesses and didn’t feel right. Well, I’ve got a .com domain so maybe I’d pass as one as an individual, but I still went with Notion for this project.

The idea was to be rather frontend-agnostic as I have been experimenting with different static site generators for my personal website in the past, from just HTML + CSS to **Gatsby **to **Hugo **to **Zola **to **Next.js. **All are great for blogs, but Gatsby & Next.js really shine when you want to do something custom, as they’re both React-based which a lot of devs are familiar with nowadays. Wanted to try doing the website with Rust (yew) first, but since yew didn’t have a stable or a de facto way of doing static site generation, I decided that the scope might be a bit too large for a personal website.

Why exactly use Notion as the CMS then? I’ve been inspired by the folks at ** **for a while, which is a way to turn a Notion page into a website. Also, the whole developer community around **Notion CMS **has been inspiring, and because of their work I knew it was possible to use Notion as the CMS for any site. Not that I especially stan Notion, but the breadth of functionality it provides is pretty unique. Google docs can also be used as a CMS for everything but that just doesn’t feel *right. *

For non-paying users Notion offers one page that can be called via their API, so if you thought of categorizing your blog posts or other site content in subpages, that’s only possible if you’re paying for Notion. That said, you usually need to do that after all if you’re using a hosted CMS service.

Figured that **Cloudflare Workers **would work wonders for this project, but any serverless platform would do, only that the deployment and the boilerplate code will look a bit different then.

Configuring Notion

Let’s keep it short: You need to create your own integration in Notion, get the Internal Integration Secret and then head on to the page that you would like to use as a CMS in Notion, and give your recently made integration page permissions. I save space and direct you to their guide instead for this part. Come back when you’re done with it. :)

Reading Notion with a Cloudflare Worker

For the worker, I used the packages itty-router, @notionhq/client and notion-to-md. Itty-router is a simple router originally designed just for this job, and it was a part of the template code I used for the worker. Notion client is self explanatory, it’s a wrapper/helper for their API, and notion-to-md is a library that uses the client to fetch pages and convert them to Markdown. All that was left for me to do was plug all these components together.

The worker needs two routes:

  1. The index route

    1. Fetches the whole database defined by env.DATABASE_ID with the Notion client

    2. Passes the stringified response JSON as the worker’s response

  2. Route for individual pages

    1. Gets the wanted page ID as the input in its query

    2. Passes the input ID to notion-to-md’s method pageToMarkdown that produces “blocks”

    3. Those blocks get parsed to a Markdown string using the method toMarkdownString, which then gets returned as the worker’s response.

import { Client } from "@notionhq/client";
import { Router } from "itty-router";
import { NotionToMarkdown } from "notion-to-md";

const router = Router();

router.get('/', async (_request, env) => {
	const notion = new Client({
		auth: env.NOTION_TOKEN,

	const db = await notion.databases.query({
		database_id: env.DATABASE_ID,
  return new Response(JSON.stringify(db))

router.get('/:id', async ({ params }, env) => {
	const notion = new Client({
		auth: env.NOTION_TOKEN,

	const n2m = new NotionToMarkdown({ notionClient: notion });
	const blocks = await n2m.pageToMarkdown(;
  const page = n2m.toMarkdownString(blocks);
	return new Response(JSON.stringify(page));

export default {
	fetch: router.handle,

Yes. That’s the whole program, without the configuration files etc, ofc.

Anecdote – The Distinction Between Service & Module Workers

The difference is not how or where they’re deployed, but just the export format – if a Javascript module is exported, it’s automatically a module worker; if it adds an event listener for a ”fetch” event and listens to requests that way, it’s a service worker. Not that easy to get but after that distinction I got it. Caused a lot of problems in figuring out how the environment variables got exposed to code – every place mentions they’re globals but in module workers they’re not: they’re passed as props to the fetcher instead.

Subscribe to jantto
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
This entry has been permanently stored onchain and signed by its creator.