md-to-notion
v1.0.0
Published
Sync local Markdown files into a Notion database. Supports frontmatter, inline formatting, code blocks, and idempotent create/update/skip.
Downloads
35
Maintainers
Readme
md-to-notion
Sync a local folder of Markdown files into a Notion database. Each .md file becomes a Notion page, folder nesting is preserved via a Path property, and re-runs are idempotent: new files are created, changed files are updated, unchanged files are skipped.
docs/
guide.md → Notion page "Getting Started Guide"
api/
auth.md → Notion page "Authentication"
users.md → Notion page "Users API"Install
npm install md-to-notionOr run it directly without installing:
npx md-to-notion --docs-dir ./docsFor global access across all projects:
npm install -g md-to-notionPrerequisites
- Node.js 18+
- A Notion integration token
- A Notion database shared with your integration
Notion database setup
Create a database in Notion with at least a "Name" title property. The script automatically adds any missing properties (Path, CreatedAt, UpdatedAt, CreatedBy, UpdatedBy) on the first run.
Share the database with your integration via the "..." menu > "Connections" in Notion.
CLI usage
Set your Notion credentials as environment variables (or use a .env file):
export NOTION_TOKEN=secret_your-integration-token
export NOTION_DATABASE_ID=your-database-idThen run:
md-to-notionCLI flags
| Flag | Description | Default |
|------|-------------|---------|
| --docs-dir <path> | Path to the Markdown directory | ./docs |
| --concurrency <n> | Files to process in parallel | 3 |
| --dry-run | Preview changes without modifying Notion | false |
| -h, --help | Show help | |
| -v, --version | Show version | |
Examples
# Sync from a custom directory with higher concurrency
md-to-notion --docs-dir ./content --concurrency 5
# Preview what would happen without touching Notion
md-to-notion --dry-run
# Use with npx in a CI pipeline
npx md-to-notion --docs-dir ./docsConsole output
Syncing docs from ./docs to Notion database...
Concurrency: 3 files in parallel
+ Creating: api/auth.md
~ Updating: guide.md
= Skipping: api/users.md
Sync complete: 1 created, 1 updated, 1 skippedProgrammatic API
Use md-to-notion as a library in your own Node.js scripts:
import { syncAll } from 'md-to-notion';
await syncAll({
notionToken: process.env.NOTION_TOKEN,
databaseId: process.env.NOTION_DATABASE_ID,
docsDir: './docs',
concurrency: 5,
dryRun: false,
});Exported functions
syncAll(options?) — Run the full sync pipeline. Accepts an options object that overrides environment variables:
| Option | Type | Description |
|--------|------|-------------|
| notionToken | string | Notion integration token |
| databaseId | string | Target Notion database ID |
| docsDir | string | Path to docs directory |
| concurrency | number | Parallel file limit |
| dryRun | boolean | Preview mode |
| gitEmail | string | Email for CreatedBy/UpdatedBy metadata |
discoverMarkdownFiles(docsDir) — Recursively scan a directory for .md files. Returns Array<{ absolutePath, relativePath }>.
parseMarkdown(raw, relativePath) — Parse raw Markdown content into a title and Notion blocks. Returns { title, blocks }.
chunkBlocks(blocks, size?) — Split a block array into chunks of size (default 100) for Notion's API limit.
configure(overrides) — Merge values into the global config object (called automatically by syncAll).
GitHub Action
A ready-to-use workflow file is included at .github/workflows/sync-docs.yml. Copy it into your repository and add two secrets:
- Go to your repo > Settings > Secrets and variables > Actions
- Add
NOTION_TOKEN(your integration token) - Add
NOTION_DATABASE_ID(your database ID)
The workflow triggers on every push to main that changes files under docs/. You can also trigger it manually from the Actions tab.
name: Sync docs to Notion
on:
push:
branches: [main]
paths:
- "docs/**"
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install -g md-to-notion
- run: md-to-notion --docs-dir ./docs
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }}Environment variables
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| NOTION_TOKEN | Yes | — | Notion integration token |
| NOTION_DATABASE_ID | Yes | — | Target database ID |
| DOCS_DIR | No | ./docs | Path to Markdown docs directory |
| CONCURRENCY | No | 3 | Files to process in parallel |
Environment variables can also be set in a .env file (loaded automatically via dotenv).
What gets synced
Markdown features
- Headings (H1, H2, H3) →
heading_1,heading_2,heading_3 - Paragraphs with inline formatting (bold, italic, code, links)
- Bullet lists →
bulleted_list_item - Numbered lists →
numbered_list_item - Code blocks with language detection (maps aliases like
js→javascript,py→python) - Blockquotes →
quote - Horizontal rules →
divider
Frontmatter
YAML frontmatter sets the page title:
---
title: Authentication Guide
---
# Auth
Content here...If no title is in the frontmatter, the filename (without extension) is used.
Skip optimization
A .sync-state.json file tracks SHA-256 content hashes. On subsequent runs, unchanged files are skipped without any Notion API calls. State is checkpointed every 10 files and on SIGINT/SIGTERM, so interrupted runs resume where they left off.
Project structure
├── sync.js CLI entry point (bin)
├── index.js Programmatic API entry point
├── src/
│ ├── config.js Environment loading, validation, configure()
│ ├── discover.js Recursive .md file scanner
│ ├── parser.js Frontmatter + Markdown → Notion blocks
│ ├── notion.js Notion API wrapper (query, create, update)
│ ├── rate-limiter.js p-queue rate limiter (3 req/s)
│ └── syncer.js Sync orchestration, hashing, state persistence
├── .env.example Environment template
└── .github/
└── workflows/
└── sync-docs.yml GitHub Action templatePublishing
# Log in to your NPM account
npm login
# Publish
npm publishLicense
ISC
