UNPKG

@knowcode/doc-builder

Version:

Reusable documentation builder for markdown-based sites with Vercel deployment support

1,200 lines (1,044 loc) • 69.2 kB
#!/usr/bin/env node const { program } = require('commander'); const chalk = require('chalk'); const prompts = require('prompts'); const ora = require('ora'); const fs = require('fs-extra'); const path = require('path'); const { build } = require('./lib/builder'); const { startDevServer } = require('./lib/dev-server'); const { deployToVercel, setupVercelProject, prepareDeployment } = require('./lib/deploy'); const { loadConfig, createDefaultConfig } = require('./lib/config'); const { generateDescription } = require('./lib/seo'); const { execSync } = require('child_process'); const matter = require('gray-matter'); const SupabaseAuth = require('./lib/supabase-auth'); // Package info const packageJson = require('./package.json'); program .name('doc-builder') .description(packageJson.description) .version(packageJson.version) .addHelpText('before', ` ${chalk.cyan('šŸš€ @knowcode/doc-builder')} - Transform your markdown into beautiful documentation sites ${chalk.bgGreen.black(' TL;DR ')} ${chalk.green('Just run:')} ${chalk.cyan.bold('npx @knowcode/doc-builder@latest deploy')} ${chalk.green('→ Your docs are live on Vercel!')} ${chalk.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')} ${chalk.yellow('What it does:')} • Converts markdown files to static HTML with a beautiful Notion-inspired theme • Automatically generates navigation from your folder structure • Supports mermaid diagrams, syntax highlighting, and dark mode • Deploys to Vercel with one command (zero configuration) • ${chalk.green.bold('NEW:')} Shows help by default, use 'deploy' to publish (v1.3.0+) • Optional authentication to protect private documentation • Handles files with non-printable characters safely (v1.9.26+) ${chalk.yellow('Requirements:')} • Node.js 14+ installed • A ${chalk.cyan('docs/')} folder with markdown files (or specify custom folder) • Vercel CLI for deployment (optional) ${chalk.yellow('Quick Start:')} ${chalk.cyan('1. Create your docs:')} ${chalk.gray('$')} mkdir docs ${chalk.gray('$')} echo "# My Documentation" > docs/README.md ${chalk.cyan('2. Build and deploy:')} ${chalk.gray('$')} npx @knowcode/doc-builder@latest ${chalk.gray('# Show help and available commands')} ${chalk.gray('$')} npx @knowcode/doc-builder@latest deploy ${chalk.gray('# Build and deploy to production')} ${chalk.gray('$')} npx @knowcode/doc-builder@latest build ${chalk.gray('# Build HTML files only')} ${chalk.yellow('Troubleshooting npx cache issues:')} ${chalk.red('If you see an old version after updating:')} ${chalk.gray('$')} npx clear-npx-cache ${chalk.gray('# Clear the npx cache')} ${chalk.gray('$')} npx @knowcode/doc-builder@latest ${chalk.gray('# Force latest version')} ${chalk.gray('$')} npx @knowcode/doc-builder@latest dev ${chalk.gray('# Start development server')} ${chalk.yellow('No docs folder yet?')} ${chalk.gray('$')} npx @knowcode/doc-builder@latest init --example ${chalk.gray('# Create example docs')} `); // Build command program .command('build') .description('Build the documentation site to static HTML') .option('-c, --config <path>', 'path to config file (default: doc-builder.config.js)') .option('-i, --input <dir>', 'input directory containing markdown files (default: docs)') .option('-o, --output <dir>', 'output directory for HTML files (default: html)') .option('--preset <preset>', 'use a preset configuration (available: notion-inspired)') .option('--legacy', 'use legacy mode for backward compatibility') .option('--no-changelog', 'disable automatic changelog generation') .option('--no-pdf', 'hide PDF download icon in header') .option('--menu-closed', 'start with navigation menu closed') .option('--no-auth', 'build without authentication (for public sites)') .option('--no-attachments', 'skip copying attachment files (Excel, PDF, etc.)') .option('--no-static', 'disable static output generation (default: enabled)') .option('--static-dir <dir>', 'directory for static output (default: html-static)') .addHelpText('after', ` ${chalk.yellow('Examples:')} ${chalk.gray('$')} doc-builder build ${chalk.gray('# Build with defaults (includes static)')} ${chalk.gray('$')} doc-builder build --input docs --output dist ${chalk.gray('$')} doc-builder build --preset notion-inspired ${chalk.gray('# Use Notion-inspired preset')} ${chalk.gray('$')} doc-builder build --config my-config.js ${chalk.gray('# Use custom config')} ${chalk.gray('$')} doc-builder build --no-auth ${chalk.gray('# Build public site without authentication')} ${chalk.gray('$')} doc-builder build --no-static ${chalk.gray('# Skip static output generation')} ${chalk.gray('$')} doc-builder build --static-dir public ${chalk.gray('# Use custom static output directory')} ${chalk.yellow('File Safety:')} Files with non-printable characters in their names are automatically skipped to prevent build failures. You'll see warnings for any problematic files. `) .action(async (options) => { const spinner = ora('Building documentation...').start(); try { const config = await loadConfig(options.config || 'doc-builder.config.js', { ...options, command: 'build' }); // Handle no-auth option if (options.auth === false) { // Temporarily disable authentication for this build config.features = config.features || {}; config.features.authentication = false; } // Handle static output options if (options.static === false) { config.features = config.features || {}; config.features.staticOutput = false; } if (options.staticDir) { config.staticOutputDir = options.staticDir; } await build(config); spinner.succeed('Documentation built successfully!'); } catch (error) { spinner.fail('Build failed'); console.error(chalk.red(error.message)); if (error.stack) { console.error(chalk.gray(error.stack)); } process.exit(1); } }); // Dev server command program .command('dev') .description('Start development server with live reload') .option('-c, --config <path>', 'path to config file (default: doc-builder.config.js)') .option('-p, --port <port>', 'port to run dev server on (default: 3000)') .option('-h, --host <host>', 'host to bind to (default: localhost)') .option('--no-open', 'don\'t open browser automatically') .addHelpText('after', ` ${chalk.yellow('Examples:')} ${chalk.gray('$')} doc-builder dev ${chalk.gray('# Start on http://localhost:3000')} ${chalk.gray('$')} doc-builder dev --port 8080 ${chalk.gray('# Use custom port')} ${chalk.gray('$')} doc-builder dev --host 0.0.0.0 ${chalk.gray('# Allow external connections')} `) .action(async (options) => { try { const config = await loadConfig(options.config, { ...options, command: 'dev' }); await startDevServer(config, options.port); } catch (error) { console.error(chalk.red(error.message)); process.exit(1); } }); // Google Site Verification command program .command('google-verify <verification-code>') .description('Add Google site verification meta tag to all pages') .option('-c, --config <path>', 'path to config file (default: doc-builder.config.js)') .addHelpText('after', ` ${chalk.yellow('Examples:')} ${chalk.gray('$')} doc-builder google-verify FtzcDTf5BQ9K5EfnGazQkgU2U4FiN3ITzM7gHwqUAqQ ${chalk.gray('$')} doc-builder google-verify YOUR_VERIFICATION_CODE ${chalk.yellow('This will add the Google site verification meta tag to all generated HTML pages.')} ${chalk.yellow('Get your verification code from Google Search Console.')} `) .action(async (verificationCode, options) => { try { const configPath = path.join(process.cwd(), options.config || 'doc-builder.config.js'); let config; if (fs.existsSync(configPath)) { // Load existing config delete require.cache[require.resolve(configPath)]; config = require(configPath); } else { console.log(chalk.yellow('āš ļø No config file found. Creating one...')); await createDefaultConfig(); config = require(configPath); } // Ensure SEO section exists if (!config.seo) { config.seo = { enabled: true, customMetaTags: [] }; } if (!config.seo.customMetaTags) { config.seo.customMetaTags = []; } // Check if Google verification already exists const googleVerifyIndex = config.seo.customMetaTags.findIndex(tag => tag.name === 'google-site-verification' ); const newTag = { name: 'google-site-verification', content: verificationCode }; if (googleVerifyIndex >= 0) { // Update existing config.seo.customMetaTags[googleVerifyIndex] = newTag; console.log(chalk.green(`āœ… Updated Google site verification code`)); } else { // Add new config.seo.customMetaTags.push(newTag); console.log(chalk.green(`āœ… Added Google site verification code`)); } // Write updated config const configContent = `module.exports = ${JSON.stringify(config, null, 2)};\n`; fs.writeFileSync(configPath, configContent); console.log(chalk.gray(`\nThe following meta tag will be added to all pages:`)); console.log(chalk.cyan(`<meta name="google-site-verification" content="${verificationCode}" />`)); console.log(chalk.gray(`\nRun ${chalk.cyan('doc-builder build')} to regenerate your documentation.`)); } catch (error) { console.error(chalk.red('Failed to add Google verification:'), error.message); process.exit(1); } }); // Bing Site Verification command program .command('bing-verify <verification-code>') .description('Add Bing site verification meta tag to all pages') .option('-c, --config <path>', 'path to config file (default: doc-builder.config.js)') .addHelpText('after', ` ${chalk.yellow('Examples:')} ${chalk.gray('$')} doc-builder bing-verify B2D8C4C12C530D47AA962B24CAA09630 ${chalk.gray('$')} doc-builder bing-verify YOUR_VERIFICATION_CODE ${chalk.yellow('This will add the Bing site verification meta tag to all generated HTML pages.')} ${chalk.yellow('Get your verification code from Bing Webmaster Tools.')} `) .action(async (verificationCode, options) => { try { const configPath = path.join(process.cwd(), options.config || 'doc-builder.config.js'); let config; if (fs.existsSync(configPath)) { // Load existing config delete require.cache[require.resolve(configPath)]; config = require(configPath); } else { console.log(chalk.yellow('āš ļø No config file found. Creating one...')); await createDefaultConfig(); config = require(configPath); } // Ensure SEO section exists if (!config.seo) { config.seo = { enabled: true, customMetaTags: [] }; } if (!config.seo.customMetaTags) { config.seo.customMetaTags = []; } // Check if Bing verification already exists const bingVerifyIndex = config.seo.customMetaTags.findIndex(tag => tag.name === 'msvalidate.01' ); const newTag = { name: 'msvalidate.01', content: verificationCode }; if (bingVerifyIndex >= 0) { // Update existing config.seo.customMetaTags[bingVerifyIndex] = newTag; console.log(chalk.green(`āœ… Updated Bing site verification code`)); } else { // Add new config.seo.customMetaTags.push(newTag); console.log(chalk.green(`āœ… Added Bing site verification code`)); } // Write updated config const configContent = `module.exports = ${JSON.stringify(config, null, 2)};\n`; fs.writeFileSync(configPath, configContent); console.log(chalk.gray(`\nThe following meta tag will be added to all pages:`)); console.log(chalk.cyan(`<meta name="msvalidate.01" content="${verificationCode}" />`)); console.log(chalk.gray(`\nRun ${chalk.cyan('doc-builder build')} to regenerate your documentation.`)); } catch (error) { console.error(chalk.red('Failed to add Bing verification:'), error.message); process.exit(1); } }); // SEO Check command program .command('seo-check [path]') .description('Analyze SEO metadata in generated HTML files') .option('-c, --config <path>', 'path to config file (default: doc-builder.config.js)') .addHelpText('after', ` ${chalk.yellow('Examples:')} ${chalk.gray('$')} doc-builder seo-check ${chalk.gray('# Check all HTML pages')} ${chalk.gray('$')} doc-builder seo-check html/guide.html ${chalk.gray('# Check specific HTML file')} ${chalk.yellow('This command analyzes generated HTML files for:')} • Title tags and length (50-60 characters) • Meta descriptions (140-160 characters) • Keywords meta tags • Canonical URLs • H1 tags and consistency • Open Graph tags (Facebook) • Twitter Card tags • Structured data (JSON-LD) ${chalk.yellow('Note:')} Run 'doc-builder build' first to generate HTML files. `) .action(async (filePath, options) => { try { const config = await loadConfig(options.config || 'doc-builder.config.js', options); const outputDir = path.resolve(config.outputDir || 'html'); // Check if outputDir exists if (!await fs.pathExists(outputDir)) { console.error(chalk.red(`Output directory not found: ${outputDir}`)); console.log(chalk.yellow('\nPlease build your documentation first with: doc-builder build')); process.exit(1); } console.log(chalk.cyan('šŸ” Analyzing SEO in generated HTML files...')); // Get files to check let files = []; if (filePath) { const fullPath = path.resolve(filePath); if (await fs.pathExists(fullPath)) { files = [fullPath]; } else { console.error(chalk.red(`File not found: ${filePath}`)); process.exit(1); } } else { // Get all HTML files const getAllFiles = async (dir) => { const results = []; const items = await fs.readdir(dir); for (const item of items) { const fullPath = path.join(dir, item); const stat = await fs.stat(fullPath); if (stat.isDirectory() && !item.startsWith('.')) { results.push(...await getAllFiles(fullPath)); } else if (item.endsWith('.html') && item !== '404.html') { results.push(fullPath); } } return results; }; files = await getAllFiles(outputDir); } // Analyze each file const issues = []; const suggestions = []; for (const file of files) { const content = await fs.readFile(file, 'utf-8'); const relativePath = path.relative(outputDir, file); // Extract metadata from HTML const titleMatch = content.match(/<title>([^<]+)<\/title>/); const descMatch = content.match(/<meta\s+name="description"\s+content="([^"]+)"/); const keywordsMatch = content.match(/<meta\s+name="keywords"\s+content="([^"]+)"/); const canonicalMatch = content.match(/<link\s+rel="canonical"\s+href="([^"]+)"/); const h1Match = content.match(/<h1>([^<]+)<\/h1>/); const title = titleMatch ? titleMatch[1] : 'No title found'; const description = descMatch ? descMatch[1] : ''; const keywords = keywordsMatch ? keywordsMatch[1].split(',').map(k => k.trim()) : []; const canonical = canonicalMatch ? canonicalMatch[1] : ''; const h1 = h1Match ? h1Match[1] : ''; // Check for Open Graph tags const ogTitleMatch = content.match(/<meta\s+property="og:title"\s+content="([^"]+)"/); const ogDescMatch = content.match(/<meta\s+property="og:description"\s+content="([^"]+)"/); const ogImageMatch = content.match(/<meta\s+property="og:image"\s+content="([^"]+)"/); // Check for Twitter Card tags const twitterTitleMatch = content.match(/<meta\s+name="twitter:title"\s+content="([^"]+)"/); const twitterDescMatch = content.match(/<meta\s+name="twitter:description"\s+content="([^"]+)"/); // Check for structured data const structuredDataMatch = content.match(/<script\s+type="application\/ld\+json">/); // Check title const titleLength = title.length; if (!titleMatch) { issues.push({ file: relativePath, type: 'title', message: 'Missing <title> tag', severity: 'critical' }); } else if (titleLength > 60) { issues.push({ file: relativePath, type: 'title', message: `Title too long (${titleLength} chars, max 60)`, current: title, suggestion: title.substring(0, 57) + '...' }); } else if (titleLength < 30) { suggestions.push({ file: relativePath, type: 'title', message: `Title might be too short (${titleLength} chars, ideal 50-60)` }); } // Check description if (!descMatch) { issues.push({ file: relativePath, type: 'description', message: 'Missing meta description', severity: 'critical' }); } else { const descLength = description.length; if (descLength > 160) { issues.push({ file: relativePath, type: 'description', message: `Description too long (${descLength} chars, max 160)`, current: description, suggestion: description.substring(0, 157) + '...' }); } else if (descLength < 120) { suggestions.push({ file: relativePath, type: 'description', message: `Description might be too short (${descLength} chars, ideal 140-160)` }); } } // Check keywords if (!keywordsMatch || keywords.length === 0) { suggestions.push({ file: relativePath, type: 'keywords', message: 'No keywords meta tag found' }); } // Check canonical URL if (!canonicalMatch) { suggestions.push({ file: relativePath, type: 'canonical', message: 'Missing canonical URL' }); } // Check H1 if (!h1Match) { issues.push({ file: relativePath, type: 'h1', message: 'Missing H1 tag', severity: 'important' }); } else if (h1 !== title.split(' | ')[0]) { suggestions.push({ file: relativePath, type: 'h1', message: 'H1 differs from page title' }); } // Check Open Graph tags if (!ogTitleMatch || !ogDescMatch) { suggestions.push({ file: relativePath, type: 'opengraph', message: 'Missing or incomplete Open Graph tags' }); } // Check Twitter Card tags if (!twitterTitleMatch || !twitterDescMatch) { suggestions.push({ file: relativePath, type: 'twitter', message: 'Missing or incomplete Twitter Card tags' }); } // Check structured data if (!structuredDataMatch) { suggestions.push({ file: relativePath, type: 'structured-data', message: 'Missing structured data (JSON-LD)' }); } } // Display results console.log(`\n${chalk.cyan('šŸ“Š SEO Analysis Complete')}\n`); console.log(`Analyzed ${files.length} files\n`); if (issues.length === 0 && suggestions.length === 0) { console.log(chalk.green('āœ… No SEO issues found!')); } else { if (issues.length > 0) { console.log(chalk.red(`āŒ Found ${issues.length} issues:\n`)); issues.forEach(issue => { console.log(chalk.red(` ${issue.file}:`)); console.log(chalk.yellow(` ${issue.message}`)); if (issue.suggestion) { console.log(chalk.gray(` Suggestion: ${issue.suggestion.substring(0, 50)}...`)); } console.log(''); }); } if (suggestions.length > 0) { console.log(chalk.yellow(`šŸ’” ${suggestions.length} suggestions:\n`)); suggestions.forEach(suggestion => { console.log(chalk.yellow(` ${suggestion.file}: ${suggestion.message}`)); }); } console.log(`\n${chalk.cyan('šŸ’” Tips to improve SEO:')}`); console.log(' • Add front matter to markdown files to customize SEO'); console.log(' • Keep titles between 50-60 characters'); console.log(' • Write descriptions between 140-160 characters'); console.log(' • Include relevant keywords in front matter'); console.log(' • Ensure each page has a unique title and description'); console.log(`\n${chalk.gray('Add to your markdown files:')}`); console.log(chalk.gray('---')); console.log(chalk.gray('title: "Your SEO Optimized Title Here"')); console.log(chalk.gray('description: "A compelling description between 140-160 characters."')); console.log(chalk.gray('keywords: ["keyword1", "keyword2", "keyword3"]')); console.log(chalk.gray('---')); } } catch (error) { console.error(chalk.red('Failed to analyze SEO:'), error.message); process.exit(1); } }); // Set Production URL command program .command('set-production-url <url>') .description('Set the production URL to display after deployment') .option('-c, --config <path>', 'path to config file (default: doc-builder.config.js)') .addHelpText('after', ` ${chalk.yellow('Examples:')} ${chalk.gray('$')} doc-builder set-production-url doc-builder-delta.vercel.app ${chalk.gray('$')} doc-builder set-production-url https://my-custom-domain.com ${chalk.yellow('This URL will be displayed after deployment instead of auto-detected URLs.')} `) .action(async (url, options) => { try { const configPath = path.join(process.cwd(), options.config || 'doc-builder.config.js'); // Ensure URL has protocol if (!url.startsWith('http')) { url = 'https://' + url; } if (fs.existsSync(configPath)) { // Update existing config let configContent = fs.readFileSync(configPath, 'utf8'); if (configContent.includes('productionUrl:')) { // Update existing productionUrl configContent = configContent.replace( /productionUrl:\s*['"][^'"]*['"]/, `productionUrl: '${url}'` ); } else { // Add productionUrl to config configContent = configContent.replace( /module\.exports = {/, `module.exports = {\n productionUrl: '${url}',` ); } fs.writeFileSync(configPath, configContent); console.log(chalk.green(`āœ… Production URL set to: ${url}`)); console.log(chalk.gray(`\nThis URL will be displayed after deployment.`)); } else { console.log(chalk.yellow('āš ļø No config file found. Creating one...')); await createDefaultConfig(); // Add production URL to newly created config let configContent = fs.readFileSync(configPath, 'utf8'); configContent = configContent.replace( /module\.exports = {/, `module.exports = {\n productionUrl: '${url}',` ); fs.writeFileSync(configPath, configContent); console.log(chalk.green(`āœ… Created config with production URL: ${url}`)); } } catch (error) { console.error(chalk.red('Failed to set production URL:'), error.message); process.exit(1); } }); // Deploy command program .command('deploy') .description('Deploy documentation to Vercel production (requires Vercel CLI)') .option('-c, --config <path>', 'path to config file (default: doc-builder.config.js)') .option('--no-prod', 'deploy as preview instead of production') .option('--force', 'force deployment without confirmation') .option('--production-url <url>', 'override production URL for this deployment') .option('--no-auth', 'build without authentication (for public sites)') .option('--no-attachments', 'skip copying attachment files (Excel, PDF, etc.)') .addHelpText('after', ` ${chalk.yellow('Examples:')} ${chalk.gray('$')} doc-builder deploy ${chalk.gray('# Deploy to production')} ${chalk.gray('$')} doc-builder deploy --no-prod ${chalk.gray('# Deploy preview only')} ${chalk.gray('$')} doc-builder deploy --no-auth ${chalk.gray('# Deploy public site without authentication')} ${chalk.yellow('File Safety:')} Files with non-printable characters are automatically skipped during deployment to prevent YAML parsing errors. You'll see warnings for any problematic files. ${chalk.yellow('First-time Vercel Setup:')} ${chalk.cyan('1. Install Vercel CLI:')} ${chalk.gray('$')} npm install -g vercel ${chalk.gray(' or')} ${chalk.gray('$')} brew install vercel ${chalk.gray('# macOS with Homebrew')} ${chalk.cyan('2. Login to Vercel:')} ${chalk.gray('$')} vercel login ${chalk.gray(' This will open your browser to authenticate')} ${chalk.cyan('3. Run doc-builder deploy:')} ${chalk.gray('$')} npx @knowcode/doc-builder@latest deploy You'll be asked several questions: ${chalk.green('Q: Would you like to set up a new Vercel project?')} → Answer: ${chalk.yellow('Yes')} (first time only) ${chalk.green('Q: What is your project name?')} → Answer: ${chalk.yellow('Your project name')} (e.g., "my-docs" or "gasworld") ${chalk.bgRed.white.bold(' āš ļø CRITICAL: ABOUT ROOT DIRECTORY ')} ${chalk.red('If asked about Root Directory at ANY point:')} ${chalk.red('• LEAVE IT EMPTY (blank)')} ${chalk.red('• DO NOT enter "html"')} ${chalk.red('• We deploy FROM html folder already!')} ${chalk.green('Q: Which framework preset?')} → Answer: ${chalk.yellow('Other (Static HTML)')} (always choose this) ${chalk.green('Q: Make the deployment publicly accessible?')} → Answer: ${chalk.yellow('Yes')} for public docs, ${chalk.yellow('No')} for private ${chalk.gray('Then Vercel CLI will ask:')} ${chalk.green('Q: Set up [your directory]?')} → Answer: ${chalk.yellow('Yes')} ${chalk.green('Q: Which scope should contain your project?')} → Answer: ${chalk.yellow('Select your account')} (usually your username) ${chalk.green('Q: Link to existing project?')} → Answer: ${chalk.yellow('No')} (first time), ${chalk.yellow('Yes')} (if redeploying) ${chalk.gray('Note: Vercel will auto-detect that we\'re deploying from the output directory')} ${chalk.green('Q: Want to modify these settings?')} → Answer: ${chalk.yellow('No')} (doc-builder handles this) ${chalk.cyan('4. Configure Access (Important!):')} After deployment, go to your Vercel dashboard: • Navigate to Project Settings → General • Under "Deployment Protection", set to ${chalk.yellow('Disabled')} • This allows public access to your docs ${chalk.yellow('Deployment Behavior:')} ${chalk.green.bold('šŸŽÆ DEFAULT: All deployments go to PRODUCTION')} ${chalk.gray('$')} npx @knowcode/doc-builder@latest ${chalk.gray('# → Shows help (v1.3.0+)')} ${chalk.gray('$')} npx @knowcode/doc-builder@latest deploy ${chalk.gray('# → yourdocs.vercel.app')} ${chalk.gray('$')} npx @knowcode/doc-builder@latest deploy --no-prod ${chalk.gray('# → preview URL only')} ${chalk.yellow('When All Else Fails - Delete and Start Fresh:')} Sometimes the cleanest solution is to delete the Vercel project: 1. Go to your project settings on Vercel 2. Scroll to bottom and click ${chalk.red('"Delete Project"')} 3. Run: ${chalk.gray('npx @knowcode/doc-builder@latest reset-vercel')} 4. Run: ${chalk.gray('npx @knowcode/doc-builder@latest deploy')} 5. Create a NEW project with correct settings This removes all conflicting configurations! ${chalk.yellow('Troubleshooting:')} • ${chalk.cyan('Command not found:')} Install Vercel CLI globally • ${chalk.cyan('Not authenticated:')} Run ${chalk.gray('vercel login')} • ${chalk.cyan('Project not linked:')} Delete ${chalk.gray('.vercel')} folder and redeploy ${chalk.red.bold('• Path "html/html" does not exist error:')} ${chalk.yellow('This happens when Vercel has the wrong Root Directory setting.')} ${chalk.green('Fix:')} 1. Go to your Vercel project settings 2. Under "Build & Development Settings" 3. Set "Root Directory" to ${chalk.yellow.bold('empty (leave blank)')} 4. Save and redeploy ${chalk.red.bold('• Linked to wrong project (username/html):')} ${chalk.yellow('Never link to the generic "html" project!')} ${chalk.green('Fix:')} 1. Delete the ${chalk.gray('html/.vercel')} folder 2. Run deployment again 3. When asked "Link to existing project?" say ${chalk.red.bold('NO')} 4. Create a new project with your actual name ${chalk.red.bold('• "buildCommand should be string,null" error:')} ${chalk.yellow('Your Vercel project has conflicting build settings.')} ${chalk.green('Fix:')} 1. Go to project settings > Build & Development Settings 2. Clear ALL fields (Build Command, Output Directory, etc.) 3. Save and try again ${chalk.gray('OR')} Delete the project and start fresh (see below) ${chalk.red.bold('• "Project was deleted or you don\'t have access" error:')} ${chalk.yellow('The Vercel project was deleted but local config remains.')} ${chalk.green('Fix:')} ${chalk.gray('$')} npx @knowcode/doc-builder@latest reset-vercel ${chalk.gray('$')} npx @knowcode/doc-builder@latest deploy This removes old project references and starts fresh `) .action(async (options) => { const spinner = ora('Deploying to Vercel...').start(); try { const config = await loadConfig(options.config || 'doc-builder.config.js', { ...options, command: 'deploy' }); // First check if Vercel CLI is installed try { execSync('vercel --version', { stdio: 'ignore' }); } catch (vercelError) { spinner.fail('Vercel CLI not found'); console.log(chalk.yellow('\nšŸ“¦ Vercel CLI is required for deployment\n')); console.log(chalk.cyan('Install it with one of these commands:')); console.log(chalk.gray(' npm install -g vercel')); console.log(chalk.gray(' yarn global add vercel')); console.log(chalk.gray(' brew install vercel # macOS\n')); console.log(chalk.yellow('Then run deployment again:')); console.log(chalk.gray(' npx @knowcode/doc-builder@latest deploy\n')); process.exit(1); } // Handle production URL option if (options.productionUrl) { config.productionUrl = options.productionUrl.startsWith('http') ? options.productionUrl : 'https://' + options.productionUrl; } // Handle no-auth option if (options.auth === false) { // Temporarily disable authentication for this build config.features = config.features || {}; config.features.authentication = false; } // Handle no-attachments option if (options.attachments === false) { config.features = config.features || {}; config.features.attachments = false; } // Always build first spinner.stop(); console.log(chalk.blue('\nšŸ“¦ Building documentation first...\n')); await build(config); spinner.start('Deploying to Vercel...'); // Prepare deployment files await prepareDeployment(config); // Check if this is the first deployment const outputPath = path.join(process.cwd(), config.outputDir || 'html'); const vercelProjectPath = path.join(outputPath, '.vercel', 'project.json'); if (!fs.existsSync(vercelProjectPath)) { spinner.stop(); console.log(chalk.blue('\nšŸš€ First time deploying to Vercel!\n')); // Check if user is running from project root console.log(chalk.yellow('šŸ“ Before proceeding, please confirm:')); console.log(chalk.gray(' • Are you running this command from your project root directory?')); console.log(chalk.gray(' • This directory should contain your docs/ folder and config files')); console.log(chalk.gray(' • Current directory: ') + chalk.cyan(process.cwd())); console.log(); const rootConfirm = await prompts({ type: 'confirm', name: 'value', message: 'Are you running this from your project root directory?', initial: true }); if (!rootConfirm.value) { console.log(chalk.red('\nāŒ Please navigate to your project root directory first.')); console.log(chalk.gray(' Then run the deploy command again.')); console.log(chalk.gray(' Example: cd /path/to/your/project && npx @knowcode/doc-builder@latest deploy')); process.exit(1); } console.log(chalk.yellow('\nšŸ“ Important: When asked about Vercel settings:')); console.log(chalk.gray(' • Root Directory: ') + chalk.green('leave empty')); console.log(chalk.gray(' • We handle the build process for you')); console.log(); const setupConfirm = await prompts({ type: 'confirm', name: 'value', message: 'Would you like to set up a new Vercel project?', initial: true }); if (setupConfirm.value) { await setupVercelProject(config); } else { console.log(chalk.gray('Run `vercel` manually to set up your project.')); process.exit(0); } } spinner.start('Deploying to Vercel...'); // Default to production deployment const isProduction = options.prod !== false; // Default true unless explicitly --no-prod const result = await deployToVercel(config, isProduction); spinner.succeed(`Deployed successfully!`); // Handle both old and new return formats let deployUrl, productionUrl; if (typeof result === 'string') { // Old format - just a URL string deployUrl = result; productionUrl = null; } else { // New format - object with deployUrl and productionUrl deployUrl = result.deployUrl; productionUrl = result.productionUrl; } // Use the configured production URL if available, then detected, then deployment URL const displayUrl = config.productionUrl || productionUrl || deployUrl; console.log(chalk.green('\nāœ… Deployment Complete!\n')); if (isProduction) { console.log(chalk.yellow('🌐 Your documentation is live at:')); console.log(chalk.cyan.bold(` ${displayUrl}`) + chalk.gray(' (Production URL - share this!)')); console.log(); if (productionUrl && deployUrl && productionUrl !== deployUrl) { console.log(chalk.gray('This deployment also created a unique preview URL:')); console.log(chalk.gray(` ${deployUrl}`)); console.log(chalk.gray(' (This URL is specific to this deployment)')); } } else { console.log(chalk.yellow('šŸ” Preview deployment created at:')); console.log(chalk.cyan(` ${deployUrl}`)); console.log(); console.log(chalk.gray('To deploy to production, run:')); console.log(chalk.gray(' npx @knowcode/doc-builder@latest deploy')); } console.log(); } catch (error) { spinner.fail('Deployment failed'); console.error(chalk.red(error.message)); if (error.stack) { console.error(chalk.gray(error.stack)); } process.exit(1); } }); // Reset command for Vercel issues program .command('reset-vercel') .description('Reset Vercel configuration (fixes common deployment issues)') .option('-c, --config <path>', 'path to config file (default: doc-builder.config.js)') .addHelpText('after', ` ${chalk.yellow('What this does:')} • Removes .vercel folder from your output directory • Lets you set up a fresh Vercel project • Fixes "html/html does not exist" errors • Fixes wrong project linking issues ${chalk.yellow('When to use:')} • After "html/html does not exist" error • When linked to wrong project (e.g., username/html) • When Root Directory settings are incorrect • After deleting a Vercel project • When you get "Project was deleted" errors • Any time you want to start fresh with deployment `) .action(async (options) => { try { const config = await loadConfig(options.config || 'doc-builder.config.js', options); const outputPath = path.join(process.cwd(), config.outputDir || 'html'); const vercelPath = path.join(outputPath, '.vercel'); if (fs.existsSync(vercelPath)) { console.log(chalk.yellow('šŸ—‘ļø Removing .vercel folder from ' + config.outputDir + '...')); fs.removeSync(vercelPath); console.log(chalk.green('āœ… Vercel configuration reset!')); console.log(chalk.gray('\nNow run deployment again:')); console.log(chalk.cyan(' npx @knowcode/doc-builder@latest deploy')); console.log(chalk.gray('\nThis time:')); console.log(chalk.gray('• Create a NEW project (not username/html)')); console.log(chalk.gray('• Use a descriptive name like "my-docs"')); console.log(chalk.gray('• Leave Root Directory EMPTY')); } else { console.log(chalk.gray('No .vercel folder found. Ready for fresh deployment!')); } } catch (error) { console.error(chalk.red(error.message)); process.exit(1); } }); // Setup SEO command program .command('setup-seo') .description('Configure SEO settings for your documentation') .option('-c, --config <path>', 'path to config file (default: doc-builder.config.js)') .addHelpText('after', ` ${chalk.yellow('What this does:')} • Configures meta tags for search engines • Sets up social media previews (Open Graph, Twitter Cards) • Enables automatic sitemap.xml generation • Creates robots.txt for search engines • Adds structured data (JSON-LD) ${chalk.yellow('What you\'ll configure:')} • Site URL (your production URL) • Author name and organization • Twitter handle for social cards • Default keywords • Open Graph image ${chalk.yellow('After setup:')} • Run ${chalk.cyan('npx @knowcode/doc-builder@latest build')} to generate with SEO • Check meta tags in generated HTML files • Submit sitemap.xml to search engines `) .action(async (options) => { try { const configPath = path.join(process.cwd(), options.config || 'doc-builder.config.js'); let config = {}; // Load existing config if it exists if (fs.existsSync(configPath)) { try { delete require.cache[require.resolve(configPath)]; config = require(configPath); } catch (e) { console.log(chalk.yellow('āš ļø Could not load existing config, starting fresh')); } } console.log(chalk.blue('\nšŸ” SEO Setup for @knowcode/doc-builder\n')); console.log(chalk.gray('This wizard will help you configure SEO settings for better search engine visibility.\n')); // Interactive prompts const answers = await prompts([ { type: 'text', name: 'siteUrl', message: 'What is your site\'s URL?', initial: config.seo?.siteUrl || config.productionUrl || 'https://my-docs.vercel.app', validate: value => { try { new URL(value); return true; } catch { return 'Please enter a valid URL (e.g., https://example.com)'; } } }, { type: 'text', name: 'author', message: 'Author name?', initial: config.seo?.author || '' }, { type: 'text', name: 'twitterHandle', message: 'Twitter handle?', initial: config.seo?.twitterHandle || '', format: value => { if (!value) return ''; return value.startsWith('@') ? value : '@' + value; } }, { type: 'text', name: 'language', message: 'Site language?', initial: config.seo?.language || 'en-US' }, { type: 'text', name: 'organizationName', message: 'Organization name (optional)?', initial: config.seo?.organization?.name || '' }, { type: prev => prev ? 'text' : null, name: 'organizationUrl', message: 'Organization URL?', initial: config.seo?.organization?.url || '' }, { type: 'text', name: 'ogImage', message: 'Default Open Graph image URL/path?', initial: config.seo?.ogImage || '/og-default.png', hint: 'Recommended: 1200x630px PNG or JPG' }, { type: 'text', name: 'keywords', message: 'Site keywords (comma-separated)?', initial: Array.isArray(config.seo?.keywords) ? config.seo.keywords.join(', ') : 'documentation, guide, api' }, { type: 'confirm', name: 'generateSitemap', message: 'Generate sitemap.xml?', initial: config.seo?.generateSitemap !== false }, { type: 'confirm', name: 'generateRobotsTxt', message: 'Generate robots.txt?', initial: config.seo?.generateRobotsTxt !== false } ]); // Build SEO config const seoConfig = { enabled: true, siteUrl: answers.siteUrl, author: answers.author, twitterHandle: answers.twitterHandle, language: answers.language, keywords: answers.keywords.split(',').map(k => k.trim()).filter(k => k), generateSitemap: answers.generateSitemap, generateRobotsTxt: answers.generateRobotsTxt, ogImage: answers.ogImage }; // Add organization if provided if (answers.organizationName) { seoConfig.organization = { name: answers.organizationName, url: answers.organizationUrl || answers.siteUrl }; } // Update config config.seo = seoConfig; // Also update productionUrl if not set if (!config.productionUrl && answers.siteUrl) { config.productionUrl = answers.siteUrl; } // Write config file const configContent = `module.exports = ${JSON.stringify(config, null, 2)};\n`; fs.writeFileSync(configPath, configContent); console.log(chalk.green('\nāœ… SEO configuration saved to ' + path.basename(configPath))); console.log(chalk.blue('\nYour documentation will now include:')); console.log(chalk.gray('• Meta tags for search engines')); console.log(chalk.gray('• Open Graph tags for social media previews')); console.log(chalk.gray('• Twitter Card tags for Twitter sharing')); console.log(chalk.gray('• JSON-LD structured data')); if (answers.generateSitemap) { console.log(chalk.gray('• Automatic sitemap.xml generation')); } if (answers.generateRobotsTxt) { console.log(chalk.gray('• robots.txt for crawler instructions')); } console.log(chalk.yellow('\nšŸ’” Tips:')); if (answers.ogImage) { console.log(chalk.gray(`- Add an image at ${answers.ogImage} (1200x630px) for social previews`)); } console.log(chalk.gray('- Run \'npx @knowcode/doc-builder@latest build\' to generate with SEO')); console.log(chalk.gray('- Check your SEO at: https://metatags.io')); } catch (error) { console.error(chalk.red('Failed to configure SEO:'), error.message); process.exit(1); } }); // Init command program .command('init') .description('Initialize doc-builder in your project') .option('--config', 'create configuration file') .option('--example', 'create example documentation structure') .addHelpText('after', ` ${chalk.yellow('Examples:')} ${chalk.gray('$')} doc-builder init --config ${chalk.gray('# Create doc-builder.config.js')} ${chalk.gray('$')} doc-builder init --example ${chalk.gray('# Create example docs folder')} ${chalk.gray('$')} doc-builder init --example --config ${chalk.gray('# Create both')} ${chalk.yellow('What gets created:')} ${chalk.cyan('--example:')} Creates a docs/ folder with: • README.md with welcome message and mermaid diagram • getting-started.md with setup instructions • guides/configuration.md with config options ${chalk.cyan('--config:')} Creates doc-builder.config.js with: • Site name and description • Feature toggles (auth, dark mode, etc.) • Directory paths • Authentication settings `) .action(async (options) => { if (options.config) { const configPath = path.join(process.cwd(), 'doc-builder.config.js'); if (fs.existsSync(configPath)) { const overwrite = await prompts({ type: 'confirm', name: 'value', message: 'Config file already exists. Overwrite?', initial: false }); if (!overwrite.value) { console.log(chalk.gray('Cancelled.')); process.exit(0); } } const config = await createDefaultConfig(); fs.writeFileSync(configPath, `module.exports = ${JSON.stringify(config, null, 2)};`); console.log(chalk.green('āœ… Created doc-builder.config.js')); } if (options.example) { const docsDir = path.join(process.cwd(), 'docs'); if (!fs.existsSync(docsDir)) { fs.mkdirSync(docsDir, { recursive: true }); // Create example files const exampleFiles = { 'README.md': `# Welcome to Your Documentation\n\nThis is an example documentation site created with @knowcode/doc-builder.\n\n## Features\n\n- šŸ“ Write in Markdown\n- šŸŽØ Beautiful Notion-inspired design\n- šŸ“Š Mermaid diagram support\n- šŸŒ™ Dark mode\n- šŸš€ Deploy to Vercel\n\n## Getting Started\n\n1. Edit this file and add your content\n2. Create new markdown files\n3. Run \`npx @knowcode/doc-builder@latest\` to build and deploy\n\n## Example Diagram\n\n\`\`\`mermaid\ngraph TD\n A[Write Docs] --> B[Build]\n B --> C[Deploy]\n C --> D[Share]\n\`\`\`\n`, 'getting-started.md': `# Getting Started\n\n**Generated**: ${new Date().toISOString().split('T')[0]}\n**Status**: Draft\n**Verified**: ā“\n\n## Overview\n\nThis guide will help you get started with your documentation.\n\n## Installation\n\nNo installation required! Just use:\n\n\`\`\`bash\nnpx @knowcode/doc-builder@latest\n\`\`\`\n\n## Writing Documentation\n\n1. Create markdown files in the \`docs\` folder\n2. Use folders to organize your content\n3. Add front matter for metadata\n\n## Building\n\nTo build your documentation:\n\n\`\`\`bash\nnpx @knowcode/doc-builder@latest build\n\`\`\`\n\n## Deployment\n\nDeploy to Vercel:\n\n\`\`\`bash\nnpx @knowcode/doc-builder@latest deploy\n\`\`\`\n`, 'guides/configuration.md': `# Configuration Guide\n\n**Generated**: ${new Date().toISOString().split('T')[0]}\n**Status**: Draft\n**Verified**: ā“\n\n## Overview\n\n@knowcode/doc-builder works with zero configuration, but you can customize it.\n\n## Configuration File\n\nCreate \`doc-builder.config.js\`:\n\n\`\`\`javascript\nmodule.exports = {\n siteName: 'My Documentation',\n siteDescription: 'Documentation for my project',\n \n features: {\n authentication: false,\n changelog: true,\n mermaid: true,\n darkMode: true\n }\n};\n\`\`\`\n\n## Options\n\n### Site Information\n\n- \`siteName\`: Your documentation site name\n- \`siteDescription\`: Brief description\n\n### Directories\n\n- \`docsDir\`: Input directory (default: 'docs')\n- \`outputDir\`: Output directory (default: 'html')\n\n### Features\n\n- \`authentication\`: Enable Supabase authentication\n- \`changelog\`: Generate changelog automatically\n- \`mermaid\`: Support for diagrams\n- \`darkMode\`: Dark theme support\n` }; // Create example files for (const [filePath, content] of Object.entries(exampleFiles)) { const fullPath = path.join(docsDir, filePath); const dir = path.dirname(fullPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(fullPath, content); console.log(chalk.green(`āœ… Created ${filePat