Release v0.17.0 - Data Safety & Directory Layout
This release fixes data-loss bugs on push, makes every command use the same folder layout, and makes content survive a pull → push round trip. Upgrading is strongly recommended, especially if you push pages that have sub-pages or databases.
⚠️ Data-loss fixes
- Pushing a page no longer trashes its sub-pages and databases. Push used to delete every block on the page, including the blocks that hold sub-pages and databases, which moves them to the Notion trash. Sub-pages, databases, Notion-hosted images, blocks Markdown can't express (columns, synced blocks, embeds, files…) and anything containing them are now left alone.
- A failed push leaves the page as it was. New content is uploaded first and the old content is deleted only once everything succeeded. If anything fails, the blocks added so far are removed again.
- Pages with more than 100 blocks are pulled completely and replaced completely on push (pagination was missing; the rest of the page used to be dropped on pull and duplicated on push).
- Tables and long text push correctly. Tables were sent in a form Notion rejects, and text over 2000 characters wasn't split. Both left the page empty, because the old content had already been deleted.
- A failed push of a new file no longer leaves an empty page linked to the file.
- Pull no longer drops your frontmatter. Tags, status, custom fields and
sync_enabled: falseare kept. - Bidirectional sync no longer overwrites Notion edits of files you didn't touch, and never reverts a page renamed in Notion.
Blocks keep their IDs on push
- Push changes only what you changed. It compares the page in Notion with your Markdown block by block: unchanged blocks are left alone, edited paragraphs, headings, list items, to-dos, quotes, callouts, toggles, code and equations are updated in place, and only blocks you added or removed are created or deleted. Blocks keep their IDs, so comments, links to blocks and edit history survive a push. Pushing an unchanged page sends nothing.
- New text above a sub-page or database lands above it (it used to be added at the end of the page).
- If a push fails half way, added blocks are removed and updated blocks get their old text back.
- Edits inside kept blocks are reported. Text between
<!-- notion:... begin -->and<!-- notion:end ... -->markers is not pushed (Notion keeps those blocks as they are); push now warns when you edited it instead of silently ignoring it.
Notion API 2026-03-11
- The client now uses Notion API version
2026-03-11(it was on2022-06-28). Databases are read, queried and written through their data source. Databases with more than one data source, which failed on the old version, now sync their first data source and print a warning. - Requests are also retried on Notion's 529 "overloaded" response, and very long
Retry-Afterwaits are capped at 5 minutes.
📁 One folder layout everywhere
pull, push, sync and watch now all use the layout pull creates: each page in a folder with its file inside, sub-pages in sub-folders.
- Sub-pages are no longer appended to their parent's file (#7). The parent's file links to them instead:
[📄 Meeting Notes](<./Meeting Notes/Meeting Notes.md>) <!-- notion:child_page 1e369015-... -->
- New files are created under the page of their folder, not always under the root page. A folder without a page gets one, saved as
Folder/Folder.md. - Bidirectional sync no longer creates flat duplicates of every page on each run, and only pushes files that actually changed.
- Files you moved or renamed stay where they are; pull finds them by
notion_id. - Sibling pages with the same title get separate folders instead of overwriting each other.
- The first pull creates
markdown_rootinstead of failing, andpull --directoryactually pulls into that directory. pull --pagefinds nested pages, not only direct children of the root page.watchsees files in sub-folders, editors that save by renaming, and new folders. It no longer pushes twice after creating a page.watch --intervalnow works.- Exclude patterns work: they are matched relative to
markdown_root,**is supported andnode_modules/**/.git/**match at any depth. Hidden folders are skipped. - The separate "streaming" pull for large workspaces, which flattened folders and wrote files without frontmatter, was removed; all workspaces use the regular pull.
✍️ Content fidelity
- Bold, italic, strikethrough, underline, inline code and links are kept in both directions (they used to be flattened to plain text).
- Nested lists, content inside list items, quotes and toggles are real nested blocks instead of flattened text with leading spaces.
- [ ]/- [x]become Notion to-dos and back.- Quotes stay quotes;
> 💡 textbecomes a callout. $$inside code blocks is no longer turned into an equation.- Blank lines and trailing newlines inside code blocks are kept.
- Database CSV columns have a stable order (title first).
- Files with Windows line endings have their frontmatter read correctly.
- Pushing an unchanged file sends nothing to Notion, and title changes in frontmatter are pushed.
🔑 Setup and CLI
ntn_tokens are accepted (#6). This was fixed onmainafter v0.16.0 but never released.initaccepts a pasted page URL and stops instead of looping forever when its input ends.sync --direction bidirectionalruns real bidirectional sync (it used to just push every file), andsyncexits with an error when files fail.- New
--timeoutflag (default 30 minutes, was a fixed 5) and clean Ctrl+C. - Paths like
--directory ../notesand file names containing..are accepted. - Requests are retried on rate limits (429) and temporary server errors, honouring
Retry-After. - Notion error messages are shown properly.
- New
NOTION_MD_SYNC_API_URLenvironment variable to point the tool at another API address (used by the end-to-end tests). statusis clean right after a pull: files written bypullandsyncare recorded as synced.- The README now covers the essentials only; the full documentation moved to the wiki.
🧹 Reliability
- Fixed data races and possible crashes in
watch,status/addand the multi-client mode. - The staging index is written atomically and saved once per command; paths like
./docs/a.mdanddocs/a.mdare the same entry; files are compared by content; a pushed file no longer shows as modified; old untracked files are no longer hidden fromstatus. - TUI:
sandPwork,ppulls only the selected files, and failures are shown as failures.- Typing
qin the token field no longer quits. - Output no longer corrupts the screen.
- Saving the configuration takes effect without a restart.
pkg/cacheandpkg/concurrent: fixed deadlocks, a panic on reuse, a crash under concurrent reads and stale cache entries. These packages are not used by the CLI.
🧪 Testing
- New end-to-end test (
cmd/notion-md-sync/e2e_test.go) builds the binary and runspull,push, staging,sync,pull --page,watchandinitagainst an in-memory Notion API (internal/notionfake) that enforces the real API's limits. - CI now checks
gofmtandgo vetand runs the tests with the race detector.
Upgrading
- Pulled files now contain marker comments (
<!-- notion:... -->) for sub-pages, databases and blocks Markdown can't express. Leave them in place: they tell push where those blocks are. You can move them to reorder your text around them. - Files pulled with v0.16 or earlier contain copies of their sub-pages' content. Run
notion-md-sync pullonce to rewrite them with links before pushing parent pages. - Frontmatter gains a
synced_atfield, used to tell which side changed since the last sync.