├── .gitignore ├── README.md ├── package.json ├── src ├── client.ts ├── convert.ts └── index.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.sh 3 | demo.ts 4 | .env 5 | dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notion to markdown blog 2 | 3 | ## Install 4 | 5 | ```bash 6 | npm install notion2mdblog 7 | ``` 8 | 9 | ```bash 10 | yarn add notion2mdblog 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```typescript 16 | // Client is extended from @notionhq/client 17 | import { Client } from "notion2mdblog"; 18 | import fs from "node:fs"; 19 | 20 | // Initializing a client 21 | const notion = new Client({ 22 | auth: process.env.NOTION_KEY, 23 | }); 24 | 25 | async function main() { 26 | const blogPages = await notion.markdown.db2md({ 27 | database_id: NOTION_DATABASE_ID, 28 | }); 29 | blogPages.map((blogPage) => { 30 | fs.writeFileSync(`blogs/${blogPage.title}`, blogPage.content); 31 | }); 32 | } 33 | main(); 34 | ``` 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notion2mdblog", 3 | "version": "0.0.1", 4 | "description": "Notion to markdown blog", 5 | "displayName": "Notion to markdown blog", 6 | "author": "Arpit Bhalla", 7 | "keywords": [ 8 | "notion", 9 | "notion-blog", 10 | "notion-markdown", 11 | "notion2markdown", 12 | "notion2blog" 13 | ], 14 | "files": [ 15 | "dist" 16 | ], 17 | "main": "dist/index.js", 18 | "types": "dist/index.d.ts", 19 | "dependencies": { 20 | "@notionhq/client": "^0.4.11", 21 | "json2md": "^1.12.0" 22 | }, 23 | "devDependencies": { 24 | "@types/json2md": "^1.5.1" 25 | }, 26 | "scripts": { 27 | "prepublish": "tsc" 28 | }, 29 | "license": "ISC", 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/arpitBhalla/notion2mdblog.git" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/arpitBhalla/notion2mdblog/issues" 36 | }, 37 | "homepage": "https://github.com/arpitBhalla/notion2mdblog#readme" 38 | } 39 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "@notionhq/client"; 2 | import { ClientOptions } from "@notionhq/client/build/src/Client"; 3 | import json2md from "json2md"; 4 | import { block2markdown } from "./convert"; 5 | 6 | export class Notion2markdownClient extends Client { 7 | constructor(options?: ClientOptions) { 8 | super(options); 9 | } 10 | private getBlocks = async ({ block_id }: { block_id: string }) => { 11 | const blocks = await this.blocks.children.list({ block_id }); 12 | return blocks.results; 13 | }; 14 | private getPageBlocks = async ({ page_id }: { page_id: string }) => { 15 | const pageBlocks = await this.getBlocks({ block_id: page_id }); 16 | return pageBlocks; 17 | }; 18 | private page2md = async ({ page_id }: { page_id: string }) => { 19 | const pageBlocks = await this.getPageBlocks({ page_id }); 20 | const jsonDataObject = block2markdown(pageBlocks); 21 | return json2md(jsonDataObject); 22 | }; 23 | private db2md = async ({ database_id }: { database_id: string }) => { 24 | const pages = await this.databases.query({ database_id }); 25 | return await Promise.all( 26 | pages.results.map((page) => this.page2md({ page_id: page.id })) 27 | ); 28 | }; 29 | readonly markdown = { 30 | page2md: this.page2md, 31 | db2md: this.db2md, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/convert.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { ListBlockChildrenResponse } from "@notionhq/client/build/src/api-endpoints"; 3 | 4 | let BLOCK_TYPES = { 5 | paragraph: { 6 | key: "p", 7 | format: (paragraphs) => 8 | paragraphs 9 | .map((para) => para.text?.map((block) => block.plain_text)) 10 | .flat(), 11 | join: true, 12 | }, 13 | heading_1: { 14 | key: "h1", 15 | format: (heading) => 16 | heading.text.map((block) => block.plain_text)?.join(""), 17 | }, 18 | heading_2: { 19 | key: "h2", 20 | format: (heading) => 21 | heading.text.map((block) => block.plain_text)?.join(""), 22 | }, 23 | heading_3: { 24 | key: "h3", 25 | format: (heading) => 26 | heading.text.map((block) => block.plain_text)?.join(""), 27 | }, 28 | bulleted_list_item: { 29 | key: "ul", 30 | format: (items) => 31 | items.map((item) => 32 | item.text.map((itemText) => itemText.plain_text)?.join("") 33 | ), 34 | join: true, 35 | }, 36 | numbered_list_item: { 37 | key: "ol", 38 | format: (items) => 39 | items.map((item) => 40 | item.text.map((itemText) => itemText.plain_text)?.join("") 41 | ), 42 | join: true, 43 | }, 44 | code: { 45 | key: "code", 46 | format: (snippet) => ({ 47 | language: snippet.language, 48 | content: snippet?.text?.map((line) => line?.plain_text), 49 | }), 50 | }, 51 | image: { 52 | key: "img", 53 | format: (image) => ({ 54 | title: "My image title", 55 | source: image[image.type].url, 56 | alt: "My image alt", 57 | }), 58 | }, 59 | // to_do: { key: "" }, 60 | // toggle: { key: "" }, 61 | // child_page: { key: "" }, 62 | // child_database: { key: "" }, 63 | // embed: { key: "" }, 64 | // video: { key: "" }, 65 | // file: { key: "" }, 66 | // pdf: { key: "" }, 67 | // bookmark: { key: "" }, 68 | // callout: { key: "" }, 69 | // quote: { key: "" }, 70 | // equation: { key: "" }, 71 | // divider: { key: "" }, 72 | // table_of_contents: { key: "" }, 73 | // column: { key: "" }, 74 | // column_list: { key: "" }, 75 | // link_preview: { key: "" }, 76 | // unsupported: { key: "" }, 77 | }; 78 | 79 | export const block2markdown = ( 80 | blockResponse: ListBlockChildrenResponse["results"] 81 | ) => { 82 | return blockResponse 83 | .map((block) => { 84 | const blockType = block["type"]; 85 | const param = BLOCK_TYPES[blockType]; 86 | if (!param?.key) return null; 87 | return { 88 | child: block[blockType], 89 | ...param, 90 | }; 91 | }) 92 | .filter(Boolean) 93 | .reduce((blockArray, currBlock) => { 94 | const prevBlock = blockArray.pop(); 95 | if (!prevBlock) return [currBlock]; 96 | if (prevBlock.key === currBlock.key && currBlock.join) { 97 | if (!Array.isArray(prevBlock.child)) { 98 | prevBlock.child = [prevBlock.child]; 99 | } 100 | blockArray.push({ 101 | ...currBlock, 102 | child: [...prevBlock.child, currBlock.child], 103 | }); 104 | } else { 105 | blockArray.push(prevBlock, currBlock); 106 | } 107 | return blockArray; 108 | }, []) 109 | .map((block) => ({ 110 | [block.key]: block?.format?.(block.child) || block.child, 111 | })); 112 | }; 113 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Notion2markdownClient } from "./client"; 2 | 3 | export { Notion2markdownClient as Client }; 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "ES5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */, 8 | "module": "CommonJS" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | "moduleResolution": "Node", 11 | // "allowJs": true, /* Allow javascript files to be compiled. */ 12 | "outDir": "dist", 13 | "declaration": true, 14 | // "checkJs": true, /* Report errors in .js files. */ 15 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 16 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 17 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 18 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 19 | // "outFile": "./", /* Concatenate and emit output to single file. */ 20 | // "resolveJsonModule": true, 21 | // "outDir": "./", /* Redirect output structure to the directory. */ 22 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 23 | // "composite": true, /* Enable project compilation */ 24 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 25 | // "removeComments": true, /* Do not emit comments to output. */ 26 | // "noEmit": true, /* Do not emit outputs. */ 27 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 28 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 29 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 30 | 31 | /* Strict Type-Checking Options */ 32 | "strict": true /* Enable all strict type-checking options. */, 33 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 34 | // "strictNullChecks": true, /* Enable strict null checks. */ 35 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 36 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 37 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 38 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 39 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 40 | 41 | /* Additional Checks */ 42 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 43 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 44 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 45 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 46 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 47 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ 48 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 49 | 50 | /* Module Resolution Options */ 51 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 52 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 53 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 54 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 55 | // "typeRoots": [], /* List of folders to include type definitions from. */ 56 | // "types": [], /* Type declaration files to be included in compilation. */ 57 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 58 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 59 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 60 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 61 | 62 | /* Source Map Options */ 63 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 64 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 65 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 66 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 67 | 68 | /* Experimental Options */ 69 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 70 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 71 | 72 | /* Advanced Options */ 73 | "skipLibCheck": true /* Skip type checking of declaration files. */, 74 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 75 | }, 76 | "include": ["src/**.*"] 77 | } 78 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@notionhq/client@^0.4.11": 6 | version "0.4.11" 7 | resolved "https://registry.yarnpkg.com/@notionhq/client/-/client-0.4.11.tgz#030d2397a7d38b3dfda2444cb8c3067bb6b7c842" 8 | integrity sha512-+zT+qBHx8V6Fj3Tj9a4KKfYE4tWhrS/7A5OWRinEjsL5J0GKskQ5EnnLEejptg0COh6BgJShVQLPJXsr0NovLQ== 9 | dependencies: 10 | "@types/node-fetch" "^2.5.10" 11 | node-fetch "^2.6.1" 12 | 13 | "@types/json2md@^1.5.1": 14 | version "1.5.1" 15 | resolved "https://registry.yarnpkg.com/@types/json2md/-/json2md-1.5.1.tgz#2eba6ececf43252950d286eeae77612024392ad3" 16 | integrity sha512-nl3qRDdBUtGeHEhwSeYMtXw5i6UXspS6pxaahmcrE4Fxt0UAvKvsJzu6UMtJ85+yTYiB9nA0TB1kFMEVkYDgUg== 17 | 18 | "@types/node-fetch@^2.5.10": 19 | version "2.5.12" 20 | resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.12.tgz#8a6f779b1d4e60b7a57fb6fd48d84fb545b9cc66" 21 | integrity sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw== 22 | dependencies: 23 | "@types/node" "*" 24 | form-data "^3.0.0" 25 | 26 | "@types/node@*": 27 | version "17.0.6" 28 | resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.6.tgz#cc1589c9ee853b389e67e8fb4384e0f250a139b9" 29 | integrity sha512-+XBAjfZmmivILUzO0HwBJoYkAyyySSLg5KCGBDFLomJo0sV6szvVLAf4ANZZ0pfWzgEds5KmGLG9D5hfEqOhaA== 30 | 31 | asynckit@^0.4.0: 32 | version "0.4.0" 33 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 34 | integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= 35 | 36 | combined-stream@^1.0.8: 37 | version "1.0.8" 38 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" 39 | integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== 40 | dependencies: 41 | delayed-stream "~1.0.0" 42 | 43 | delayed-stream@~1.0.0: 44 | version "1.0.0" 45 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 46 | integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= 47 | 48 | form-data@^3.0.0: 49 | version "3.0.1" 50 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" 51 | integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== 52 | dependencies: 53 | asynckit "^0.4.0" 54 | combined-stream "^1.0.8" 55 | mime-types "^2.1.12" 56 | 57 | indento@^1.1.13: 58 | version "1.1.13" 59 | resolved "https://registry.yarnpkg.com/indento/-/indento-1.1.13.tgz#751331b327c04740eeb7be40c5606e6e255c9e36" 60 | integrity sha512-YZWk3mreBEM7sBPddsiQnW9Z8SGg/gNpFfscJq00HCDS7pxcQWWWMSVKJU7YkTRyDu1Zv2s8zaK8gQWKmCXHlg== 61 | 62 | json2md@^1.12.0: 63 | version "1.12.0" 64 | resolved "https://registry.yarnpkg.com/json2md/-/json2md-1.12.0.tgz#3ac0a2f8f5af140d6f29d91ab1107ca696305165" 65 | integrity sha512-bpJpuqECzkndCa10aGPxeJNikmkDN7PAGXHvrBGTI4uey6QbTL5p0rkhk9lB3lKU4J7yGvkSmVBt8VhzUGu/fA== 66 | dependencies: 67 | indento "^1.1.13" 68 | 69 | mime-db@1.51.0: 70 | version "1.51.0" 71 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" 72 | integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== 73 | 74 | mime-types@^2.1.12: 75 | version "2.1.34" 76 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" 77 | integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A== 78 | dependencies: 79 | mime-db "1.51.0" 80 | 81 | node-fetch@^2.6.1: 82 | version "2.6.6" 83 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89" 84 | integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA== 85 | dependencies: 86 | whatwg-url "^5.0.0" 87 | 88 | tr46@~0.0.3: 89 | version "0.0.3" 90 | resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" 91 | integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= 92 | 93 | webidl-conversions@^3.0.0: 94 | version "3.0.1" 95 | resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" 96 | integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= 97 | 98 | whatwg-url@^5.0.0: 99 | version "5.0.0" 100 | resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" 101 | integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= 102 | dependencies: 103 | tr46 "~0.0.3" 104 | webidl-conversions "^3.0.0" 105 | --------------------------------------------------------------------------------