├── .gitignore ├── .vscode └── settings.json ├── Procfile ├── README.md ├── assets ├── 2020-07-16-22-36-52-895669.png ├── 2020-07-16-22-36-53-795056.png ├── 2020-07-16-22-36-54-695166.png ├── 2020-07-16-22-36-55-407341.png ├── 2020-07-16-22-36-56-394616.png ├── 2020-07-16-22-36-57-445010.png ├── 2020-07-16-22-36-59-197396.png ├── 2020-07-16-22-37-00-245922.png ├── 2020-07-16-22-37-01-351948.png └── 2020-07-16-22-37-02-563403.png ├── notion-md-exporter.py ├── requirements.txt └── setup.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "/Users/wsy/opt/anaconda3/bin/python" 3 | } -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: sh setup.sh && streamlit run notion-md-exporter.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | This web-based app can help you to batch export notion pages to Markdown format correctly. 4 | 5 | Please follow these simple steps. 6 | 7 | Step 1. [Open this link](http://notion-to-markdown.herokuapp.com/), and you will see the web interface. 8 | 9 | ![](assets/2020-07-16-22-36-52-895669.png) 10 | 11 | Step 2. Get your notion token_v2 and input it in the first textbox. You can learn how to get your token by [reading this web page](https://www.redgregory.com/notion/2020/6/15/9zuzav95gwzwewdu1dspweqbv481s5). 12 | 13 | Step 3. Move **all the pages** you want to export to a new page. 14 | 15 | ![](assets/2020-07-16-22-36-53-795056.png) 16 | 17 | As you may see, links to pages are also acceptable. In this case, you don't need to move the pages at all. 18 | 19 | Step 4. Please copy the link to the new page and input it in the second textbox and press Enter. 20 | 21 | ![](assets/2020-07-16-22-36-54-695166.png) 22 | 23 | Step 5. You will see a new button named "export." Click it. 24 | 25 | ![](assets/2020-07-16-22-36-55-407341.png) 26 | 27 | Step 6. When you see a new link named "Click to download" shown on the web page, click it and download a zip file. Extract it, and you will find all the Markdown files as well as the images. 28 | 29 | ![](assets/2020-07-16-22-36-56-394616.png) 30 | ![](assets/2020-07-16-22-36-57-445010.png) 31 | 32 | Step 7. (Optional) You can drag the folder to Obsidian's Root Directory and browse the Markdown files. 33 | 34 | ![](assets/2020-07-16-22-36-59-197396.png) 35 | 36 | As you can see here, all the titles of pages are kept as the name of Markdown files. 37 | 38 | In contrast, if you export the Markdown files using the default export function in notion, you will get an extracted directory like this. 39 | 40 | ![](assets/2020-07-16-22-37-00-245922.png) 41 | 42 | If you put it into Obsidian, you will realize the linked sub-page was not downloaded at all. 43 | 44 | ![](assets/2020-07-16-22-37-01-351948.png) 45 | 46 | Besides, in the subfolder, there is no image file. The image is still in the cloud. 47 | 48 | ![](assets/2020-07-16-22-37-02-563403.png) 49 | 50 | If you like this app, please add a star to [my Github Repo](https://github.com/wshuyi/demo-notion-markdown-exporter). Thanks! 51 | 52 | That's all. Enjoy! :) 53 | 54 | ## Acknowledgement 55 | 56 | This Web app is developed by [Shuyi Wang](https://twitter.com/wshuyi) based on [Eunchan Cho(@echo724)\'s notion2md](https://github.com/echo724/notion2md) 57 | 58 | -------------------------------------------------------------------------------- /assets/2020-07-16-22-36-52-895669.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wshuyi/demo-notion-markdown-exporter/d115582d701d5e5609e3a2faa573921cb4761aaf/assets/2020-07-16-22-36-52-895669.png -------------------------------------------------------------------------------- /assets/2020-07-16-22-36-53-795056.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wshuyi/demo-notion-markdown-exporter/d115582d701d5e5609e3a2faa573921cb4761aaf/assets/2020-07-16-22-36-53-795056.png -------------------------------------------------------------------------------- /assets/2020-07-16-22-36-54-695166.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wshuyi/demo-notion-markdown-exporter/d115582d701d5e5609e3a2faa573921cb4761aaf/assets/2020-07-16-22-36-54-695166.png -------------------------------------------------------------------------------- /assets/2020-07-16-22-36-55-407341.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wshuyi/demo-notion-markdown-exporter/d115582d701d5e5609e3a2faa573921cb4761aaf/assets/2020-07-16-22-36-55-407341.png -------------------------------------------------------------------------------- /assets/2020-07-16-22-36-56-394616.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wshuyi/demo-notion-markdown-exporter/d115582d701d5e5609e3a2faa573921cb4761aaf/assets/2020-07-16-22-36-56-394616.png -------------------------------------------------------------------------------- /assets/2020-07-16-22-36-57-445010.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wshuyi/demo-notion-markdown-exporter/d115582d701d5e5609e3a2faa573921cb4761aaf/assets/2020-07-16-22-36-57-445010.png -------------------------------------------------------------------------------- /assets/2020-07-16-22-36-59-197396.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wshuyi/demo-notion-markdown-exporter/d115582d701d5e5609e3a2faa573921cb4761aaf/assets/2020-07-16-22-36-59-197396.png -------------------------------------------------------------------------------- /assets/2020-07-16-22-37-00-245922.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wshuyi/demo-notion-markdown-exporter/d115582d701d5e5609e3a2faa573921cb4761aaf/assets/2020-07-16-22-37-00-245922.png -------------------------------------------------------------------------------- /assets/2020-07-16-22-37-01-351948.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wshuyi/demo-notion-markdown-exporter/d115582d701d5e5609e3a2faa573921cb4761aaf/assets/2020-07-16-22-37-01-351948.png -------------------------------------------------------------------------------- /assets/2020-07-16-22-37-02-563403.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wshuyi/demo-notion-markdown-exporter/d115582d701d5e5609e3a2faa573921cb4761aaf/assets/2020-07-16-22-37-02-563403.png -------------------------------------------------------------------------------- /notion-md-exporter.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | import base64 3 | import shutil 4 | from zipfile import ZipFile 5 | from pathlib import Path 6 | import notion 7 | import os 8 | from notion.client import NotionClient 9 | import requests 10 | import sys 11 | 12 | def recursive_getblocks(block,container,client): 13 | new_id = client.get_block(block.id) 14 | if not new_id in container: 15 | container.append(new_id) 16 | try: 17 | for children_id in block.get("content"): 18 | children = client.get_block(children_id) 19 | recursive_getblocks(children,container,client) 20 | except: 21 | return 22 | 23 | def link(name,url): 24 | return "["+name+"]"+"("+url+")" 25 | 26 | def image_export(url,count,directory): 27 | img_dir = directory + 'img_{0}.png'.format(count) 28 | r = requests.get(url, allow_redirects=True) 29 | open(img_dir,'wb').write(r.content) 30 | return img_dir 31 | 32 | def block2md(blocks,directory): 33 | md = "" 34 | img_count = 0 35 | numbered_list_index = 0 36 | title = blocks[0].title 37 | title = title.replace(' ','') 38 | directory += '{0}/'.format(title) 39 | if not(os.path.isdir(directory)): 40 | Path(directory).mkdir() 41 | for block in blocks: 42 | try: 43 | btype = block.type 44 | except: 45 | continue 46 | if btype != "numbered_list": 47 | numbered_list_index = 0 48 | try: 49 | bt = block.title 50 | except: 51 | pass 52 | if btype == 'header': 53 | md += "# " + bt 54 | elif btype == "sub_header": 55 | md += "## " +bt 56 | elif btype == "sub_sub_header": 57 | md += "### " +bt 58 | elif btype == 'page': 59 | try: 60 | if "https:" in block.icon: 61 | icon = "!"+link("",block.icon) 62 | else: 63 | icon = block.icon 64 | md += "# " + icon + bt 65 | except: 66 | md += "# " + bt 67 | elif btype == 'text': 68 | md += bt +" " 69 | elif btype == 'bookmark': 70 | md += link(bt,block.link) 71 | elif btype == "video" or btype == "file" or btype =="audio" or btype =="pdf" or btype == "gist": 72 | md += link(block.source,block.source) 73 | elif btype == "bulleted_list" or btype == "toggle": 74 | md += '- '+bt 75 | elif btype == "numbered_list": 76 | numbered_list_index += 1 77 | md += str(numbered_list_index)+'. ' + bt 78 | elif btype == "image": 79 | img_count += 1 80 | try: 81 | img_dir = image_export(block.source,img_count,directory) 82 | md += "!"+link(img_dir,img_dir) 83 | except: 84 | # pass 85 | st.markdown(f"error exporting {block.source}") 86 | elif btype == "code": 87 | md += "```"+block.language+"\n"+block.title+"\n```" 88 | elif btype == "equation": 89 | md += "$$"+block.latex+"$$" 90 | elif btype == "divider": 91 | md += "---" 92 | elif btype == "to_do": 93 | if block.checked: 94 | md += "- [x] "+ bt 95 | else: 96 | md += "- [ ]" + bt 97 | elif btype == "quote": 98 | md += "> "+bt 99 | elif btype == "column" or btype =="column_list": 100 | continue 101 | else: 102 | pass 103 | md += "\n\n" 104 | return md 105 | 106 | def export(url,token): 107 | client = NotionClient(token_v2=token) 108 | page = client.get_block(url) 109 | blocks = [] 110 | recursive_getblocks(page,blocks,client) 111 | md = block2md(blocks,'./') 112 | return md 113 | 114 | def export_cli(fname, directory, token_v2, url): 115 | fname = os.path.join(directory,fname) 116 | file = open(fname,'w') 117 | blocks = [] 118 | 119 | client = NotionClient(token_v2 = token_v2) 120 | page = client.get_block(url) 121 | 122 | recursive_getblocks(page,blocks,client) 123 | md = block2md(blocks,directory) 124 | 125 | file.write(md) 126 | file.close() 127 | 128 | def notion_markdown_export(token_v2, url, directory): 129 | pages_to_download = [] 130 | 131 | client = NotionClient(token_v2 = token_v2) 132 | page = client.get_block(url) 133 | 134 | for children_id in page.get("content"): 135 | children = client.get_block(children_id) 136 | if children.title: 137 | pages_to_download.append({"title":children.title, "id":children.id}) 138 | 139 | if not(os.path.isdir(directory)): 140 | Path(directory).mkdir() 141 | 142 | for item in pages_to_download: 143 | try: 144 | export_cli(f"{item['title']}.md", directory, token_v2, item["id"]) 145 | except: 146 | st.markdown(f"Error exporting {item['title']}.md!") 147 | return 148 | 149 | def adjust_notion_image_dir(source_md): 150 | with open(source_md) as f: 151 | data = f.read() 152 | data = data.replace("./notion_output/", "") 153 | with open(source_md, 'w') as f: 154 | f.write(data) 155 | 156 | def batch_adjust_notion_image_dir(directory): 157 | source_mds = list(Path(directory).glob("*.md")) 158 | for source_md in source_mds: 159 | adjust_notion_image_dir(source_md) 160 | 161 | def zipdir(path, ziph): 162 | # ziph is zipfile handle 163 | for root, dirs, files in os.walk(path): 164 | for file in files: 165 | ziph.write(os.path.join(root, file)) 166 | 167 | # main proc starts here 168 | st.title("Notion Markdown Exporter") 169 | st.markdown("This Web app is developed by [Shuyi Wang](https://twitter.com/wshuyi) based on [Eunchan Cho(@echo724)\'s notion2md](https://github.com/echo724/notion2md)") 170 | st.markdown("The coressponding [Github Page of this app is here](https://github.com/wshuyi/demo-notion-markdown-exporter).") 171 | 172 | 173 | token_v2 = st.text_input("Your Notion token_v2:") 174 | url = st.text_input("The Link or ID you want to export:") 175 | 176 | directory = './notion_output/' 177 | 178 | 179 | running = False 180 | 181 | if token_v2 and url and not running: 182 | if st.button("export"): 183 | running = True 184 | 185 | if Path(directory).exists(): 186 | shutil.rmtree(Path(directory)) 187 | notion_markdown_export(token_v2, url, directory) 188 | batch_adjust_notion_image_dir(directory) 189 | with ZipFile('exported.zip', 'w') as myzip: 190 | zipdir(directory, myzip) 191 | with open('exported.zip', "rb") as f: 192 | bytes = f.read() 193 | b64 = base64.b64encode(bytes).decode() 194 | href = f'\ 195 | Click to download\ 196 | ' 197 | st.markdown(href, unsafe_allow_html=True) 198 | running = False 199 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.24.0 2 | streamlit==0.63.0 3 | notion==0.0.25 4 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | mkdir -p ~/.streamlit/ 2 | 3 | echo "\ 4 | [general]\n\ 5 | email = \"nkwshuyi@gmail.com\"\n\ 6 | " > ~/.streamlit/credentials.toml 7 | 8 | echo "\ 9 | [server]\n\ 10 | headless = true\n\ 11 | enableCORS=false\n\ 12 | port = $PORT\n\ 13 | " > ~/.streamlit/config.toml --------------------------------------------------------------------------------