Skip to content

Instantly share code, notes, and snippets.

@Sal7one
Last active November 29, 2025 01:34
Show Gist options
  • Select an option

  • Save Sal7one/783c7e903dc68e6ec15b87dd1241cc3f to your computer and use it in GitHub Desktop.

Select an option

Save Sal7one/783c7e903dc68e6ec15b87dd1241cc3f to your computer and use it in GitHub Desktop.
Notion to md. AI slop but mostly works!

Complete Guide: Export Your Notion Workspace to Markdown Files

Overview: The Two-Step Processs

We'll use two specialized scripts:

  1. Backup Script (bulk-exporter.js): Downloads your Notion content as structured JSON
  2. Converter Script (json-to-markdown.js): Transforms JSON into beautifully formatted Markdown

Step 1: Setup Your Project

Create and configure your export directory:

mkdir notion-backup
cd notion-backup

Package Configuration

Create package.json:

{
  "name": "notion-bulk-exporter",
  "version": "1.0.0",
  "description": "Bulk Notion workspace backup to JSON and Markdown",
  "main": "bulk-exporter.js",
  "type": "commonjs",
  "scripts": {
    "backup": "node bulk-exporter.js",
    "convert": "node json-to-markdown.js",
    "full-export": "node bulk-exporter.js && node json-to-markdown.js"
  },
  "dependencies": {
    "@notionhq/client": "^2.3.0",
    "dotenv": "^16.4.5"
  }
}

Install dependencies:

npm install

Step 2: Configure Notion Integration

Get Your Notion Tokens

  1. Go to Notion Integrations
  2. Click "+ New integration"
  3. Give it a name (e.g., "Workspace Backup Bot")
  4. Select the workspace you want to backup
  5. Click "Submit" to create the integration
  6. Copy the "Internal Integration Token" (starts with ntn_ or secret_)
  7. Repeat for each workspace you want to backup

Note: Each Notion workspace requires its own integration token. You cannot use one token across multiple workspaces.

Share Pages with Your Integration

Critical Step: Your integration can only access pages explicitly shared with it.

For each workspace:

  1. Open Notion and navigate to the top-level page you want to export
  2. Click "Share" in the top-right corner
  3. Click "Invite" and search for your integration name
  4. Select your integration from the dropdown
  5. The integration now has access to that page and all its children

Pro Tip: Share your workspace's root page to export everything, or share specific pages for selective backups.

Step 3: Set Environment Variables

Create a .env file in your project directory:

# .env file
NOTION_TOKENS="ntn_64xxx...,ntn_333xxx...,ntn_444xxx..."
NOTION_BACKUP_DIR="./notion-bulk-backup"
NOTION_MARKDOWN_DIR="./notion-markdown-export"

Or export directly in your terminal:

export NOTION_TOKENS="ntn_64xxx...,ntn_333xxx...,ntn_444xxx..."
export NOTION_BACKUP_DIR="./notion-bulk-backup"
export NOTION_MARKDOWN_DIR="./notion-markdown-export"

Token Format:

  • Comma-separated list of tokens
  • No spaces between tokens (unless inside quotes)
  • Each token represents one workspace

Step 4: Create the Backup Script

Save this as bulk-exporter.js:

#!/usr/bin/env node
const { Client } = require("@notionhq/client");
const fs = require("fs");
const path = require("path");
const readline = require("readline");
require("dotenv").config();

// [Insert the complete bulk-exporter.js code here]

Make it executable (optional):

chmod +x bulk-exporter.js

Key Features:

  • βœ… Multi-workspace support (unlimited workspaces)
  • βœ… Recursive page/database crawling
  • βœ… Preserves complete block hierarchies (nested blocks)
  • βœ… Handles all Notion block types
  • βœ… Creates manifest.json for tracking
  • βœ… Incremental updates (won't re-download existing pages)
  • βœ… Error recovery (continues if one workspace fails)

Step 5: Run the Backup

Execute the backup script:

node bulk-exporter.js
# Or if you made it executable:
# ./bulk-exporter.js

You'll be prompted with export options:

How do you want to export from each workspace?
  1) Export EVERYTHING each integration can see
  2) Search and pick root pages interactively (per workspace)
  3) Same root IDs for all workspaces (enter manually)

Recommendations:

  • Option 1: Best for complete workspace backups (recommended for most users)
  • Option 2: Best if you want to select specific pages per workspace interactively
  • Option 3: Best if you have the same page structure across workspaces and know the page IDs

Expected Output:

======================================
WORKSPACE 1 of 3
======================================
βœ“ Successfully connected to workspace

[PAGE] My Important Notes (abc-123...)
[PAGE] Project Documentation (def-456...)
[DATABASE] Task List (ghi-789...)

βœ… Workspace export complete.

Time Estimate:

  • Small workspace (< 100 pages): 2-5 minutes
  • Medium workspace (100-500 pages): 10-30 minutes
  • Large workspace (> 500 pages): 30+ minutes

Step 6: Create the Markdown Converter

Save this as json-to-markdown.js:

#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const https = require("https");
const http = require("http");
const readline = require("readline");

// [Insert the complete json-to-markdown.js code here]

Conversion Capabilities:

  • βœ… All text formatting (bold, italic, strikethrough, code, underline)
  • βœ… Headings (H1, H2, H3)
  • βœ… Lists (bulleted, numbered, to-do with checkboxes)
  • βœ… Code blocks with language syntax
  • βœ… Images (with optional download)
  • βœ… Files and PDFs (with optional download)
  • βœ… Videos (with optional download)
  • βœ… Tables, quotes, callouts
  • βœ… Nested blocks (preserves hierarchy)
  • βœ… Links and bookmarks
  • βœ… Database schemas
  • βœ… YAML frontmatter with metadata

Step 7: Convert to Markdown

Run the converter:

node json-to-markdown.js

Interactive Prompts:

Enter path to backup directory: [./notion-bulk-backup]
Download files, images, PDFs, and videos? [y/N]

Asset Download Recommendation:

  • Yes: Choose if you want a complete offline archive (recommended)
  • No: Choose if you want to keep Notion's hosted URLs (smaller export)

What happens during conversion:

  1. Reads all JSON files from backup
  2. Converts each page to Markdown
  3. Downloads assets if requested (may take time for large workspaces)
  4. Creates INDEX.md with export summary
  5. Preserves folder structure

Step 8: Review Your Export

Your exported structure will look like:

notion-markdown-export/
β”œβ”€β”€ workspace_1/
β”‚   β”œβ”€β”€ INDEX.md              # Summary of this workspace
β”‚   β”œβ”€β”€ pages/
β”‚   β”‚   β”œβ”€β”€ My_Page_abc123.md
β”‚   β”‚   └── Another_Page_def456.md
β”‚   β”œβ”€β”€ databases/
β”‚   β”‚   └── Tasks_ghi789.md
β”‚   β”œβ”€β”€ assets/              # Downloaded files (if enabled)
β”‚   β”‚   β”œβ”€β”€ image_123.png
β”‚   β”‚   β”œβ”€β”€ pdf_456.pdf
β”‚   β”‚   └── video_789.mp4
β”‚   β”œβ”€β”€ manifest.json         # Export metadata
β”‚   └── workspace_info.json   # Workspace details
β”œβ”€β”€ workspace_2/
β”‚   └── [same structure]
└── workspace_3/
    └── [same structure]

File Naming Convention:

  • Pages: Title_NotionID.md (e.g., Project_Notes_abc123.md)
  • Assets: type_NotionID.ext (e.g., image_abc123.png)
  • Databases: DatabaseName_NotionID.md

What Gets Converted

βœ… Fully Supported:

  • Text: All rich text formatting (bold, italic, code, strikethrough, underline)
  • Blocks: Paragraphs, headings, lists, to-dos, quotes, callouts
  • Code: Code blocks with language syntax highlighting
  • Media: Images, videos, files, PDFs (with download option)
  • Embeds: Bookmarks, link previews
  • Databases: Schema documentation with property types
  • Nested Content: Toggles, nested lists, child pages/databases
  • Metadata: Created/edited times, page properties, URLs

⚠️ Partially Supported:

  • Tables: Converted to Markdown tables (complex formatting may be simplified)
  • Equations: Preserved as LaTeX ($$formula$$)
  • Synced Blocks: Marked as references, not duplicated
  • Column Layouts: Marked but flattened to single column
  • Database Relations: Links preserved as IDs, not expanded
  • Advanced Properties: Formulas and rollups show values, not formulas

❌ Not Supported:

  • Interactive Elements: Buttons, embedded apps
  • Real-time Collaboration: Comments, mentions (metadata only)
  • Database Views: Only default view captured
  • Page History: Only current version exported
  • Permissions: All content exported without access restrictions

Troubleshooting

Common Issues

Issue: "NOTION_TOKEN not set"

# Solution: Make sure .env file exists and tokens are set
echo 'NOTION_TOKENS="your_token_here"' > .env

Issue: "Failed to talk to Notion API"

  • Check that your token is valid (starts with ntn_ or secret_)
  • Verify you've shared pages with your integration
  • Ensure your integration has the correct permissions

Issue: "No pages found for this integration"

  • You must explicitly share pages with your integration (see Step 2)
  • Go to Notion β†’ Share β†’ Invite your integration

Issue: "Failed to download image/file"

  • Notion's file URLs expire after ~1 hour
  • Run the converter immediately after backup
  • Or use Notion-hosted URLs (choose 'N' for asset download)

Issue: "Rate limit exceeded"

  • Notion has API rate limits (~3 requests/second)
  • The scripts include built-in delays
  • For very large workspaces, the export may take several hours

Issue: "Memory issues with large workspaces"

# Increase Node.js memory limit
node --max-old-space-size=4096 bulk-exporter.js

Validation Checklist

After export, verify:

  • All expected workspaces have folders
  • Page count matches expectations (check INDEX.md)
  • Images display correctly in Markdown viewers
  • Assets folder contains downloaded files
  • No error messages in console output
  • manifest.json shows exportCompletedAt timestamp

Advanced Usage

Scheduled Backups

Create a cron job for automatic backups:

# Edit crontab
crontab -e

# Add this line: Run every Sunday at 2 AM
0 2 * * 0 cd /path/to/notion-backup && /usr/bin/node bulk-exporter.js && /usr/bin/node json-to-markdown.js

Backup Script

Create backup.sh for easy execution:

#!/bin/bash
set -e

echo "Starting Notion backup..."
node bulk-exporter.js

echo "Converting to Markdown..."
node json-to-markdown.js

echo "Creating archive..."
tar -czf notion-backup-$(date +%Y%m%d).tar.gz notion-markdown-export/

echo "βœ… Backup complete!"

Make it executable:

chmod +x backup.sh
./backup.sh

Selective Export

Export specific pages by ID:

# When prompted, choose option 3 and enter page IDs:
# https://notion.so/page-name-abc123def456...
# Just copy the URL and the script will extract the ID

Docker Support

Create Dockerfile:

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "full-export"]

Build and run:

docker build -t notion-exporter .
docker run -e NOTION_TOKENS="token1,token2" -v $(pwd)/output:/app/notion-markdown-export notion-exporter

Next Steps: Importing to Other Platforms

After exporting, you can import your Markdown files to:

  1. Wiki.js - Best for team wikis (git-based, searchable)
  2. Obsidian - Best for personal knowledge management
  3. Docusaurus - Best for public documentation
  4. MkDocs - Simple static site generator
  5. BookStack - Good for structured documentation

See the separate "IMPORT_GUIDE.md" for detailed instructions.

FAQ

Q: Can I export multiple workspaces at once? A: Yes! Just provide all tokens comma-separated in NOTION_TOKENS.

Q: How long do exports take? A: Depends on workspace size. Typical: 5-30 minutes. Large workspaces: 1-2 hours.

Q: Will this work with Notion personal accounts? A: Yes, integrations work with all Notion account types.

Q: Do I need to keep Notion open during export? A: No, the scripts use Notion's API independently.

Q: Can I resume a failed export? A: Yes, the backup script tracks progress in manifest.json and skips already-exported pages.

Q: Will this export private pages? A: Only pages shared with your integration. You must explicitly share them.

Q: What about database entries? A: All pages in databases are exported as individual markdown files.

Q: Can I import back to Notion? A: Yes, Notion supports Markdown import, but you'll lose some metadata and structure.

Q: Are there any costs? A: No, this uses Notion's free API. No rate limits for personal use.

Q: What if I have 1000+ pages? A: The scripts handle large workspaces. Just increase Node.js memory if needed.

Support & Contributing

  • Issues: Report bugs on GitHub
  • Features: Suggest improvements via Issues
  • Pull Requests: Contributions welcome!

License

MIT License - Feel free to use, modify, and distribute.


Happy exporting! πŸš€

For import guides and platform-specific instructions, see IMPORT_GUIDE.md

@Sal7one
Copy link
Author

Sal7one commented Nov 16, 2025

delete wikijs pages and more :P

#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const axios = require('axios');
const FormData = require('form-data');
require('dotenv').config();

// Configuration
const WIKI_URL = process.env.WIKIJS_URL || 'http://localhost:3000';
const API_TOKEN = process.env.WIKIJS_TOKEN || '';
const INPUT_DIR = process.env.NOTION_MARKDOWN_DIR || './notion-markdown-export';

const DELAY_BETWEEN_REQUESTS = 500; // ms
const DELAY_BETWEEN_DELETES = 300; // ms

class WikiJSManager {
  constructor(wikiUrl, apiToken) {
    this.wikiUrl = wikiUrl.replace(/\/$/, '');
    this.apiToken = apiToken;
    this.headers = {
      'Authorization': `Bearer ${apiToken}`,
      'Content-Type': 'application/json'
    };
    this.stats = {
      deleted: 0,
      pages: { success: 0, failed: 0 },
      assets: { success: 0, failed: 0 }
    };
    this.assetMap = new Map();
    this.pageIdToPath = new Map();
  }

  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  /**
   * Get all pages from Wiki.js
   */
  async getAllPages() {
    try {
      const query = `
        query {
          pages {
            list {
              id
              path
              title
            }
          }
        }
      `;

      const response = await axios.post(
        `${this.wikiUrl}/graphql`,
        { query },
        { headers: this.headers, timeout: 30000 }
      );

      return response.data?.data?.pages?.list || [];
    } catch (error) {
      console.error('Failed to get pages:', error.message);
      return [];
    }
  }

  /**
   * Delete a single page
   */
  async deletePage(pageId) {
    try {
      const mutation = `
        mutation {
          pages {
            delete(id: ${pageId}) {
              responseResult {
                succeeded
                message
              }
            }
          }
        }
      `;

      const response = await axios.post(
        `${this.wikiUrl}/graphql`,
        { query: mutation },
        { headers: this.headers, timeout: 30000 }
      );

      const result = response.data?.data?.pages?.delete?.responseResult;
      return result?.succeeded || false;
    } catch (error) {
      console.error(`Failed to delete page ${pageId}:`, error.message);
      return false;
    }
  }

  /**
   * Delete all pages from Wiki.js
   */
  async deleteAllPages() {
    console.log('\nπŸ—‘οΈ  Deleting all existing pages...\n');

    const pages = await this.getAllPages();
    
    if (pages.length === 0) {
      console.log('  No pages found to delete.');
      return;
    }

    console.log(`  Found ${pages.length} pages to delete\n`);

    for (const page of pages) {
      const success = await this.deletePage(page.id);
      if (success) {
        console.log(`  βœ“ Deleted: ${page.title}`);
        this.stats.deleted++;
      } else {
        console.log(`  βœ— Failed: ${page.title}`);
      }
      await this.delay(DELAY_BETWEEN_DELETES);
    }

    console.log(`\n  Total deleted: ${this.stats.deleted} pages\n`);
  }

  /**
   * Clean filename for Wiki.js
   */
  cleanFilename(filename) {
    return filename
      .replace(/_[a-f0-9-]{36}\.md$/, '')
      .replace(/_/g, '-')
      .replace(/\.md$/, '');
  }

  /**
   * Extract title from markdown
   */
  extractTitle(content) {
    const match = content.match(/^#\s+(.+)$/m);
    return match ? match[1].trim() : 'Untitled';
  }

  /**
   * Remove frontmatter
   */
  removeFrontmatter(content) {
    return content.replace(/^---\n[\s\S]*?\n---\n\n?/, '');
  }

  /**
   * Upload asset to Wiki.js
   */
  async uploadAsset(assetPath) {
    try {
      const filename = path.basename(assetPath);
      const fileBuffer = fs.readFileSync(assetPath);
      
      const formData = new FormData();
      formData.append('mediaUpload', fileBuffer, filename);
      
      const response = await axios.post(
        `${this.wikiUrl}/u`,
        formData,
        {
          headers: {
            ...formData.getHeaders(),
            'Authorization': `Bearer ${this.apiToken}`
          },
          timeout: 60000,
          maxContentLength: Infinity,
          maxBodyLength: Infinity
        }
      );

      if (response.data && response.data.path) {
        this.stats.assets.success++;
        return response.data.path;
      }
      return null;
    } catch (error) {
      console.error(`    βœ— Failed to upload ${path.basename(assetPath)}:`, error.message);
      this.stats.assets.failed++;
      return null;
    }
  }

  /**
   * Upload all assets from workspace
   */
  async uploadWorkspaceAssets(workspaceDir) {
    const assetsDir = path.join(workspaceDir, 'assets');
    
    if (!fs.existsSync(assetsDir)) {
      return;
    }

    const assetFiles = fs.readdirSync(assetsDir);
    console.log(`  Uploading ${assetFiles.length} assets...`);

    for (const file of assetFiles) {
      const assetPath = path.join(assetsDir, file);
      const stat = fs.statSync(assetPath);
      
      if (stat.isFile()) {
        const wikiPath = await this.uploadAsset(assetPath);
        
        if (wikiPath) {
          const relativePath = path.relative(workspaceDir, assetPath);
          this.assetMap.set(relativePath, wikiPath);
          this.assetMap.set(`../${relativePath}`, wikiPath);
          this.assetMap.set(`./${relativePath}`, wikiPath);
          console.log(`    βœ“ ${file}`);
        }
      }
    }
  }

  /**
   * Fix asset paths in content
   */
  fixAssetPaths(content) {
    let fixedContent = content;

    // Fix images
    fixedContent = fixedContent.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, imagePath) => {
      if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
        return match;
      }

      for (const [oldPath, newPath] of this.assetMap.entries()) {
        if (imagePath.includes(path.basename(oldPath)) || oldPath.includes(path.basename(imagePath))) {
          return `![${alt}](${newPath})`;
        }
      }

      return match;
    });

    // Fix file links
    fixedContent = fixedContent.replace(/(?<!!)\[([^\]]+)\]\(([^)]+)\)/g, (match, text, linkPath) => {
      if (linkPath.startsWith('http://') || linkPath.startsWith('https://') || linkPath.startsWith('`')) {
        return match;
      }

      if (linkPath.includes('assets/') || linkPath.match(/\.(pdf|mp4|webm|zip|docx?|xlsx?)$/i)) {
        for (const [oldPath, newPath] of this.assetMap.entries()) {
          if (linkPath.includes(path.basename(oldPath)) || oldPath.includes(path.basename(linkPath))) {
            return `[${text}](${newPath})`;
          }
        }
      }

      return match;
    });

    return fixedContent;
  }

  /**
   * Combine all pages from a workspace into one
   */
  combineWorkspacePages(workspaceDir) {
    console.log(`  Combining pages from ${path.basename(workspaceDir)}...`);
    
    let combinedContent = '';
    const pagesDir = path.join(workspaceDir, 'pages');
    const databasesDir = path.join(workspaceDir, 'databases');
    
    // Process pages
    if (fs.existsSync(pagesDir)) {
      const pageFiles = fs.readdirSync(pagesDir)
        .filter(f => f.endsWith('.md'))
        .sort();

      console.log(`    Found ${pageFiles.length} pages`);

      for (const file of pageFiles) {
        const filePath = path.join(pagesDir, file);
        let content = fs.readFileSync(filePath, 'utf8');
        
        // Remove frontmatter
        content = this.removeFrontmatter(content);
        
        // Extract title
        const title = this.extractTitle(content);
        
        // Remove the H1 (we'll add it back)
        content = content.replace(/^#\s+.+$/m, '').trim();
        
        // Fix asset paths
        content = this.fixAssetPaths(content);
        
        // Remove child page/database sections (they're just references)
        content = content.replace(/\n---\n\n## πŸ“„ Child Pages[\s\S]*$/, '');
        content = content.replace(/\n## πŸ—„οΈ Child Databases[\s\S]*$/, '');
        
        // Add to combined content with clear separation
        combinedContent += `\n\n---\n\n# ${title}\n\n${content}\n`;
      }
    }

    // Process databases
    if (fs.existsSync(databasesDir)) {
      const dbFiles = fs.readdirSync(databasesDir)
        .filter(f => f.endsWith('.md'))
        .sort();

      console.log(`    Found ${dbFiles.length} databases`);

      for (const file of dbFiles) {
        const filePath = path.join(databasesDir, file);
        let content = fs.readFileSync(filePath, 'utf8');
        
        content = this.removeFrontmatter(content);
        const title = this.extractTitle(content);
        content = content.replace(/^#\s+.+$/m, '').trim();
        content = this.fixAssetPaths(content);
        
        combinedContent += `\n\n---\n\n# ${title}\n\n${content}\n`;
      }
    }

    return combinedContent.trim();
  }

  /**
   * Create a page in Wiki.js
   */
  async createPage(pagePath, title, content) {
    try {
      const mutation = `
        mutation Page {
          pages {
            create(
              content: ${JSON.stringify(content)},
              description: "Combined workspace from Notion export",
              editor: "markdown",
              isPublished: true,
              isPrivate: false,
              locale: "en",
              path: ${JSON.stringify(pagePath)},
              publishEndDate: "",
              publishStartDate: "",
              tags: ["notion-import", "workspace"],
              title: ${JSON.stringify(title)}
            ) {
              responseResult {
                succeeded
                errorCode
                message
              }
              page {
                id
                path
                title
              }
            }
          }
        }
      `;

      const response = await axios.post(
        `${this.wikiUrl}/graphql`,
        { query: mutation },
        {
          headers: this.headers,
          timeout: 60000,
          maxContentLength: Infinity,
          maxBodyLength: Infinity
        }
      );

      const result = response.data?.data?.pages?.create?.responseResult;
      
      if (result?.succeeded) {
        console.log(`  βœ“ Created: ${title}`);
        this.stats.pages.success++;
        return true;
      } else {
        throw new Error(result?.message || 'Unknown error');
      }
    } catch (error) {
      console.error(`  βœ— Failed: ${title}`);
      console.error(`    Error: ${error.response?.data?.errors?.[0]?.message || error.message}`);
      this.stats.pages.failed++;
      return false;
    }
  }

  /**
   * Import workspace as single combined page
   */
  async importWorkspaceAsSinglePage(workspaceDir) {
    const workspaceName = path.basename(workspaceDir);
    console.log(`\n${'='.repeat(60)}`);
    console.log(`Processing: ${workspaceName}`);
    console.log(`${'='.repeat(60)}\n`);

    // Clear asset map for this workspace
    this.assetMap.clear();

    // Upload assets
    console.log('πŸ“Ž Step 1: Uploading assets...');
    await this.uploadWorkspaceAssets(workspaceDir);

    // Combine all pages
    console.log('\nπŸ“„ Step 2: Combining pages...');
    const combinedContent = this.combineWorkspacePages(workspaceDir);

    if (!combinedContent) {
      console.log('  ⚠️  No content found in workspace');
      return;
    }

    // Get workspace info for title
    const workspaceInfoPath = path.join(workspaceDir, 'workspace_info.json');
    let workspaceIndex = workspaceName.replace('workspace_', '');
    let title = `Workspace ${workspaceIndex}`;

    if (fs.existsSync(workspaceInfoPath)) {
      try {
        const info = JSON.parse(fs.readFileSync(workspaceInfoPath, 'utf8'));
        workspaceIndex = info.workspaceIndex || workspaceIndex;
        title = `Workspace ${workspaceIndex}`;
      } catch (e) {
        // Use default title
      }
    }

    // Create single page
    console.log('\nπŸ“ Step 3: Creating combined page...');
    const pagePath = `workspace-${workspaceIndex}`;
    
    // Add table of contents at the top
    const tocContent = `# ${title}\n\n> πŸ“š Combined export from Notion workspace\n\n## Table of Contents\n\nThis page contains all content from this workspace. Use the heading links in the sidebar to navigate.\n\n${combinedContent}`;
    
    await this.createPage(pagePath, title, tocContent);
    await this.delay(DELAY_BETWEEN_REQUESTS);
  }

  /**
   * Import all workspaces as combined pages
   */
  async importAllAsCombined(baseDir) {
    const entries = fs.readdirSync(baseDir, { withFileTypes: true });
    const workspaces = entries
      .filter(e => e.isDirectory() && e.name.startsWith('workspace_'))
      .map(e => path.join(baseDir, e.name))
      .sort();

    if (workspaces.length === 0) {
      console.log('Single workspace detected, importing...');
      await this.importWorkspaceAsSinglePage(baseDir);
    } else {
      console.log(`Found ${workspaces.length} workspace(s)\n`);
      
      for (const workspace of workspaces) {
        await this.importWorkspaceAsSinglePage(workspace);
      }
    }

    this.printSummary();
  }

  /**
   * Print summary
   */
  printSummary() {
    console.log('\n' + '='.repeat(60));
    console.log('IMPORT SUMMARY');
    console.log('='.repeat(60));
    console.log(`\nπŸ—‘οΈ  Deleted: ${this.stats.deleted} pages`);
    console.log(`\nπŸ“„ Combined Pages:`);
    console.log(`  βœ“ Success: ${this.stats.pages.success}`);
    console.log(`  βœ— Failed:  ${this.stats.pages.failed}`);
    console.log(`\nπŸ“Ž Assets:`);
    console.log(`  βœ“ Success: ${this.stats.assets.success}`);
    console.log(`  βœ— Failed:  ${this.stats.assets.failed}`);
    console.log('\n' + '='.repeat(60) + '\n');
  }

  /**
   * Test connection
   */
  async testConnection() {
    try {
      console.log('Testing connection...');
      console.log(`  Target: ${this.wikiUrl}`);
      
      const healthCheck = await axios.get(this.wikiUrl, {
        timeout: 10000,
        validateStatus: () => true,
        proxy: false,
        family: 4
      });
      console.log(`  βœ“ HTTP Response: ${healthCheck.status}`);

      const query = `
        query {
          pages {
            list(limit: 1) {
              id
            }
          }
        }
      `;

      const response = await axios.post(
        `${this.wikiUrl}/graphql`,
        { query },
        { 
          headers: this.headers,
          timeout: 15000,
          proxy: false,
          family: 4
        }
      );

      if (response.data.errors) {
        console.error('βœ— GraphQL API error:', response.data.errors[0].message);
        return false;
      }

      console.log('βœ“ Successfully connected to Wiki.js GraphQL API\n');
      return true;
    } catch (error) {
      console.error('\nβœ— Failed to connect:', error.message);
      return false;
    }
  }
}

/**
 * Interactive prompt
 */
function promptInput(message) {
  return new Promise((resolve) => {
    const readline = require('readline');
    const rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout
    });
    
    rl.question(message, (answer) => {
      rl.close();
      resolve(answer.trim().toLowerCase());
    });
  });
}

/**
 * Main function
 */
async function main() {
  console.log('======================================');
  console.log('   Wiki.js Clean & Import Tool       ');
  console.log('======================================\n');

  if (!API_TOKEN) {
    console.error('❌ Error: WIKIJS_TOKEN not set!\n');
    process.exit(1);
  }

  if (!fs.existsSync(INPUT_DIR)) {
    console.error(`❌ Error: Input directory not found: ${INPUT_DIR}\n`);
    process.exit(1);
  }

  console.log(`Wiki.js URL: ${WIKI_URL}`);
  console.log(`Input directory: ${INPUT_DIR}\n`);

  const manager = new WikiJSManager(WIKI_URL, API_TOKEN);

  // Test connection
  const connected = await manager.testConnection();
  if (!connected) {
    process.exit(1);
  }

  // Ask what to do
  console.log('What would you like to do?\n');
  console.log('  1) Delete all pages only');
  console.log('  2) Import workspaces as combined pages (keep existing)');
  console.log('  3) Delete all + Import as combined pages (fresh start)');
  console.log('  4) Cancel\n');

  const choice = await promptInput('Enter choice (1-4): ');

  if (choice === '1') {
    const confirm = await promptInput('⚠️  Delete ALL pages? This cannot be undone! (yes/no): ');
    if (confirm === 'yes') {
      await manager.deleteAllPages();
    } else {
      console.log('Cancelled.');
    }
  } else if (choice === '2') {
    await manager.importAllAsCombined(INPUT_DIR);
  } else if (choice === '3') {
    const confirm = await promptInput('⚠️  Delete ALL existing pages and import fresh? (yes/no): ');
    if (confirm === 'yes') {
      await manager.deleteAllPages();
      await manager.importAllAsCombined(INPUT_DIR);
    } else {
      console.log('Cancelled.');
    }
  } else {
    console.log('Cancelled.');
  }
}

if (require.main === module) {
  main().catch(error => {
    console.error('\n❌ Fatal error:', error.message);
    process.exit(1);
  });
}

module.exports = WikiJSManager;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment