DEV Community

appierun
appierun

Posted on

How to Convert Notion to Markdown (Export, Public Pages and the API), With a Cleanup Script

You wrote the docs in Notion. Now you want them as .md files: for a docs site, for a Git repo, or for an LLM pipeline that reads Markdown far more happily than it reads a Notion URL. There are three practical ways to get there, and each one fits a different situation:

  1. Notion's built-in "Markdown & CSV" export, for private pages and whole page trees.
  2. A public-page converter in the browser, for a page that's already published.
  3. The Notion API with notion-to-md, for repeatable, scripted conversion.

The built-in export is the most common route, and it leaves a few rough edges, so this post also includes a small Python script that tidies the export folder.

Disclosure: Appie.run publishes this post, and the public-page converter in Method 2 is our free tool. Methods 1 and 3 don't need anything from us.

Notion behaviour below is from Notion's Help Center and developer docs as read on 25 Oct 2026. Notion changes its menus from time to time, so check the linked pages if a label doesn't match.

Which method should you use?

  • Built-in export: Needs an account or token: Notion account with access to the page; Private pages: Yes; Subpages and databases: Subpages as separate files; databases as CSV plus a .md per row; Best for: One-off exports, backups, migrating a wiki
  • Public-page converter: Needs an account or token: The page must be published to the web; Private pages: No; Subpages and databases: The published page's text; Best for: Quick copy of a page that's already public
  • API + notion-to-md: Needs an account or token: Notion API token; Private pages: Yes, if the token can see the page; Subpages and databases: Child pages returned as separate strings; Best for: Docs pipelines, scheduled syncs, CI

If you only need to do this once, start with Method 1. If you'll do it every week, go straight to Method 3.

Method 1: Notion's built-in Markdown & CSV export

According to Notion's export guide, any non-database page can be exported as a Markdown file. Full-page databases export as a CSV file, with a Markdown file for each subpage.

On desktop:

  1. Open the page and click the ••• menu at the top right.
  2. Click Export.
  3. Choose Markdown & CSV from the format dropdown.
  4. Switch on Include subpages if you want the pages nested under this one.
  5. Click Export. You get a zip file with the Markdown and CSV files.

Mobile works the same way: •••, then Export, then pick Markdown & CSV under Export format. Notion also emails you a download link. For very large exports, Notion says it may email a link rather than start the download straight away.

Whole workspace. Notion's guide lists Settings > Workspace > General > Export all workspace content. The download link arrives by email (or in your Notion inbox if email notifications are off), expires after 7 days, and Notion says the export can take up to 30 hours for large workspaces. Pages the exporter can't access, such as other people's private pages, are left out. There's also a beta admin API for starting workspace exports on the Enterprise Plan.

No Export option? On Enterprise workspaces, an owner can disable exporting. Guests need Full access to a page to export it.

Clean up the export

Unzip the export and open a file. The content is fine, but a few things get in the way of using it in a repo or docs site:

  • File and folder names often carry a long ID after the page title, for example Team Wiki 1a2b3c4d5e6f708192a3b4c5d6e7f809.md. Links between pages point at those names, URL-encoded.
  • Callouts come out as HTML. Notion's guide says callout blocks are exported as HTML because Markdown has no equivalent. Static site generators and Markdown linters may not render that the way you want.

Here's a script that strips the ID suffix from file and folder names, rewrites relative links to match, and turns callouts into blockquotes. It only uses the Python standard library.

"""Tidy a Notion "Markdown & CSV" export: strip ID suffixes, fix links, convert callouts."""
import re
import sys
import pathlib
import urllib.parse

ID = re.compile(r" [0-9a-f]{32}")  # " <32 hex chars>" after the page title
root = pathlib.Path(sys.argv[1])

# 1. Rename files and folders, deepest paths first so parents rename last.
for p in sorted(root.rglob("*"), key=lambda p: -len(p.parts)):
    new = p.with_name(ID.sub("", p.name))
    if new == p:
        continue
    if new.exists():
        print(f"skip (name taken): {p}")
        continue
    p.rename(new)

def fix_link(m):
    target = m.group(1)
    if "://" in target or target.startswith(("#", "mailto:")):
        return m.group(0)  # leave external links and anchors alone
    path, _, anchor = target.partition("#")
    path = urllib.parse.quote(ID.sub("", urllib.parse.unquote(path)))
    return "](" + path + ("#" + anchor if anchor else "") + ")"

def fix_callout(m):
    lines = m.group(1).strip().splitlines()
    return "\n".join("> " + line if line.strip() else ">" for line in lines)

# 2. Rewrite relative links and turn <aside> callouts into blockquotes.
for md in root.rglob("*.md"):
    text = md.read_text(encoding="utf-8")
    text = re.sub(r"\]\(([^)\s]+)\)", fix_link, text)
    text = re.sub(r"<aside>(.*?)</aside>", fix_callout, text, flags=re.S)
    md.write_text(text, encoding="utf-8")
    print(f"cleaned: {md.relative_to(root)}")
Enter fullscreen mode Exit fullscreen mode

Run it on a copy of the unzipped folder, since it renames files in place:

cp -r "Export-folder" notion-md
python3 clean_notion_export.py notion-md
Enter fullscreen mode Exit fullscreen mode

We ran it with Python 3.13 on a small sample export we built by hand to mimic Notion's format, not a real Notion export: a parent page, a subpage, a database CSV and an image, all with ID suffixes, plus an external link and a two-line callout. Before:

Start with [Onboarding](Team%20Wiki%201a2b3c4d5e6f708192a3b4c5d6e7f809/Onboarding%200f1e2d3c4b5a69788796a5b4c3d2e1f0.md).

See our [site](https://example.com/docs?a=1&b=2#top).

<aside>
💡 Callout text
Second line of the callout.

</aside>
Enter fullscreen mode Exit fullscreen mode

After:

Start with [Onboarding](Team%20Wiki/Onboarding.md).

See our [site](https://example.com/docs?a=1&b=2#top).

> 💡 Callout text
> Second line of the callout.
Enter fullscreen mode Exit fullscreen mode

Every relative link in the sample still resolved to a real file after the rename, and the external URL was left untouched.

Two things to check on your own export before you trust the script:

  • The ID pattern. The script expects a space followed by 32 lowercase hex characters. Open your zip and compare. If your names look different, change the ID regex.
  • The callout HTML. The script assumes callouts are wrapped in <aside> tags. Open one exported callout in a text editor; if Notion used different markup, change the second re.sub pattern.

The script also skips a rename when the clean name is already taken (two sibling pages with the same title, for example), so nothing gets overwritten. Fix those by hand.

Method 2: Convert a public Notion page in the browser

If the page is already published, you don't need an export at all. Our free Convert Notion to Markdown tool takes a public notion.so or notion.site URL and converts the published text to Markdown.

  1. Paste the public URL.
  2. Complete the captcha (Cloudflare Turnstile, one check per run).
  3. Copy the Markdown or download it.

The limit is in the name: it reads what the public page exposes. A private Notion URL doesn't expose any text to read, so for private pages you'd either publish the page first or use Method 1 or 3. It also works on one page at a time, so for a tree of subpages the built-in export is the better fit.

Method 3: Use the Notion API with notion-to-md

For anything you'll repeat, such as syncing a docs space into a repo every night, use the API. notion-to-md is an open-source package that turns Notion blocks into Markdown. As of 25 Oct 2026, the latest version on npm is 3.1.9 (a 4.0 alpha is also published; the code below is for 3.x).

1. Get a token. Notion's developer docs describe two simple options:

  • A personal access token, created in the Developer portal, which acts as you and uses your permissions. Notion notes that on Business and Enterprise plans, creating one may need a workspace owner to enable it.
  • An internal connection token, for team-owned automations. Notion says the page must be shared with the connection first: open the page, click •••, choose Add connections, and pick your connection. Without that, API requests return an error.

Keep the token in an environment variable, not in your code.

2. Install the packages.

npm install notion-to-md @notionhq/client
Enter fullscreen mode Exit fullscreen mode

3. Convert a page. This follows the package README:

// convert.js
const { Client } = require("@notionhq/client");
const { NotionToMarkdown } = require("notion-to-md");
const fs = require("fs");

const notion = new Client({ auth: process.env.NOTION_API_KEY });
const n2m = new NotionToMarkdown({ notionClient: notion });

(async () => {
  const pageId = process.argv[2]; // the page ID, taken from the page URL
  const mdblocks = await n2m.pageToMarkdown(pageId);
  const mdString = n2m.toMarkdownString(mdblocks);
  fs.writeFileSync("page.md", mdString.parent);
  console.log("Wrote page.md");
})();
Enter fullscreen mode Exit fullscreen mode
NOTION_API_KEY=your_token node convert.js <page-id>
Enter fullscreen mode Exit fullscreen mode

toMarkdownString returns an object. The page itself is in .parent. Since v2.7.0 the package doesn't write child pages to disk for you; it returns their Markdown in the same object, so loop over it and save them yourself if you need them.

We checked the method names against version 3.1.9 by running the script above with a stubbed Notion client that returned a heading, paragraph, bullet, to-do and callout. Output:

# Team Wiki

Hello from Notion.

- First point

> 💡 Callout text

- [x] Ship docs
Enter fullscreen mode Exit fullscreen mode

Note that notion-to-md rendered the callout as a blockquote, so API output doesn't need the callout fix from the cleanup script. Run it against one real page of yours before wiring it into CI, since a stub can't catch permission errors.

Gotchas

Callouts are HTML in the export. Covered above. The API route with notion-to-md gave us blockquotes instead.

Databases come out as CSV. A full-page database exports as a CSV plus one Markdown file per row (subpage). If you want the table itself in Markdown, convert the CSV. Our free CSV to Markdown tool takes the first row as the header and returns a Markdown table. Notion also notes you can export only the current view or the default view of a database, not all views at once.

Windows and long paths. Notion's guide says Windows may fail to open the zip when nested folder and file names pass the 260-character path limit. Its suggested fixes: uncheck Create folders for subpages when exporting, or extract with 7-Zip. The cleanup script shortens names too, but only after you've extracted the files.

Images and attachments. Check where the image files landed and whether the image links still resolve after you move the folder. The cleanup script rewrites relative image links along with page links.

Private pages. The public-URL converter can't see them. Use the export or the API.

Using the Markdown for RAG or a chatbot

Clean Markdown from Notion makes a decent knowledge source because headings survive, and headings are natural split points. For chunking and what to strip before indexing, see our guide on converting PDFs to Markdown for RAG; the same advice applies here.

To turn a published Notion FAQ or help page into question-and-answer pairs, our free Notion to FAQ Generator writes FAQs from a public Notion URL, and it's instructed to skip questions the page can't answer. For the full setup of a site chatbot on your docs, see How to set up an AI chatbot for your website FAQ.

FAQ

Can Notion export to Markdown?
Yes. Use ••• > Export and choose Markdown & CSV. Non-database pages export as Markdown; full-page databases export as CSV with a Markdown file per subpage.

Why are my Notion callouts HTML after export?
Notion exports callouts as HTML because Markdown has no equivalent. The cleanup script above turns <aside> callouts into blockquotes.

How do I convert a Notion database to Markdown?
It exports as a CSV plus a .md file for each subpage. Convert the CSV into a Markdown table with a script or a CSV-to-Markdown converter.

Can I convert a private Notion page without exporting?
Not with a public-URL converter, since a private page exposes no text. Use the built-in export or the API with a token that can see the page.

Does notion-to-md need the page to be shared?
With an internal connection token, yes: add the connection to the page via ••• > Add connections. A personal access token acts as you and uses your own permissions.


Got a public Notion page? Paste the URL into our free converter and copy the Markdown: appie.run/tools/convert-notion-to-markdown. To turn those docs into a site chatbot, Appie's AI Chatbots start on the Free plan (500 credits, no credit card needed).

If you use the AI agent and approval patterns from our earlier post, how to add human approval to AI agent actions, a Notion-to-Markdown sync is a good low-risk job to automate first: it reads, it doesn't write back.

Top comments (0)