Skip to content

feat(notion-api): restore Notion sidebar hierarchy in import tree#520

Open
oliviermattei wants to merge 1 commit intoobsidianmd:masterfrom
oliviermattei:master
Open

feat(notion-api): restore Notion sidebar hierarchy in import tree#520
oliviermattei wants to merge 1 commit intoobsidianmd:masterfrom
oliviermattei:master

Conversation

@oliviermattei
Copy link
Copy Markdown

@oliviermattei oliviermattei commented Mar 19, 2026

The Notion API importer now mirrors your Notion sidebar structure as nested folders in Obsidian.

Previously all imported pages and databases were dumped flat into the output folder regardless of how they were organised in Notion. This was caused by three bugs: the hierarchy option defaulted to off, databases that were children of a selected page were silently skipped, and any item nested inside a layout block (column, toggle, callout) had its parent chain broken by the API, landing it at vault root.

This fix enables hierarchy preservation by default, ensures databases are always imported explicitly, and resolves the block chain by making targeted API calls at load time to recover the missing parent links.

Problem

When importing from Notion via the API importer, the vault output was completely flat: every page and database ended up at the root of the output folder regardless of its position in the Notion sidebar. Users with a structured workspace would get hundreds of unrelated files dumped at the same level, making the result unusable without manual reorganisation.

There were three distinct root causes.


1. preserveHierarchy defaulted to false

The option to mirror the Notion sidebar structure existed but was opt-in and off by default. Most users would never discover it, and a flat output is rarely what anyone wants when their Notion workspace has meaningful structure.


2. Databases selected as children were silently skipped

When a user selects a parent page that contains a database, selectAllChildren() marks the database node as disabled. The import loop filtered out all disabled nodes — including databases.

Unlike sub-pages, databases are not imported as child_page blocks inside their parent page's content: the Notion API returns them as independent data_source objects. They must always be imported with an explicit call regardless of their disabled state, otherwise any database that is a child of a selected page is simply never imported.


3. Items nested inside layout blocks fell to vault root

This was the most impactful bug. Some Notion pages and databases live inside a layout block (column, toggle, callout…) rather than directly under a page. The Search API marks these items with parent.type = "block_id". Because block objects are not returned by Search, extractParentId() resolves block_id to null, silently breaking the ancestry chain. Every item whose parent chain passed through a block was orphaned at vault root.

Example — Notion sidebar structure:

Work
└─ Strategy
     └─ [two-column layout block]
          ├─ Roadmap (database)
          │    ├─ Q1 Goals
          │    ├─ Q2 Goals
          │    └─ ...
          └─ Competitors (database)
               ├─ Acme Corp
               └─ ...

Before this fix — vault output:

Notion/
├─ Work.md
├─ Strategy.md
├─ Q1 Goals.md        ← should be inside Roadmap/
├─ Q2 Goals.md        ← should be inside Roadmap/
└─ Acme Corp.md       ← should be inside Competitors/

After — vault output:

Notion/
└─ Work/
     ├─ Work.md
     └─ Strategy/
          ├─ Strategy.md
          ├─ Roadmap/
          │    ├─ Roadmap.base
          │    ├─ Q1 Goals.md
          │    └─ Q2 Goals.md
          └─ Competitors/
               ├─ Competitors.base
               └─ Acme Corp.md

Solution

preserveHierarchy defaults to true

The toggle is now enabled by default so the output mirrors the Notion sidebar out of the box. Users can disable it for a flat import if they prefer.

Databases always imported explicitly

getSelectedNodeIds() now includes disabled nodes of type database in the import list, regardless of whether their parent was selected.

Ancestry escalation via rawParentMap + block chain resolution

Two additions to loadPageTree():

rawParentMap — a map built from all raw Search results (including filtered items) that records every item's direct parent ID. This is passed to buildTree().

Ancestor escalation in buildTree() — when a node's direct parentId is not present in the visible tree, resolveVisibleParent() walks up rawParentMap until it finds the nearest visible ancestor (cycle-safe). This handles items whose parent is inaccessible or was filtered for another reason.

Block chain resolution — for filtered items whose parent is a block_id, the importer makes targeted blocks.retrieve() calls at load time, walking up the block chain until it reaches a page_id. This page ID is written back into rawParentMap, giving the escalation logic the missing link it needs to attach those items to the correct workspace page.

The number of extra API calls equals the number of unique block_id values across all filtered items — typically a handful per workspace.


Changes

File What changed
src/formats/notion-api.ts All fixes described above

No changes to other importers, helper modules, or test data.


Testing

Validated against a real Notion workspace with ~2 200 items including pages nested 4–5 levels deep, databases inside columns and toggles, and databases marked as children of selected parent pages.

Before After
Root nodes in import tree 104 6
Matches Notion sidebar
Databases inside layout blocks orphaned at root correctly nested

- Default preserveHierarchy to true so folders mirror the Notion sidebar
- Ensure intermediate ancestor folders are created before importing each item
- Always import databases explicitly even when disabled (they are not
  cascaded as child_page blocks by their parent page)
- Build rawParentMap from all raw Search items (including filtered) so
  buildTree can escalate past missing/filtered parents to the nearest
  visible ancestor instead of dropping to root
- Resolve block_id parent chains via targeted API calls so items nested
  inside columns/toggles are correctly attached to their workspace page
  (fixes pages like Antibes, Architecture appearing at vault root)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant