告别Notion导出烦恼:notion-to-md让Markdown转换效率提升10倍的全攻略
你是否还在为Notion页面导出Markdown格式时排版错乱、嵌套丢失而头疼?作为一名内容创作者或开发者,你可能经历过手动调整导出文件的痛苦:代码块格式错误、表格变形、嵌套列表层级混乱...这些问题不仅浪费时间,还可能破坏原始内容的结构完整性。
本文将全面解析notion-to-md(一款专为Notion到Markdown转换设计的开源工具)的核心功能、使用方法和高级技巧。通过本文,你将能够:
- 掌握notion-to-md的安装与基础配置
- 实现Notion页面到Markdown的一键转换
- 处理复杂嵌套结构和特殊元素(表格、代码块、数学公式等)
- 定制化转换规则以满足特定需求
- 解决常见的转换问题和性能优化
项目概述:Notion到Markdown的桥梁
notion-to-md是一个轻量级但功能强大的Node.js库,专门用于将Notion页面、区块和区块列表转换为格式规范的Markdown文本。它完美支持Notion的嵌套结构,并提供了灵活的自定义解析选项,让你能够轻松地将Notion内容整合到静态网站生成器(如Hexo、Jekyll)、文档系统或版本控制系统中。
核心优势
| 特性 | notion-to-md | 原生Notion导出 | 其他转换工具 |
|---|---|---|---|
| 嵌套结构支持 | ✅ 完整支持 | ❌ 部分支持 | ⚠️ 有限支持 |
| 代码块保留 | ✅ 完整保留格式 | ⚠️ 基础支持 | ⚠️ 部分支持 |
| 表格转换 | ✅ 完美转换 | ❌ 转换为纯文本 | ⚠️ 格式错乱 |
| 数学公式 | ✅ LaTeX格式支持 | ❌ 无法导出 | ⚠️ 图片形式 |
| 自定义规则 | ✅ 高度可定制 | ❌ 无 | ⚠️ 有限定制 |
| 性能 | ✅ 高效处理 | ⚠️ 较慢 | ⚠️ 中等 |
| 离线使用 | ✅ 支持 | ❌ 需联网 | ⚠️ 部分支持 |
技术架构
notion-to-md采用模块化设计,主要包含以下核心组件:
快速开始:从零到一的安装与配置
环境准备
在开始使用notion-to-md之前,请确保你的开发环境满足以下要求:
- Node.js 12.x或更高版本
- npm或yarn包管理器
- Notion API密钥(获取方式见下文)
安装步骤
# 使用npm安装
npm install notion-to-md @notionhq/client
# 或使用yarn安装
yarn add notion-to-md @notionhq/client
获取Notion API密钥
- 访问Notion开发者页面:https://www.notion.so/my-integrations
- 点击"New integration"创建新集成
- 填写集成名称(如"notion-to-md")
- 选择相关工作区
- 点击"Submit"创建
- 复制生成的"Internal Integration Token"(这就是你的API密钥)
基础使用示例
const { Client } = require("@notionhq/client");
const { NotionToMarkdown } = require("notion-to-md");
// 初始化Notion客户端
const notion = new Client({
auth: "your-notion-api-key", // 替换为你的Notion API密钥
});
// 初始化notion-to-md
const n2m = new NotionToMarkdown({ notionClient: notion });
async function convertNotionPageToMarkdown(pageId) {
// 将Notion页面转换为Markdown块
const mdBlocks = await n2m.pageToMarkdown(pageId);
// 将Markdown块转换为字符串
const mdString = n2m.toMarkdownString(mdBlocks);
// 输出结果
console.log(mdString.parent);
// 如果有子页面,也可以获取子页面内容
for (const key in mdString) {
if (key !== "parent") {
console.log(`\n\n--- 子页面: ${key} ---\n`);
console.log(mdString[key]);
}
}
return mdString;
}
// 使用页面ID调用函数
convertNotionPageToMarkdown("your-notion-page-id"); // 替换为你的页面ID
核心功能详解:处理复杂Notion内容
支持的Notion块类型
notion-to-md支持几乎所有Notion块类型的转换,包括但不限于:
文本格式化
notion-to-md能够完美保留Notion中的文本格式,包括粗体、斜体、下划线、删除线、内联代码等:
// Notion文本块示例转换
const textBlock = {
"object": "block",
"type": "paragraph",
"paragraph": {
"rich_text": [
{
"type": "text",
"text": {
"content": "这是一个包含",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
}
},
{
"type": "text",
"text": {
"content": "粗体",
"link": null
},
"annotations": {
"bold": true,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
}
},
{
"type": "text",
"text": {
"content": "、",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
}
},
{
"type": "text",
"text": {
"content": "斜体",
"link": null
},
"annotations": {
"bold": false,
"italic": true,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
}
},
{
"type": "text",
"text": {
"content": "和",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
}
},
{
"type": "text",
"text": {
"content": "内联代码",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": true,
"color": "default"
}
},
{
"type": "text",
"text": {
"content": "的文本示例。",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
}
}
]
}
};
// 转换结果
// 这是一个包含**粗体**、*斜体*和`内联代码`的文本示例。
代码块处理
notion-to-md对代码块的支持尤为出色,能够保留语法高亮信息并完美转换:
// 代码块转换示例
const codeBlock = {
"object": "block",
"type": "code",
"code": {
"rich_text": [
{
"type": "text",
"text": {
"content": "function greeting(name) {\n console.log(`Hello, ${name}!`);\n}\n\ngreeting('World');",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
}
}
],
"language": "javascript"
}
};
// 转换结果
```javascript
function greeting(name) {
console.log(`Hello, ${name}!`);
}
greeting('World');
表格转换
notion-to-md能够将Notion表格完美转换为Markdown表格,包括合并单元格和复杂表格结构:
// 表格转换示例
async function convertTable() {
// 获取表格块
const tableBlocks = await n2m.pageToMarkdown("table-page-id");
// 转换为Markdown
const mdTable = n2m.toMarkdownString(tableBlocks).parent;
console.log(mdTable);
}
// 转换结果
| 姓名 | 年龄 | 职业 | 城市 |
|------|------|------|------|
| 张三 | 28 | 工程师 | 北京 |
| 李四 | 32 | 设计师 | 上海 |
| 王五 | 25 | 产品经理 | 广州 |
数学公式支持
对于学术写作和技术文档,notion-to-md提供了对LaTeX数学公式的完美支持:
// 数学公式转换示例
const equationBlock = {
"object": "block",
"type": "equation",
"equation": {
"expression": "E = mc^2"
}
};
// 转换结果
// $$E = mc^2$$
const inlineEquationBlock = {
"object": "block",
"type": "paragraph",
"paragraph": {
"rich_text": [
{
"type": "text",
"text": {
"content": "爱因斯坦的质能方程",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
}
},
{
"type": "equation",
"equation": {
"expression": "E = mc^2"
}
},
{
"type": "text",
"text": {
"content": "是相对论的重要成果。",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
}
}
]
}
};
// 转换结果
// 爱因斯坦的质能方程 $E = mc^2$ 是相对论的重要成果。
高级应用:定制化与性能优化
自定义转换规则
notion-to-md允许你为特定类型的Notion块定义自定义转换规则,以满足个性化需求:
// 自定义转换示例:将引用块转换为自定义格式
n2m.setCustomTransformer("quote", async (block) => {
const quoteContent = await n2m.blockToMarkdown(block);
// 获取引用块的图标(如果有)
const icon = block.quote.icon
? `[${block.quote.icon.emoji}] `
: "";
// 返回自定义格式的引用
return `<div class="custom-quote">${icon}${quoteContent}</div>`;
});
// 自定义转换示例:处理特殊的数据库块
n2m.setCustomTransformer("database", async (block) => {
// 获取数据库属性
const databaseId = block.id;
const databaseTitle = block.child_database.title;
// 返回数据库链接而非内容
return `[数据库: ${databaseTitle}](notion://www.notion.so/${databaseId})`;
});
嵌套结构处理
Notion内容的一大特点是丰富的嵌套结构,notion-to-md对此提供了完美支持:
以下是处理嵌套结构的代码示例:
// 处理嵌套结构示例
async function processNestedStructure(pageId) {
// 获取Markdown块
const mdBlocks = await n2m.pageToMarkdown(pageId);
// 转换为Markdown字符串,保留嵌套结构
const mdString = n2m.toMarkdownString(mdBlocks).parent;
console.log(mdString);
}
// 转换结果示例
// - 一级列表项
// - 二级列表项
// - 三级列表项
// - 四级列表项
// - 一级列表项
// > 引用内容
// > - 引用内列表项
// > - 引用内列表项
性能优化策略
对于大型Notion页面或批量转换任务,性能优化尤为重要。以下是一些提高转换效率的策略:
- 批量处理
// 批量处理多个页面
async function batchConvertPages(pageIds) {
// 控制并发数量
const concurrency = 3;
const results = {};
// 分批次处理
for (let i = 0; i < pageIds.length; i += concurrency) {
const batch = pageIds.slice(i, i + concurrency);
// 并行处理当前批次
const batchResults = await Promise.all(
batch.map(async (pageId) => {
const mdBlocks = await n2m.pageToMarkdown(pageId);
return { [pageId]: n2m.toMarkdownString(mdBlocks).parent };
})
);
// 合并结果
batchResults.forEach(result => {
Object.assign(results, result);
});
}
return results;
}
- 缓存机制
// 实现缓存机制
const cache = new Map();
async function convertWithCache(pageId) {
// 检查缓存
if (cache.has(pageId)) {
console.log(`使用缓存: ${pageId}`);
return cache.get(pageId);
}
// 无缓存,执行转换
console.log(`转换页面: ${pageId}`);
const mdBlocks = await n2m.pageToMarkdown(pageId);
const mdString = n2m.toMarkdownString(mdBlocks);
// 存入缓存,设置过期时间(1小时)
cache.set(pageId, mdString);
setTimeout(() => cache.delete(pageId), 3600000);
return mdString;
}
- 增量转换
// 增量转换实现
async function incrementalConvert(pageId, lastModified) {
// 获取页面信息
const pageInfo = await notion.pages.retrieve({ page_id: pageId });
// 检查最后修改时间
const pageLastModified = new Date(pageInfo.last_edited_time);
if (lastModified && pageLastModified <= new Date(lastModified)) {
console.log("页面未修改,无需转换");
return null; // 或返回缓存内容
}
// 执行转换
const mdBlocks = await n2m.pageToMarkdown(pageId);
const mdString = n2m.toMarkdownString(mdBlocks);
return {
content: mdString,
lastModified: pageLastModified.toISOString()
};
}
实战案例:从Notion到静态博客的完整流程
案例概述
本案例将展示如何使用notion-to-md构建一个工作流,将Notion作为内容管理系统,自动将内容转换为Markdown并部署到静态博客(以Hexo为例)。
完整实现代码
const { Client } = require("@notionhq/client");
const { NotionToMarkdown } = require("notion-to-md");
const fs = require("fs");
const path = require("path");
const matter = require("gray-matter");
const { execSync } = require("child_process");
// 配置
const CONFIG = {
NOTION_API_KEY: "your-notion-api-key",
BLOG_DATABASE_ID: "your-blog-database-id",
OUTPUT_DIR: path.join(__dirname, "../source/_posts"),
HEXO_PATH: path.join(__dirname, "..")
};
// 初始化
const notion = new Client({ auth: CONFIG.NOTION_API_KEY });
const n2m = new NotionToMarkdown({ notionClient: notion });
// 自定义转换规则
n2m.setCustomTransformer("callout", async (block) => {
const icon = block.callout.icon ? block.callout.icon.emoji : "💡";
const content = await n2m.blockToMarkdown(block);
return `::: tip ${icon}\n${content}\n:::\n`;
});
// 获取博客文章列表
async function getBlogPosts() {
const response = await notion.databases.query({
database_id: CONFIG.BLOG_DATABASE_ID,
filter: {
property: "发布状态",
select: {
equals: "已发布"
}
},
sorts: [
{
property: "发布日期",
direction: "descending"
}
]
});
return response.results;
}
// 从页面属性提取元数据
function extractMetadata(page) {
const properties = page.properties;
return {
title: properties.标题.title[0].plain_text,
date: properties.发布日期.date.start,
tags: properties.标签.multi_select.map(tag => tag.name),
categories: properties.分类.select ? [properties.分类.select.name] : [],
excerpt: properties.摘要.rich_text[0]?.plain_text || "",
slug: properties.Slug.rich_text[0]?.plain_text ||
properties.标题.title[0].plain_text
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^\w-]/g, '')
};
}
// 转换并保存文章
async function convertAndSavePost(page) {
const metadata = extractMetadata(page);
console.log(`转换文章: ${metadata.title}`);
// 获取Markdown内容
const mdBlocks = await n2m.pageToMarkdown(page.id);
const mdContent = n2m.toMarkdownString(mdBlocks).parent;
// 创建Front Matter
const postData = matter.stringify(mdContent, metadata);
// 确保输出目录存在
if (!fs.existsSync(CONFIG.OUTPUT_DIR)) {
fs.mkdirSync(CONFIG.OUTPUT_DIR, { recursive: true });
}
// 保存文件
const filePath = path.join(CONFIG.OUTPUT_DIR, `${metadata.slug}.md`);
fs.writeFileSync(filePath, postData, "utf-8");
return metadata.slug;
}
// 构建Hexo博客
function buildHexoBlog() {
console.log("构建Hexo博客...");
execSync("hexo clean && hexo generate", { cwd: CONFIG.HEXO_PATH });
console.log("博客构建完成");
}
// 主函数
async function main() {
try {
console.log("开始同步博客文章...");
// 获取博客文章
const posts = await getBlogPosts();
console.log(`找到 ${posts.length} 篇已发布文章`);
// 转换并保存所有文章
const slugs = [];
for (const post of posts) {
const slug = await convertAndSavePost(post);
slugs.push(slug);
}
// 构建博客
buildHexoBlog();
console.log(`同步完成,共处理 ${slugs.length} 篇文章`);
console.log("文章Slug:", slugs.join(", "));
} catch (error) {
console.error("同步失败:", error);
process.exit(1);
}
}
// 执行主函数
main();
自动化部署
为了实现完全自动化,你可以使用GitHub Actions或其他CI/CD工具设置定时任务:
# .github/workflows/sync-notion.yml
name: Sync Notion to Hexo
on:
schedule:
- cron: '0 0 * * *' # 每天午夜执行
workflow_dispatch: # 允许手动触发
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: 设置Node.js
uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'npm'
- name: 安装依赖
run: npm ci
- name: 同步Notion内容
run: node scripts/sync-notion.js
env:
NOTION_API_KEY: ${{ secrets.NOTION_API_KEY }}
BLOG_DATABASE_ID: ${{ secrets.BLOG_DATABASE_ID }}
- name: 部署到GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./public
常见问题与解决方案
认证问题
问题:API密钥无效或认证失败
解决方案:
- 检查API密钥是否正确
- 确保集成已添加到目标页面/数据库
- 验证工作区权限设置
// 验证API密钥
async function verifyNotionAuth(apiKey) {
try {
const client = new Client({ auth: apiKey });
await client.users.me();
return true;
} catch (error) {
console.error("认证失败:", error.message);
return false;
}
}
转换格式问题
问题:转换后的Markdown格式与预期不符
解决方案:
- 检查notion-to-md版本,确保使用最新版
- 自定义转换规则覆盖默认行为
- 处理特殊块类型的自定义逻辑
// 调试格式问题
async function debugFormatIssue(pageId, blockId) {
// 获取原始块数据
const block = await notion.blocks.retrieve({ block_id: blockId });
console.log("原始块数据:", JSON.stringify(block, null, 2));
// 单独转换该块
const md = await n2m.blockToMarkdown(block);
console.log("转换结果:", md);
return md;
}
性能问题
问题:转换大型页面或批量转换时速度慢
解决方案:
- 实现缓存机制避免重复转换
- 使用并发控制限制同时转换的页面数量
- 增量转换只处理修改过的内容
// 带并发控制的批量转换
async function batchConvertWithConcurrency(pageIds, concurrency = 3) {
const results = [];
const batches = [];
// 分成多个批次
for (let i = 0; i < pageIds.length; i += concurrency) {
batches.push(pageIds.slice(i, i + concurrency));
}
// 按批次处理
for (const batch of batches) {
const batchResults = await Promise.all(
batch.map(id => convertPage(id))
);
results.push(...batchResults);
// 每批之间短暂延迟
await new Promise(resolve => setTimeout(resolve, 1000));
}
return results;
}
特殊块类型问题
问题:某些特殊块类型无法正确转换
解决方案:
- 使用自定义转换函数处理特殊块
- 提交issue到官方仓库请求支持
- 实现临时解决方案作为过渡
// 处理不支持的块类型
n2m.setCustomTransformer("unsupported", async (block) => {
// 记录不支持的块类型
console.warn(`不支持的块类型: ${block.type}, ID: ${block.id}`);
// 返回占位内容或跳过
return `<!-- 不支持的块类型: ${block.type} -->`;
});
总结与展望
notion-to-md作为一款强大的Notion到Markdown转换工具,为内容创作者和开发者提供了高效、可靠的内容转换解决方案。它不仅完美支持Notion的各种块类型和嵌套结构,还提供了灵活的自定义转换规则,满足个性化需求。
通过本文介绍的安装配置、基础使用、高级技巧和实战案例,你应该已经能够熟练运用notion-to-md处理各种Notion到Markdown的转换任务。无论是构建个人博客、管理技术文档还是处理学术写作,notion-to-md都能显著提高你的工作效率。
最佳实践清单
- 始终使用最新版本的notion-to-md以获得最佳支持
- 实现缓存机制提高性能,特别是批量转换时
- 为特殊块类型编写自定义转换规则
- 处理API错误和网络问题的健壮代码
- 定期备份重要内容以防转换过程中出现问题
未来发展方向
notion-to-md仍在持续发展中,未来可能的改进方向包括:
- 更丰富的格式支持:进一步完善对Notion新块类型的支持
- 性能优化:提高大型文档的转换速度和内存使用效率
- 插件系统:允许第三方插件扩展转换功能
- 图形界面:提供可视化配置工具简化使用难度
- 多语言支持:扩展对非英语内容的处理能力
如果你对notion-to-md感兴趣,欢迎通过以下方式参与项目:
- 贡献代码:https://gitcode.com/gh_mirrors/no/notion-to-md
- 报告问题:项目Issues页面
- 分享经验:在社区中分享你的使用案例和技巧
通过不断优化和完善,notion-to-md有望成为Notion生态系统中不可或缺的工具,帮助更多人无缝连接Notion与Markdown生态。
如果你觉得本文对你有帮助,请点赞、收藏并关注作者,以获取更多关于Notion生态和技术工具的深度文章。下期预告:《使用notion-to-md构建个人知识管理系统》
附录:参考资料
-
官方文档
- notion-to-md GitHub仓库:https://gitcode.com/gh_mirrors/no/notion-to-md
- Notion API文档:https://developers.notion.com/
-
相关工具
- Notion SDK:https://github.com/makenotion/notion-sdk-js
- Markdown指南:https://www.markdownguide.org/
-
学习资源
- Notion API入门教程
- Markdown高级技巧
- Node.js异步编程最佳实践
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



