
When I post on my blog, I write the content in markdown and Gatsby.js do some magic and converts everything to HTML. I had some cooking recipes on my Notion workspace and I recently wanted to publish them online.
Since I wanted to keep Notion as my source of truth, I had to find a way to publish them differently.
By the way, I titled this post mentioning Gatsby.js. But, if you use a different static site generator (like Astro or Next.js).
Big picture
I have one JS script that does different things in 2 steps:
- Get all database pages (with the filter βreadyβ). And convert them to a markdown (or MDX) file.
- Gatsby makes HTML content out of the markdown.
Step #1: From Notion to Markdown
I wrote a NodeJS script that uses 2 libraries:
@notionhq/client
to query the pages of my database.notion-to-md
to convert the pages to markdown.
const fs = require('fs')var http = require('https')const { Client } = require('@notionhq/client')const { NotionToMarkdown } = require('notion-to-md')
const notion = new Client({ auth: 'your-secret-id' })const DB_ID = 'your-db-id'
const n2m = new NotionToMarkdown({ notionClient: notion })
// Get all pages in the databaseasync function getPages() { const pages = await notion.databases.query({ database_id: DB_ID, filter: { property: 'Ready', checkbox: { equals: true, }, }, }) return pages.results}
const getBlocks = async blockId => { const response = await notion.blocks.children.list({ block_id: blockId, page_size: 50, }) return response.results}
const download = (url, destination) => { const fileStream = fs.createWriteStream(destination) http.get(url, function (response) { response.pipe(fileStream) console.log('π¨βπ³ File downloaded', destination) })}
;(async () => { fs.rmdirSync('./content/recipes', { recursive: true }) fs.mkdirSync('./content/recipes') fs.mkdirSync('./content/recipes/assets') const pages = await getPages()
for (const page of pages) { const fileName = page.url.replace('https://www.notion.so/', '') const title = page.properties.Name.title.map(t => t.plain_text).join() const blocks = await getBlocks(page.id) let olCounter = 1
const cleanBlocks = blocks.map((block, i) => { if (block.type === 'image') { const imgFileName = `${fileName}-${i}.jpg` const destination = './content/recipes/assets/' + imgFileName download(block.image.file.url, destination) block.image.file.url = './assets/' + imgFileName } else if (block.type === 'numbered_list_item') { olCounter = blocks[i - 1]?.type === 'numbered_list_item' ? olCounter + 1 : 1 block.numbered_list_item.number = olCounter } return block })
const mdblocks = await n2m.blocksToMarkdown(cleanBlocks) const mdString = n2m.toMarkdownString(mdblocks).parent const coverFileName = `${fileName}-cover.jpg` page.cover && download(page.cover.file.url, './content/recipes/assets/' + coverFileName)
const header = `---title: ${title}date: ${page.created_time}slug: ${fileName}${page.cover ? `cover: ./assets/${coverFileName}` : ''}--- ` const fileContent = header + mdString
fs.writeFile(`./content/recipes/${fileName}.mdx`, fileContent, () => { console.log(`π¨βπ³ File downloaded: ${fileName}`) }) }})()
Heads-up!
- To run the script, run
node scripts/notion-to-markdown.js
on your terminal. - I had to handle the
numbered_list_item
manually (see theolCounter
variable). Thenotion-to-md
library currently has an issue with the ordered lists.
After running the script, my folder looks like the following:
content/βββ posts # all my blog postsβββ recipes βββ [recipe-1-slug].mdx βββ [recipe-2-slug].mdx βββ ... βββ assets βββ [recipe-1-slug]-cover.jpg βββ [recipe-1-slug]-0.jpg βββ [recipe-2-slug]-cover.jpg βββ [recipe-2-slug]-0.jpg βββ ...
Step #2 - Gatsby.js
Dealing with Markdown is a thing that Gatsby does well. If youβre unfamiliar with it, I recommend you look at their Getting Started guides.
After some CSS fine-tuning, I ended up with something like that π₯³
If you want to check it out, the project is available here. By the way, my websiteβs GitHub repository is public!
What could be improved?
Deployment
If I change a typo in Notion, to see the fix live on my website, I need to run the command and then git commit&push the change.
Limited block support
At the moment I only support basic notion items. Things such as linking pages together, videos or uploaded files are not supported.
Also, I didnβt find a way to write MDX (Markdown+JSX)
Import everything or nothing
My notion-to-markdown.js
script gathers all the posts and all the images at once. Itβs ok for now
because I donβt have thousands of items in Notion. Otherwise, Iβd probably have to add a new filter
in the notion.databases.query()
function!
About the author

Hey, I'm Maxence Poutord, a passionate software engineer. In my day-to-day job, I'm working as a senior front-end engineer at Orderfox. When I'm not working, you can find me travelling the world or cooking.
Follow me on Bluesky