| #!/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(); |
| } |
|
|