Convert HTML To Notion Blocks
Tags: javascript, tutorial, html, node
Published: 2023-03-01 10:59:08

If you need to import HTML into Notion, you may have faced the challenge of converting HTML to Notion's block format. In my recent project, I stumbled upon this issue and was unable to find any existing package that provided a solution. That is why in this post I want to share the code and libraries that helped me make it work.

The first step is to parse the HTML code into a JavaScript object. To do this, we use the `fromHtml` function from the [`hast-util-from-html`](https://github.com/syntax-tree/hast-util-from-html) library, which takes an HTML string as input and returns a `hast` object:

```ts
import { fromHtml } from 'hast-util-from-html';

const html = '<h1>Hello <strong>world!</strong></h1>';
// HTML to HTML AST
const hast = fromHtml(html, { fragment: true });
```

The `hast` object represents the HTML code as an abstract syntax tree (AST) that can be manipulated and transformed into other formats:

```js
{
  type: 'root',
  children: [
    {
      type: 'element',
      tagName: 'h1',
      properties: {},
      children: [
        {
          type: 'text',
          value: 'Hello ',
          position: {
            start: { line: 1, column: 5, offset: 4 },
            end: { line: 1, column: 11, offset: 10 }
          }
        },
        {
          type: 'element',
          tagName: 'strong',
          properties: {},
          children: [
            {
              type: 'text',
              value: 'world!',
              position: {
                start: { line: 1, column: 19, offset: 18 },
                end: { line: 1, column: 25, offset: 24 }
              }
            }
          ],
          position: {
            start: { line: 1, column: 11, offset: 10 },
            end: { line: 1, column: 34, offset: 33 }
          }
        }
      ],
      position: {
        start: { line: 1, column: 1, offset: 0 },
        end: { line: 1, column: 39, offset: 38 }
      }
    }
  ],
  data: { quirksMode: false },
  position: {
    start: { line: 1, column: 1, offset: 0 },
    end: { line: 1, column: 39, offset: 38 }
  }
}
```

Once the HTML code is parsed into a `hast` object, the next step is to convert it to Markdown. We use the `toMdast` function from the [`hast-util-to-mdast`](https://github.com/syntax-tree/hast-util-to-mdast) library, which takes a `hast` object as input and returns a `mdast` object. 

```ts
import { fromHtml } from 'hast-util-from-html';
import { toMdast } from 'hast-util-to-mdast';

const html = '<h1>Hello <strong>world!</strong></h1>';
const hast = fromHtml(html, { fragment: true });
// HTML AST to Markdown AST
const mdast = toMdast(hast);
```

The `mdast` object represents the Markdown code as an AST, similar to the previous HTML AST:

```js
{
  type: 'root',
  children: [
    {
      type: 'heading',
      depth: 1,
      children: [
        {
          type: 'text',
          value: 'Hello ',
          position: {
            start: { line: 1, column: 5, offset: 4 },
            end: { line: 1, column: 11, offset: 10 }
          }
        },
        {
          type: 'strong',
          children: [
            {
              type: 'text',
              value: 'world!',
              position: {
                start: { line: 1, column: 19, offset: 18 },
                end: { line: 1, column: 25, offset: 24 }
              }
            }
          ],
          position: {
            start: { line: 1, column: 11, offset: 10 },
            end: { line: 1, column: 34, offset: 33 }
          }
        }
      ],
      position: {
        start: { line: 1, column: 1, offset: 0 },
        end: { line: 1, column: 39, offset: 38 }
      }
    }
  ],
  position: {
    start: { line: 1, column: 1, offset: 0 },
    end: { line: 1, column: 39, offset: 38 }
  }
}
```

The next step is to serialize Markdown AST into a Markdown string. We use the `toMarkdown` function from the [`mdast-util-to-markdown`](https://github.com/syntax-tree/mdast-util-to-markdown) library, which takes a `mdast` object as input and returns Markdown string:

```ts
import { fromHtml } from 'hast-util-from-html';
import { toMdast } from 'hast-util-to-mdast';
import { toMarkdown } from 'mdast-util-to-markdown';

const html = '<h1>Hello <strong>world!</strong></h1>';
const hast = fromHtml(html, { fragment: true });
const mdast = toMdast(hast);
// Markdown AST to Markdown string
const markdown = toMarkdown(mdast);
```

The Markdown string has the format we are used to from Editors like on Dev.to or GitHub:

```md
# Hello **world!**
```

The final step is to convert the Markdown string into Notion block objects that can be used to create new blocks using the Notion API. We use the `markdownToBlocks` function from the [`@tryfabric/martian`](https://github.com/tryfabric/martian) library, which takes the Markdown string as input and returns an array of `BlockObjectRequest` objects:

```ts
import { fromHtml } from 'hast-util-from-html';
import { toMdast } from 'hast-util-to-mdast';
import { toMarkdown } from 'mdast-util-to-markdown';
import { markdownToBlocks } from '@tryfabric/martian';

const html = '<h1>Hello <strong>world!</strong></h1>';
const hast = fromHtml(html, { fragment: true });
const mdast = toMdast(hast);
const markdown = toMarkdown(mdast);
// Markdown string to Notion Blocks
const blocks = markdownToBlocks(markdown);
```

Each `BlockObjectRequest` object represents a Notion block and contains the necessary information to create and format the block in your Notion page:

```js
[
  {
    object: 'block',
    type: 'heading_1',
    heading_1: {
      rich_text: [
        {
          type: 'text',
          annotations: {
            bold: false,
            strikethrough: false,
            underline: false,
            italic: false,
            code: false,
            color: 'default'
          },
          text: { content: 'Hello ', link: undefined }
        },
        {
          type: 'text',
          annotations: {
            bold: true,
            strikethrough: false,
            underline: false,
            italic: false,
            code: false,
            color: 'default'
          },
          text: { content: 'world!', link: undefined }
        }
      ]
    }
  }
]
```

Here is an example of how to create a new page in your Notion workspace with the blocks converted from the original HTML.  We use the [`@notionhq/client`](https://github.com/makenotion/notion-sdk-js) library and you'll need to have a valid Notion API key and database ID:

```ts
import { Client } from '@notionhq/client';

// ...
const blocks = markdownToBlocks(markdown);

const notion = new Client({ auth: process.env.NOTION_API_KEY });

const properties = {
  Name: {
    title: [{ text: { content: 'New Page' } }]
  }
};

const response = await notion.pages.create({
  parent: { database_id: process.env.NOTION_DATABASE_ID },
  properties,
  children: blocks,
});
```

The response from Notion contains the new page and its properties. However, it does not contain the blocks, since these must be queried separately.

```js
{
  object: 'page',
  id: 'c7d50cb4-0518-4ef9-8457-056a53fd53eb',
  created_time: '2023-03-01T10:28:00.000Z',
  last_edited_time: '2023-03-01T10:28:00.000Z',
  created_by: { object: 'user', id: '9d72c6df-be87-4bb5-8edb-1c19461d01bb' },
  last_edited_by: { object: 'user', id: '9d72c6df-be87-4bb5-8edb-1c19461d01bb' },
  cover: null,
  icon: null,
  parent: {
    type: 'database_id',
    database_id: '95970798-23cc-4f7c-a0c5-4870b80fcdee'
  },
  archived: false,
  properties: {
    Name: {
      id: 'title',
      type: 'title',
      title: [
        {
          type: 'text',
          text: { content: 'New Page', link: null },
          annotations: {
            bold: false,
            italic: false,
            strikethrough: false,
            underline: false,
            code: false,
            color: 'default'
          },
          plain_text: 'New Page',
          href: null
        }
      ]
    },
    // ...
  },
  url: 'https://www.notion.so/New-Page-c7d50cb405184ef98457056a53fd53eb'
}
```

---
I hope you found this post helpful. If you have any questions or comments, feel free to leave them below. If you'd like to connect with me, you can find me on [LinkedIn](https://www.linkedin.com/in/christian-zirkel/) or [GitHub](https://github.com/zirkelc). Thanks for reading!