| | #!/usr/bin/env node |
| |
|
| | import { config } from 'dotenv'; |
| | import { join, dirname, basename } from 'path'; |
| | import { fileURLToPath } from 'url'; |
| | import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs'; |
| | import { convertNotionToMarkdown } from './notion-converter.mjs'; |
| | import { convertToMdx } from './mdx-converter.mjs'; |
| |
|
| | |
| | config(); |
| |
|
| | const __filename = fileURLToPath(import.meta.url); |
| | const __dirname = dirname(__filename); |
| |
|
| | |
| | const DEFAULT_INPUT = join(__dirname, 'input', 'pages.json'); |
| | const DEFAULT_OUTPUT = join(__dirname, 'output'); |
| | const ASTRO_CONTENT_PATH = join(__dirname, '..', '..', 'src', 'content', 'article.mdx'); |
| | const ASTRO_ASSETS_PATH = join(__dirname, '..', '..', 'src', 'content', 'assets', 'image'); |
| | const ASTRO_BIB_PATH = join(__dirname, '..', '..', 'src', 'content', 'bibliography.bib'); |
| |
|
| | function parseArgs() { |
| | const args = process.argv.slice(2); |
| | const config = { |
| | input: DEFAULT_INPUT, |
| | output: DEFAULT_OUTPUT, |
| | clean: false, |
| | notionOnly: false, |
| | mdxOnly: false, |
| | token: process.env.NOTION_TOKEN |
| | }; |
| |
|
| | for (const arg of args) { |
| | if (arg.startsWith('--input=')) { |
| | config.input = arg.split('=')[1]; |
| | } else if (arg.startsWith('--output=')) { |
| | config.output = arg.split('=')[1]; |
| | } else if (arg.startsWith('--token=')) { |
| | config.token = arg.split('=')[1]; |
| | } else if (arg === '--clean') { |
| | config.clean = true; |
| | } else if (arg === '--notion-only') { |
| | config.notionOnly = true; |
| | } else if (arg === '--mdx-only') { |
| | config.mdxOnly = true; |
| | } |
| | } |
| |
|
| | return config; |
| | } |
| |
|
| | function showHelp() { |
| | console.log(` |
| | π Notion to MDX Toolkit |
| | |
| | Usage: |
| | node index.mjs [options] |
| | |
| | Options: |
| | --input=PATH Input pages configuration file (default: input/pages.json) |
| | --output=PATH Output directory (default: output/) |
| | --token=TOKEN Notion API token (or set NOTION_TOKEN env var) |
| | --clean Clean output directory before processing |
| | --notion-only Only convert Notion to Markdown (skip MDX conversion) |
| | --mdx-only Only convert existing Markdown to MDX |
| | --help, -h Show this help |
| | |
| | Environment Variables: |
| | NOTION_TOKEN Your Notion integration token |
| | |
| | Examples: |
| | # Full conversion workflow |
| | NOTION_TOKEN=your_token node index.mjs --clean |
| | |
| | # Only convert Notion pages to Markdown |
| | node index.mjs --notion-only --token=your_token |
| | |
| | # Only convert existing Markdown to MDX |
| | node index.mjs --mdx-only |
| | |
| | # Custom paths |
| | node index.mjs --input=my-pages.json --output=converted/ --token=your_token |
| | |
| | Configuration File Format (pages.json): |
| | { |
| | "pages": [ |
| | { |
| | "id": "your-notion-page-id", |
| | "title": "Page Title", |
| | "slug": "page-slug" |
| | } |
| | ] |
| | } |
| | |
| | Workflow: |
| | 1. Notion β Markdown (with media download) |
| | 2. Markdown β MDX (with Astro components) |
| | 3. Copy to Astro content directory |
| | `); |
| | } |
| |
|
| | function ensureDirectory(dir) { |
| | if (!existsSync(dir)) { |
| | mkdirSync(dir, { recursive: true }); |
| | } |
| | } |
| |
|
| | async function cleanDirectory(dir) { |
| | if (existsSync(dir)) { |
| | const { execSync } = await import('child_process'); |
| | execSync(`rm -rf "${dir}"/*`, { stdio: 'inherit' }); |
| | } |
| | } |
| |
|
| | function readPagesConfig(inputFile) { |
| | try { |
| | const content = readFileSync(inputFile, 'utf8'); |
| | return JSON.parse(content); |
| | } catch (error) { |
| | console.error(`β Error reading pages config: ${error.message}`); |
| | return { pages: [] }; |
| | } |
| | } |
| |
|
| | function copyToAstroContent(outputDir) { |
| | console.log('π Copying MDX files to Astro content directory...'); |
| |
|
| | try { |
| | |
| | mkdirSync(dirname(ASTRO_CONTENT_PATH), { recursive: true }); |
| | mkdirSync(ASTRO_ASSETS_PATH, { recursive: true }); |
| |
|
| | |
| | const files = readdirSync(outputDir); |
| | const mdxFiles = files.filter(file => file.endsWith('.mdx')); |
| | if (mdxFiles.length > 0) { |
| | const mdxFile = join(outputDir, mdxFiles[0]); |
| | copyFileSync(mdxFile, ASTRO_CONTENT_PATH); |
| | console.log(` β
Copied MDX to ${ASTRO_CONTENT_PATH}`); |
| | } |
| |
|
| | |
| | const mediaDir = join(outputDir, 'media'); |
| | if (existsSync(mediaDir)) { |
| | const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.svg']; |
| | let imageCount = 0; |
| |
|
| | function copyImagesRecursively(dir) { |
| | const files = readdirSync(dir); |
| | for (const file of files) { |
| | const filePath = join(dir, file); |
| | const stat = statSync(filePath); |
| |
|
| | if (stat.isDirectory()) { |
| | copyImagesRecursively(filePath); |
| | } else if (imageExtensions.some(ext => file.toLowerCase().endsWith(ext))) { |
| | const filename = basename(filePath); |
| | const destPath = join(ASTRO_ASSETS_PATH, filename); |
| | copyFileSync(filePath, destPath); |
| | imageCount++; |
| | } |
| | } |
| | } |
| |
|
| | copyImagesRecursively(mediaDir); |
| | console.log(` β
Copied ${imageCount} image(s) to ${ASTRO_ASSETS_PATH}`); |
| |
|
| | |
| | const mdxContent = readFileSync(ASTRO_CONTENT_PATH, 'utf8'); |
| | let updatedContent = mdxContent.replace(/\.\/media\//g, './assets/image/'); |
| | |
| | updatedContent = updatedContent.replace(/\.\/assets\/image\/[^\/]+\//g, './assets/image/'); |
| | writeFileSync(ASTRO_CONTENT_PATH, updatedContent); |
| | console.log(` β
Updated image paths in MDX file`); |
| | } |
| |
|
| | |
| | writeFileSync(ASTRO_BIB_PATH, ''); |
| | console.log(` β
Created empty bibliography at ${ASTRO_BIB_PATH}`); |
| |
|
| | } catch (error) { |
| | console.warn(` β οΈ Failed to copy to Astro: ${error.message}`); |
| | } |
| | } |
| |
|
| |
|
| | async function main() { |
| | const args = process.argv.slice(2); |
| |
|
| | if (args.includes('--help') || args.includes('-h')) { |
| | showHelp(); |
| | process.exit(0); |
| | } |
| |
|
| | const config = parseArgs(); |
| |
|
| | console.log('π Notion to MDX Toolkit'); |
| | console.log('========================'); |
| |
|
| | try { |
| | if (config.clean) { |
| | console.log('π§Ή Cleaning output directory...'); |
| | await cleanDirectory(config.output); |
| | } |
| |
|
| | if (config.mdxOnly) { |
| | |
| | console.log('π MDX conversion only mode'); |
| | await convertToMdx(config.output, config.output); |
| | copyToAstroContent(config.output); |
| |
|
| | } else if (config.notionOnly) { |
| | |
| | console.log('π Notion conversion only mode'); |
| | await convertNotionToMarkdown(config.input, config.output, config.token); |
| |
|
| | } else { |
| | |
| | console.log('π Full conversion workflow'); |
| |
|
| | |
| | console.log('\nπ Step 1: Converting Notion pages to Markdown...'); |
| | await convertNotionToMarkdown(config.input, config.output, config.token); |
| |
|
| | |
| | console.log('\nπ Step 2: Converting Markdown to MDX...'); |
| | const pagesConfig = readPagesConfig(config.input); |
| | const firstPage = pagesConfig.pages && pagesConfig.pages.length > 0 ? pagesConfig.pages[0] : null; |
| | const pageId = firstPage ? firstPage.id : null; |
| | await convertToMdx(config.output, config.output, pageId, config.token); |
| |
|
| | |
| | console.log('\nπ Step 3: Copying to Astro content directory...'); |
| | copyToAstroContent(config.output); |
| | } |
| |
|
| | console.log('\nπ Conversion completed successfully!'); |
| |
|
| | } catch (error) { |
| | console.error('β Error:', error.message); |
| | process.exit(1); |
| | } |
| | } |
| |
|
| | |
| | export { convertNotionToMarkdown, convertToMdx }; |
| |
|
| | |
| | if (import.meta.url === `file://${process.argv[1]}`) { |
| | main(); |
| | } |
| |
|