blog setup of this site

Tags
Published
Description
Created by
recordMap and src arg in custom image component of unsplash, embed link and notion hosted img
page record map data below, order is unsplash, notion hosted then embed link
"6568b258-9a85-4299-bf65-7f1bc270b34a": { "role": "reader", "value": { "id": "6568b258-9a85-4299-bf65-7f1bc270b34a", "version": 6, "type": "image", "properties": { "source": [ [ "https://images.unsplash.com/photo-1453728013993-6d66e9c9123a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8OXx8Y2hhbmdlfGVufDB8fDB8fA%3D%3D&w=1000&q=80" ] ] }, "format": { "display_source": "https://images.unsplash.com/photo-1453728013993-6d66e9c9123a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8OXx8Y2hhbmdlfGVufDB8fDB8fA%3D%3D&w=1000&q=80" }, "created_time": 1642335647171, "last_edited_by": "5a6fb216-9190-4ab3-a0a5-b37e6ddb8485", "last_edited_time": 1642335600000, "parent_id": "687b5560-294a-417d-b6fd-8b2b63872359", "parent_table": "block", "alive": true, "created_by_table": "notion_user", "created_by_id": "5a6fb216-9190-4ab3-a0a5-b37e6ddb8485", "last_edited_by_table": "notion_user", "last_edited_by_id": "5a6fb216-9190-4ab3-a0a5-b37e6ddb8485", "space_id": "b05838a9-a0e0-47b7-9398-b18e3d81c98c" } } "f7edc5d7-379b-4d8b-9647-f9ea3bbe3cbd": { "role": "reader", "value": { "id": "f7edc5d7-379b-4d8b-9647-f9ea3bbe3cbd", "version": 11, "type": "image", "properties": { "size": [ [ "88.0KB" ] ], "title": [ [ "95c89f42493a86b12451cf9e50667b49.jpeg" ] ], "source": [ [ "https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c3e29f1d-c649-47b6-9020-872ada0350f6/95c89f42493a86b12451cf9e50667b49.jpeg" ] ] }, "format": { "block_width": 576, "block_height": 450, "display_source": "https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c3e29f1d-c649-47b6-9020-872ada0350f6/95c89f42493a86b12451cf9e50667b49.jpeg", "block_full_width": false, "block_page_width": false, "block_aspect_ratio": 0.75, "block_preserve_scale": true }, "created_time": 1642312091154, "last_edited_by": "5a6fb216-9190-4ab3-a0a5-b37e6ddb8485", "last_edited_time": 1642312140000, "parent_id": "687b5560-294a-417d-b6fd-8b2b63872359", "parent_table": "block", "alive": true, "file_ids": [ "c3e29f1d-c649-47b6-9020-872ada0350f6" ], "created_by_table": "notion_user", "created_by_id": "5a6fb216-9190-4ab3-a0a5-b37e6ddb8485", "last_edited_by_table": "notion_user", "last_edited_by_id": "5a6fb216-9190-4ab3-a0a5-b37e6ddb8485", "space_id": "b05838a9-a0e0-47b7-9398-b18e3d81c98c" } }, "b76e221a-ddcd-4d52-816a-8b154724e6a3": { "role": "reader", "value": { "id": "b76e221a-ddcd-4d52-816a-8b154724e6a3", "version": 4, "type": "image", "properties": { "source": [ [ "https://www.visitbrisbane.com.au/~/media/articles/2016/october-2016/jacaranda/universityofqldjacaranda_imlee_20161022_web.ashx" ] ], "caption": [ [ "Jacaranda at University of Queensland" ] ] }, "format": { "block_width": 576, "display_source": "https://www.visitbrisbane.com.au/~/media/articles/2016/october-2016/jacaranda/universityofqldjacaranda_imlee_20161022_web.ashx", "block_full_width": false, "block_page_width": false, "block_aspect_ratio": 0.8333333333333334, "copied_from_pointer": { "id": "f9ad20b5-34a7-40fd-b348-e841dc91ef7c", "table": "block", "spaceId": "b05838a9-a0e0-47b7-9398-b18e3d81c98c" }, "block_preserve_scale": true }, "created_time": 1642249658661, "last_edited_by": "5a6fb216-9190-4ab3-a0a5-b37e6ddb8485", "last_edited_time": 1642249620000, "parent_id": "687b5560-294a-417d-b6fd-8b2b63872359", "parent_table": "block", "alive": true, "copied_from": "f9ad20b5-34a7-40fd-b348-e841dc91ef7c", "created_by_table": "notion_user", "created_by_id": "5a6fb216-9190-4ab3-a0a5-b37e6ddb8485", "last_edited_by_table": "notion_user", "last_edited_by_id": "5a6fb216-9190-4ab3-a0a5-b37e6ddb8485", "space_id": "b05838a9-a0e0-47b7-9398-b18e3d81c98c" } },
https://images.unsplash.com/photo-1453728013993-6d66e9c9123a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8OXx8Y2hhbmdlfGVufDB8fDB8fA%3D%3D&w=1000&q=80 https://www.notion.so/image/https%3A%2F%2Fs3.us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fc3e29f1d-c649-47b6-9020-872ada0350f6%2F95c89f42493a86b12451cf9e50667b49.jpeg%3FX-Amz-Algorithm%3DAWS4-HMAC-SHA256%26X-Amz-Content-Sha256%3DUNSIGNED-PAYLOAD%26X-Amz-Credential%3DAKIAT73L2G45EIPT3X45%252F20220117%252Fus-west-2%252Fs3%252Faws4_request%26X-Amz-Date%3D20220117T112111Z%26X-Amz-Expires%3D86400%26X-Amz-Signature%3D148ec5f6363907cb07a981a766261187e8293b846c433ab06b542c5cbf76ae2c%26X-Amz-SignedHeaders%3Dhost%26x-id%3DGetObject?table=block&id=f7edc5d7-379b-4d8b-9647-f9ea3bbe3cbd&cache=v2 https://www.notion.so/image/https%3A%2F%2Fwww.visitbrisbane.com.au%2F~%2Fmedia%2Farticles%2F2016%2Foctober-2016%2Fjacaranda%2Funiversityofqldjacaranda_imlee_20161022_web.ashx?table=block&id=b76e221a-ddcd-4d52-816a-8b154724e6a3&cache=v2
I have been using notion for note taking exclusively and with the beta release of its official API, I decided to try it out as a CMS.
 
The official API provides an easy way to filter pages for e.g. only show published post. However, the complexity in handling nested nature of Notion blocks and rendering it makes me find another library to handle that. react-notion-x renders the page given an id, but since it not use the official API as the library predates it, we need to make our pages public.
The JavaScript framework used is NextJs with typescript to have a server to hold the token. Recently, Notion allows for read only tokens, so it is safer to store client side, but it can still be spammed maliciously to exceed the rate limit.
Another benefit of NextJs is that blog post pages can be statically generated for fast load time and be eventually up-to-date without touching the server. This is done by specifying revalidate in getStaticProps, read about their static generation here and revalidation here.

getting list of published posts

  1. first setup the database by using this template I prepared, so all properties are matching with the code snippets
  1. get Notion token by following steps 1 and 2 in the Notion API getting started guide
  1. install @notionhq/client , prepare the functions to use for fetching list of pages in e.g. lib/notion.ts
const officialNotionClient = new Client({ auth: process.env.NOTION_TOKEN, }); const queryDatabase = async ( args: QueryDatabaseParameters ): Promise<QueryDatabaseResponse> => { return officialNotionClient.databases.query(args); }; const parseBlogPostProp = (pageProp: any): NotionBlogPost => { return { id: pageProp.id as string, title: pageProp.properties.Name.title as NotionRichText[], tags: pageProp.properties.Tags["multi_select"] as NotionTag[], lastEditedDateTime: pageProp.properties["Last edited time"][ "last_edited_time" ] as string, createdDateTime: pageProp.properties["Created time"][ "created_time" ] as string, thumbnailUrl: pageProp.properties.Thumbnail.files?.[0]?.file?.url || (null as string | null), published: pageProp.properties.Published.checkbox as boolean, }; };
  1. create the page in pages/index.tsx. Using .then() in getStaticProps somehow breaks InferGetStaticPropsType , so I did it with await
export default function BlogIndex({ posts, }: InferGetStaticPropsType<typeof getStaticProps>) { return (<></>); export const getStaticProps = async () => { const pages = ( await queryDatabase({ database_id: process.env.BLOG_DATABASE_ID!, ...(process.env.NODE_ENV !== "development" && { filter: { property: "Published", checkbox: { equals: true, }, }, }), }) ).results; const posts = pages.map(parseBlogPostProp); return { props: { posts, }, revalidate: revalidateDurationInSec, }; };
The original JSON response from the official API is like below, but parseBlogPostProp helps flatten it.
[{ object: 'page', id: '687b5560-294a-417d-b6fd-8b2b63872359', created_time: '2021-10-18T12:32:00.000Z', last_edited_time: '2022-01-03T11:24:00.000Z', cover: null, icon: null, parent: { type: 'database_id', database_id: '51774570-9da2-43a4-839f-db02e4e5c0b7' }, archived: false, properties: { Thumbnail: { id: '%3ADzk', type: 'files', files: [ { name: 'Capture.PNG', type: 'file', file: { url: 'https://s3.us-west-2.amazonaws.com/secure.notion-static.com/40f38f7f-77e9-4282-86fb-8d77ceef4680/Capture.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20220110%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20220110T111340Z&X-Amz-Expires=3600&X-Amz-Signature=e66833bc641d34f4643fb136f556e9965314d7905da8eabce30c23f8372a5c11&X-Amz-SignedHeaders=host&x-id=GetObject', expiry_time: '2022-01-10T12:13:40.024Z' } } ] }, 'Last edited time': { id: 'MX%40o', type: 'last_edited_time', last_edited_time: '2022-01-03T11:24:00.000Z' }, Tags: { id: 'WEsL', type: 'multi_select', multi_select: [ { id: 'ce2c40ef-5fc5-4b08-8680-a2ae3d39fe49', name: 'for-dev-purposes', color: 'default' } ] }, 'Created time': { id: 'o%3F%5BW', type: 'created_time', created_time: '2021-10-18T12:32:00.000Z' }, Published: { id: 'zH_%5E', type: 'checkbox', checkbox: true }, Name: { id: 'title', type: 'title', title: [ { type: 'text', text: { content: 'test notion blocks', link: null }, annotations: { bold: false, italic: false, strikethrough: false, underline: false, code: false, color: 'default' }, plain_text: 'test notion blocks', href: null } ] } }, url: 'https://www.notion.so/test-notion-blocks-687b5560294a417db6fd8b2b63872359' }]

rendering a page

Official API response for retrieve block children for a simple text and nested bullet point
{ "object": "list", "results": [ { "object": "block", "id": "161dda0f-e383-4ec5-aa2e-366c4d7ee007", "created_time": "2022-01-10T11:19:00.000Z", "last_edited_time": "2022-01-10T11:20:00.000Z", "has_children": false, "archived": false, "type": "paragraph", "paragraph": { "text": [ { "type": "text", "text": { "content": "Table of contents", "link": null }, "annotations": { "bold": false, "italic": false, "strikethrough": false, "underline": false, "code": false, "color": "default" }, "plain_text": "Table of contents", "href": null } ] } }, { "object": "block", "id": "f3e0a025-a8e0-4896-ba09-c80e9c04e36d", "created_time": "2021-10-19T05:49:00.000Z", "last_edited_time": "2022-01-10T11:20:00.000Z", "has_children": true, "archived": false, "type": "bulleted_list_item", "bulleted_list_item": { "text": [ { "type": "text", "text": { "content": "Write blogpost in Notion", "link": null }, "annotations": { "bold": false, "italic": false, "strikethrough": false, "underline": false, "code": false, "color": "default" }, "plain_text": "Write blogpost in Notion", "href": null } ] } } ], "next_cursor": "f932445c-50f3-45cd-9477-a561ea26fa30", "has_more": true }

problems throughout development

  • typing issues?
  • change css of react notion x
  • expiring image thumbnail as page property
    • available in v4.13.1
    • recordMap of photos contain block.value.source for url which consists of https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ followed by value.file_ids[0] and block.value.properties.title
    • for the dimensions, can be inferred from value.format.block_width and block_aspect_ratio which are always present regardless of external/self hosted images. block_page_width true means pic should be 100% width
  • deployment with docker and runtime env var for supplying token, not possible without sacrificnig feature in nextjs
 
 
  • have been using notion for note taking exclusively, due to being pleasant UI, nice features and unlimited devices
  • previously used evernote for notetaking and contentful CMS for blog
 
  • then found which renders pages like in the Notion app, however its using unofficial API, so your database needs to be shared to the public
  • there is also limited functionality for listing pages, filtering based on properties, so I used the official notion API for that
 
  • official API used for get list of pages, their properties and doing some filtering
 
  • still in browser, to get database id, select “posts” view under “personal-blog template”, the url will be in the form of notion.so/<hash1>?v=<hash2>, copy hash1 to get the database id used for the unofficial API and page renderer
  • share the database to the public by going to share > enable share to web