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
- first setup the database by using this template I prepared, so all properties are matching with the code snippets
- get Notion token by following steps 1 and 2 in the Notion API getting started guide
- 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, }; };
- create the page in
pages/index.tsx
. Using.then()
ingetStaticProps
somehow breaksInferGetStaticPropsType
, so I did it withawait
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]
andblock.value.properties.title
- for the dimensions, can be inferred from
value.format.block_width
andblock_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
- with release of Notion official API https://developers.notion.com/changelog/hello-world-notion-api-is-now-in-public-beta, why not try using it
- previously used evernote for notetaking and contentful CMS for blog
- came across this blog post https://samuelkraft.com/blog/building-a-notion-blog-with-public-api, using official API but have to render blocks ourselves
- 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
- follow step 1 and step 2 of Getting started https://developers.notion.com/docs/getting-started#getting-started to get token for the official API, I only enable read content capability and select no user information
- go to browser https://nicolauscg.notion.site/personal-blog-template-96f39e79b6944d478a495b57931970a3, click duplicate on top right
- 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
- tried to setup sample repo with CRA, but v5 contains breaking change, webpack does not polyfill, which breaks needed packages
- deployment with docker needs runtime env var to supply notion token, however nextjs will lose automatic static optimization. An alternative is using build time env var and use private image in dockerhub or a private docker registry
- tried finding occurrence of token in docker container, but couldn’t find it
- https://nextjs.org/docs/basic-features/environment-variables
- https://nextjs.org/docs/api-reference/next.config.js/runtime-configuration
- https://www.saltycrane.com/blog/2021/04/buildtime-vs-runtime-environment-variables-nextjs-docker/