Skip to content

gerfru/notion-sync

Repository files navigation

notion-sync

Migrates Notion pages/databases (including images and PDFs, which Notion's own export skips) into a self-hosted Docmost wiki.

Two steps:

  1. Exportnotion-downloader pulls pages via the Notion API into Markdown + downloaded assets on disk, one folder per configured root page/database.
  2. Importsrc/import-to-docmost.ts walks that folder tree, creates matching Docmost pages via the API, uploads assets per page, rewrites internal links, renders Notion database properties (frontmatter) into the page body, and records a Notion→Docmost id map (sync-map.json) so re-runs update existing pages instead of duplicating them.

Setup

  1. npm install
  2. cp .env.example .env and fill in:
    • NOTION_TOKEN — from an internal integration at notion.so/my-integrations
    • NOTION_ROOT_PAGE_IDS / NOTION_ROOT_DB_IDS — 32-char id(s) of the page(s)/database(s) to export (comma-separated, no quotes/spaces). Share each root with the integration first — Notion only exposes pages explicitly shared with it. Root type is auto-detected, so it doesn't matter which of the two variables you use.
    • DOCMOST_BASE_URL, DOCMOST_EMAIL, DOCMOST_PASSWORD
    • DOCMOST_CA_CERT_PATH — only needed if your Docmost instance uses a self-signed/internal CA certificate (common for home-lab setups behind Caddy/Traefik). Point it at the CA root cert file.
    • DOCMOST_SPACE_IDthe space's UUID, not its slug. Run npm run list-spaces after filling in the Docmost credentials above to get it.

Usage

npm run export   # -> ./export/<rootId>/content (markdown + assets), one folder per root
npm run import   # -> creates/updates pages in Docmost, writes sync-map.json

Run against a small test subtree first (one page, one child, one image, one PDF) and check the result in Docmost before pointing at the full workspace — set NOTION_ROOT_PAGE_IDS/NOTION_ROOT_DB_IDS to just that one id for the first run.

Notion databases

Docmost (community edition) has no database/table concept. Each Notion database becomes a parent page in Docmost, and every database row becomes a regular child page under it.

Oversized pages

Docmost's page-update endpoint has a hard ~1MB request body limit that isn't configurable via env var (Fastify default, not exposed in Docmost's server setup). Pages whose content exceeds that get automatically split into <title> (Teil 2), (Teil 3), ... child pages — see MAX_CHUNK_BYTES in src/import-to-docmost.ts.

Advanced: rebuilding toggle-heading structure

Notion's collapsible "toggle heading" blocks are not separate pages — they're just inline content within one page. notion-downloader flattens them into one giant Markdown file, which can produce a single enormous page that loses the collapsible sections as real structure. src/advanced/ contains scripts to reconstruct that structure as real nested Docmost pages by reading the page's actual block tree from the Notion API directly (bypassing the already-flattened export):

  • split-toggle-page.ts <notionPageId> <docmostParentPageId> — recursively turns every toggle heading into its own Docmost sub-page.
  • reparent-children.ts <notionPageId> <docmostParentPageId> — run afterwards. Notion sub-pages (child_page blocks) that notion-downloader already exported flat under the root get moved to nest under their real toggle-heading parent, matching Notion's true structure.
  • inspect-toggles.ts [notionPageId] — read-only recon: prints the toggle- heading tree and block-type counts before you commit to splitting anything.

This is a one-off, page-specific operation — inspect first, since not every page needs it (only ones that visibly used toggle headings for organization).

Debug utilities

src/debug/ has small scripts for inspecting/fixing Docmost state directly, useful when a migration run partially fails:

  • check-pages.ts — list top-level pages in the configured space
  • check-content.ts <pageId> — dump a page's full metadata + content
  • print-tree.ts [pageId] — print a page's (or the whole space's) subtree
  • delete-pages.ts <pageId> [pageId...] — permanently delete pages (careful — cascades to children)
  • restore-page.ts <pageId> — restore a page from Docmost's trash

Re-running / incremental sync

sync-map.json (gitignored) tracks which Notion-exported file maps to which Docmost page id, plus whether its content has been pushed (contentSynced). Re-running npm run import after a fresh npm run export updates existing pages' content instead of creating duplicates, and skips already-synced pages entirely (so it won't re-upload the same attachments on every run). Delete sync-map.json to force a full re-import.

Notes from building this

A few real bugs were found and worked around along the way, in case you hit them too:

  • notion-downloader's pull CLI flags (--root-page, --root-is-db) don't map to the schema field the tool actually reads (rootId) — writing a downloader.json config file directly (as export.ts does) is the reliable path.
  • notion-downloader resolves its ./content output path against the process's actual OS working directory, not its --cwd flag — export.ts works around this by setting the child process's cwd directly.
  • Its asset cache can go stale relative to the content output tree (seen after fixing the bug above), silently skipping re-copying already-cached images/files into the new output path. export.ts does a clean pull (wipes the per-root folder first) every run to avoid this.
  • Asset/internal-link paths in the exported Markdown are sometimes absolute (rooted at the export folder, e.g. /Page/img.png), not relative to the referencing file — resolving them naively turns the leading / into the OS filesystem root.
  • Docmost wraps every API response as { data, success, status }.

About

Migrates Notion pages/databases (incl. images, PDFs) into a self-hosted Docmost wiki - fixes real gaps in notion-downloader's export.

Topics

Resources

License

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors