github byvfx/go-notion-md-sync v0.17.0

3 hours ago

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: false are 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 on 2022-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-After waits 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_root instead of failing, and pull --directory actually pulls into that directory.
  • pull --page finds nested pages, not only direct children of the root page.
  • watch sees files in sub-folders, editors that save by renaming, and new folders. It no longer pushes twice after creating a page. watch --interval now works.
  • Exclude patterns work: they are matched relative to markdown_root, ** is supported and node_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; > 💡 text becomes 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 on main after v0.16.0 but never released.
  • init accepts a pasted page URL and stops instead of looping forever when its input ends.
  • sync --direction bidirectional runs real bidirectional sync (it used to just push every file), and sync exits with an error when files fail.
  • New --timeout flag (default 30 minutes, was a fixed 5) and clean Ctrl+C.
  • Paths like --directory ../notes and 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_URL environment variable to point the tool at another API address (used by the end-to-end tests).
  • status is clean right after a pull: files written by pull and sync are 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/add and the multi-client mode.
  • The staging index is written atomically and saved once per command; paths like ./docs/a.md and docs/a.md are the same entry; files are compared by content; a pushed file no longer shows as modified; old untracked files are no longer hidden from status.
  • TUI:
    • s and P work, p pulls only the selected files, and failures are shown as failures.
    • Typing q in the token field no longer quits.
    • Output no longer corrupts the screen.
    • Saving the configuration takes effect without a restart.
  • pkg/cache and pkg/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 runs pull, push, staging, sync, pull --page, watch and init against an in-memory Notion API (internal/notionfake) that enforces the real API's limits.
  • CI now checks gofmt and go vet and 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 pull once to rewrite them with links before pushing parent pages.
  • Frontmatter gains a synced_at field, used to tell which side changed since the last sync.

Don't miss a new go-notion-md-sync release

NewReleases is sending notifications on new releases.