CMS hosted website using Notion

Even though Notion API is in beta status it already offers stable and easy tools to connect with your workspace and retrieve page content. Actually, this website is built upon Notion.


I decided to build myself a personal website as a first step to start working regularly on side projects. Also, I don’t feel like updating my CV every now and then because summarising all your experience and highlight things in a more custom way is hard when you are bounded to do it on a simple PDF file...

I wondered for days how could I build a different website, something that doesn’t look so empty like “I’m Sergio and this is my experience, that’s all folks” (if you website looks like that don’t take this post personally 😅 ). At some point I found out the Octopus Energy CTO’s personal page which had a TIL (Today I Learnt) section with small pills of information that he was learning from time to time and that can be useful for others. That’s a great idea!! So I started to build something similar.

I realised from the very beginning that this web should be server-less, as there is no logic aside from handling content that I could be storing on a CMS. In Factorial we use DatoCMS for that, a good option to build kinda complex structures, and manage content for a bunch of different users. Unfortunately, I’m not a big fan of their API docs so I decided to give Contentful a try and see if it could be a suitable alternative.


Different tools, but the same pains IMO... I found myself spending about half an hour to define the models I wanted for this website, e.g. a Page, a TextSection, a MarkdownSection, ... Again I had to face some not very clear API docs, but not really a big deal. Once everything was setted up, I faced the most important problem, which made me red-flag it and look for another solution: the friction to generate content fast and all at once. I had to create one component every time, write in editors that didn’t provide the best experience, if I wanted to change some block I had to remove it, but copying things before-hand...

As a matter of casualty that week I read a very interesting message related to the brand new Factorial engineering blog. Some dev was explaining the approach they followed and it was exactly what I needed:

“Any amount of friction that gets in the middle of writing a blog article will be used an excuse, so we’ve thought that using Notion as a repository looks like a great idea.”

Notion API was in beta, but it looked powerful enough to build a simple website like this so I decided to give it a try. Best decision ever.


Probably this post requires some more code fragments than others, because connecting to an API, specially to a CMS and retrieving/handling the content from it is not trivial. Is not hard, but it can’t be done it in seconds (at least I can’t), if you follow the guides, you should find almost no problems.

First of all, you’ll have to get familiar with the concept of databases that Notion uses internally, because these are the places where you can store your content. In fact they are nothing more than easy structures like tables, boards, or timelines (there are more, check the docs). Some pseudocode to fetch information from these database would look like this:

1 2 3 4 5 6 7 8 9 10 11 import { Client } from "@notionhq/client" const fetchNotion = async ( query: Query, handler: any ) : Promise<ValidFetchResult> => { const notion = new Client({ auth: process.env.NOTION_KEY }) const response = await notion.databases.query(query) return }

The steps are more or less the same that you need to follow in order to fetch any other API:

  • You start the connection with Notion client using your NOTION_KEY.
  • You pass some query to the databases.query function to retrieve the data you want.
  • You use a handler to sanitise the results.

The queries are not hard to understand either, below you can find an example of some query:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const getPageBySlugQuery = (slug: string) => { return { database_id: process.env.DATABASE_ID, sorts: [ { property: 'Publish date', direction: 'ascending', }, ], filter: { property: 'Slug', text: { equals: slug } } } }

An easy to understand query that anyone could understand without even reading the function name. All queries are oriented towards a specific database, so you have to provide its ID.

The only “problem” I found while using Notion API is that querying an item from a database doesn’t return the “Page content” with it. If you’ve ever used Notion (if you hadn’t give it a try), when you create an item inside a database it has some properties that you define (those above fold when you open the page), and then the rest of the content (below fold) that includes any kind of Notion block to format your content as you want.

If you want to access the content of some database item you’ll have to do two queries (at least this is what I had to do), one to obtain the item that you are looking for, and another one to retrieve the content using that item ID → If you know another way to simplify this process, pleas ping me, I’d love to know more about this 🙏

1 2 3 4 5 6 7 8 9 10 const handler = (pageItem: any) => { const notion = new Client({ auth: process.env.NOTION_KEY }) const pageContent = await notion.blocks.children.list({ block_id: }) return { content: pageContent.results } }

Notice that the query in this case is a bit different, we search for block_id now, rather than by database_id, and also the endpoint we use from the Notion client in this case is blocks.children. Small differences, but doesn’t add a lot of complexity, though it would be great to do this operation all at once of course.


I want to thank:

  • Notion team for releasing an API which I can use for free (at least for now I think 🤔 ) and is a great option for short-term content management.
  • My workmate Josep Jaume, that posted the idea to use Notion API as the base for our blog. Not going to lie, how Factorial engineering blog was built is a great inspiration for this website. Despite that, I built most things here before the blog was launched, so is very far from being a copy/paste 🙂