Skip to content

Notion

Import and export Notion API block objects as RelationalText documents.

Package: relational-text/notionNamespace: com.notion.facet

Functions

ts
import { from, to } from 'relational-text/registry'

from('notion', input: NotionBlock[] | string): Document

Parse a Notion API blocks array into a Document. Accepts either a parsed array or a JSON string.

ts
const blocks = [
  {
    type: 'heading_1',
    heading_1: {
      rich_text: [
        { type: 'text', text: { content: 'Hello' }, annotations: { bold: false, italic: false, strikethrough: false, underline: false, code: false } },
      ],
    },
  },
  {
    type: 'paragraph',
    paragraph: {
      rich_text: [
        { type: 'text', text: { content: 'World' }, annotations: { bold: true, italic: false, strikethrough: false, underline: false, code: false } },
      ],
    },
  },
]
const doc = from('notion', blocks)

Supported block types:

  • paragraph — plain paragraph
  • callout — mapped to paragraph (callout styling is dropped)
  • heading_1, heading_2, heading_3 — headings
  • bulleted_list_item — unordered list item
  • numbered_list_item — ordered list item
  • quote — blockquote
  • code — fenced code block with optional language
  • divider — horizontal rule

Supported inline marks (from annotations object on each rich text element):

  • bold, italic, underline, strikethrough, code
  • Links from text.link.url or href

Non-text rich text elements (mentions, equations) are skipped. Unknown block types are silently ignored.

to('notion', doc: Document): NotionBlock[]

Render a Document to an array of Notion API block objects.

ts
import { from, to } from 'relational-text/registry'

const doc = from('markdown', '## Hello\n\nA **bold** word.')
const blocks = to('notion', doc)
// [
//   { type: 'heading_2', heading_2: { rich_text: [...] } },
//   { type: 'paragraph', paragraph: { rich_text: [...] } },
// ]

Automatically applies any registered lenses targeting com.notion.facet via lensGraph.autoTransform(). Documents from CommonMark, HTML, and other formats convert automatically when their lexicons are registered.

Rendered block types:

  • paragraph, heading_1, heading_2, heading_3
  • bulleted_list_item, numbered_list_item
  • quote (from blockquote containers)
  • code (with language attr preserved)
  • divider (from horizontal-rule)

Unknown block names fall back to paragraph.

ensureNotionLexicon(): void

Register the com.notion.facet lexicon and the Notion-to-RT lens with autoApply: true. Called automatically by from('notion', ...) and to('notion', ...) on first use. Safe to call multiple times — subsequent calls are no-ops.

Feature Mapping

Inline Marks

Notion annotationsFeature nameNamespaceExpand
bold: trueboldcom.notion.facetboth sides
italic: trueitaliccom.notion.facetboth sides
underline: trueunderlinecom.notion.facetboth sides
strikethrough: truestrikethroughcom.notion.facetboth sides
code: truecodecom.notion.facetneither side
text.link.url / hreflink (attr: url)com.notion.facetentity

Block Elements

Notion block typeFeature name(s)attrs
paragraphparagraph
calloutparagraph
heading_1heading_1
heading_2heading_2
heading_3heading_3
bulleted_list_itembullet-list-marker, list-item-marker, list-item-text
numbered_list_itemordered-list-marker, list-item-marker, list-item-text
quoteblockquote-marker, paragraph (parents: ['blockquote'])
codecode{ language? }
dividerdivider

Lens: Notion → RT Hub

The lens com.notion.to.relationaltext.v1 is invertible: false because heading_1/heading_2/heading_3 collapse into a single heading feature (with a numeric level attr), and dividerhorizontal-rule is a name change with no unique inverse.

Notion featureRT hub featurenotes
boldboldidentity
italicitalicidentity
underlineunderlineidentity
strikethroughstrikethroughidentity
codecodeidentity
linklinkidentity
paragraphparagraphidentity
heading_1headingaddAttrs: { level: 1 }
heading_2headingaddAttrs: { level: 2 }
heading_3headingaddAttrs: { level: 3 }
code (block)code-blockfeatureClass: block
list-item-textlist-item-textidentity
dividerhorizontal-rulename change
blockquote-markerblockquote-markeridentity
bullet-list-markerbullet-list-markeridentity
ordered-list-markerordered-list-markeridentity
list-item-markerlist-item-markeridentity

Examples

Import from Notion API

ts
import { from } from 'relational-text/registry'

const blocks = [
  {
    type: 'bulleted_list_item',
    bulleted_list_item: {
      rich_text: [
        { type: 'text', text: { content: 'Item with ' }, annotations: { bold: false, italic: false, strikethrough: false, underline: false, code: false } },
        { type: 'text', text: { content: 'bold' }, annotations: { bold: true, italic: false, strikethrough: false, underline: false, code: false } },
      ],
    },
  },
]

const doc = from('notion', blocks)

Export to Notion API

ts
import { from, to } from 'relational-text/registry'

const doc = from('markdown', '## Hello\n\n- item one\n- item two\n\n```js\nconsole.log("hi")\n```')
const blocks = to('notion', doc)
// [
//   { type: 'heading_2', heading_2: { rich_text: [...] } },
//   { type: 'bulleted_list_item', bulleted_list_item: { rich_text: [...] } },
//   { type: 'bulleted_list_item', bulleted_list_item: { rich_text: [...] } },
//   { type: 'code', code: { rich_text: [...], language: 'js' } },
// ]

Accept JSON string directly

ts
import { from } from 'relational-text/registry'

const json = JSON.stringify([
  { type: 'paragraph', paragraph: { rich_text: [{ type: 'text', text: { content: 'Hello' }, annotations: { bold: false, italic: false, strikethrough: false, underline: false, code: false } }] } },
])

const doc = from('notion', json) // same as passing a parsed array

Notes

  • List boundaries: The importer tracks the type of the previous list item to emit a single bullet-list-marker or ordered-list-marker per consecutive run of like list items, not one per item. This matches the Kleppmann block model where the list container marker separates runs.
  • Heading granularity: Notion exposes three heading levels (heading_1heading_3). These are preserved verbatim as com.notion.facet features, then collapsed into heading with a numeric level attr by the RT lens.
  • Code blocks: The language attr is taken from code.language on import and written back on export. When exporting and no language is present, Notion's conventional "plain text" string is used as the default.
  • Non-text rich text: Notion rich_text arrays can contain mention or equation elements. Only type: "text" elements contribute text and marks; others are silently skipped.
  • color annotation: Notion's color field is present in API responses but is not captured as a feature. The exporter always writes color: "default" in the annotations object.
  • callout blocks: Mapped to paragraph on import. The icon and background color of a callout have no RT equivalent and are not preserved.