@alvincrespo/hashnode-content-converter - v0.2.2
    Preparing search index...

    Advanced Usage

    This guide covers advanced features for customizing the conversion process.

    The Converter class extends EventEmitter, allowing fine-grained control:

    import { Converter } from '@alvincrespo/hashnode-content-converter';

    const converter = new Converter();

    // Track progress with custom UI
    let startTime: number;

    converter.on('conversion-starting', ({ post, index, total }) => {
    if (index === 1) {
    startTime = Date.now();
    console.log(`Starting conversion of ${total} posts...`);
    }
    const percent = Math.round((index / total) * 100);
    process.stdout.write(`\r[${percent}%] ${post.title.slice(0, 40)}...`);
    });

    converter.on('conversion-completed', ({ result, index, total, durationMs }) => {
    // Update progress bar, log to file, etc.
    console.log(`Completed ${result.slug} in ${durationMs}ms`);
    });

    converter.on('image-downloaded', ({ filename, postSlug, success, error }) => {
    // Track image downloads for analytics
    if (!success) {
    console.warn(`Failed to download ${filename} for ${postSlug}: ${error}`);
    }
    });

    converter.on('conversion-error', ({ type, slug, message }) => {
    // Send to error tracking service
    console.error(`[${type}] ${slug}: ${message}`);
    });

    const result = await converter.convertAllPosts('./export.json', './blog');

    For maximum flexibility, use the processors directly:

    Extracts metadata from Hashnode posts:

    import { PostParser } from '@alvincrespo/hashnode-content-converter';

    const parser = new PostParser();
    const metadata = parser.parse(hashnodePost);

    // metadata contains:
    // - title, slug, dateAdded, brief
    // - contentMarkdown
    // - coverImage (optional), tags (optional)

    Cleans Hashnode-specific markdown quirks:

    import { MarkdownTransformer } from '@alvincrespo/hashnode-content-converter';

    const transformer = new MarkdownTransformer({
    removeAlignAttributes: true, // Remove align="center" etc.
    trimWhitespace: true, // Clean up extra whitespace
    });

    const cleanMarkdown = transformer.transform(rawMarkdown);

    Downloads and localizes images:

    import {
    ImageProcessor,
    ImageDownloader,
    } from '@alvincrespo/hashnode-content-converter';

    const downloader = new ImageDownloader({
    maxRetries: 3,
    retryDelayMs: 1000,
    });

    const processor = new ImageProcessor(downloader);

    const result = await processor.process(markdown, './images');
    // result.markdown - Updated markdown with local image paths
    // result.images - Array of downloaded image info
    // result.errors - Array of failed downloads

    Generates YAML frontmatter:

    import { FrontmatterGenerator } from '@alvincrespo/hashnode-content-converter';

    const generator = new FrontmatterGenerator();
    const frontmatter = generator.generate({
    title: 'My Post',
    slug: 'my-post',
    date: new Date().toISOString(),
    tags: ['javascript', 'tutorial'],
    });

    // Returns:
    // ---
    // title: "My Post"
    // slug: "my-post"
    // date: "2024-01-15T10:30:00.000Z"
    // tags:
    // - javascript
    // - tutorial
    // ---

    Build your own conversion pipeline:

    import {
    PostParser,
    MarkdownTransformer,
    ImageProcessor,
    FrontmatterGenerator,
    FileWriter,
    ImageDownloader,
    } from '@alvincrespo/hashnode-content-converter';
    import { readFile } from 'fs/promises';

    async function customConvert(exportPath: string, outputDir: string) {
    // Load export
    const exportData = JSON.parse(await readFile(exportPath, 'utf-8'));

    // Initialize processors
    const parser = new PostParser();
    const transformer = new MarkdownTransformer();
    const imageProcessor = new ImageProcessor(new ImageDownloader());
    const frontmatter = new FrontmatterGenerator();
    const writer = new FileWriter();

    for (const post of exportData.posts) {
    // 1. Parse metadata
    const metadata = parser.parse(post);

    // 2. Transform markdown
    let markdown = transformer.transform(post.contentMarkdown);

    // 3. Custom transformation (example: add disclaimer)
    markdown = addDisclaimer(markdown);

    // 4. Process images
    const imageResult = await imageProcessor.process(
    markdown,
    `${outputDir}/${metadata.slug}/images`
    );
    markdown = imageResult.markdown;

    // 5. Generate frontmatter
    const yaml = frontmatter.generate(metadata);

    // 6. Combine and write
    const content = `${yaml}\n${markdown}`;
    await writer.write(`${outputDir}/${metadata.slug}/index.md`, content);
    }
    }

    function addDisclaimer(markdown: string): string {
    return `> This post was migrated from Hashnode.\n\n${markdown}`;
    }

    Configure image downloading behavior:

    import {
    ImageDownloader,
    ImageDownloadConfig,
    } from '@alvincrespo/hashnode-content-converter';

    const config: ImageDownloadConfig = {
    maxRetries: 5, // Retry failed downloads
    retryDelayMs: 2000, // Wait between retries
    timeoutMs: 30000, // Request timeout
    };

    const downloader = new ImageDownloader(config);

    Some Hashnode images may return 403 errors (access denied). The converter tracks these separately:

    import { Converter } from '@alvincrespo/hashnode-content-converter';

    const converter = new Converter();

    converter.on('image-downloaded', ({ filename, postSlug, success, error, is403 }) => {
    if (success) {
    // Ignore successful downloads
    return;
    }

    if (!success) {
    if (is403) {
    // Image is permanently inaccessible
    console.log(`Access denied: ${filename} in ${postSlug}`);
    // Consider using a placeholder image
    } else {
    // Transient error, might work on retry
    console.log(`Download failed: ${filename} - ${error}`);
    }
    }
    });

    Use the built-in logger for tracking:

    import { Logger } from '@alvincrespo/hashnode-content-converter';

    const logger = new Logger({
    filePath: './conversion.log', // Optional: auto-generates if not provided
    verbosity: 'normal', // 'quiet' | 'normal' | 'verbose'
    });

    // Log conversion events
    logger.info('Starting conversion');
    logger.warn('Skipping draft post');
    logger.error(`Failed to download image: ${url}`);
    logger.success('Post converted successfully');

    // Track HTTP 403 errors separately (for detailed reporting)
    logger.trackHttp403(slug, filename, url);

    // Write summary at end of conversion
    logger.writeSummary(converted, skipped, errors);

    // Close file stream when done
    await logger.close();
    import { Converter } from '@alvincrespo/hashnode-content-converter';

    // Convert to Astro content collection format
    await Converter.fromExportFile(
    './hashnode-export.json',
    './src/content/blog'
    );
    import { Converter } from '@alvincrespo/hashnode-content-converter';

    // Convert to Next.js MDX format
    await Converter.fromExportFile(
    './hashnode-export.json',
    './content/posts'
    );
    import { Converter } from '@alvincrespo/hashnode-content-converter';

    // Convert to Gatsby blog format
    await Converter.fromExportFile(
    './hashnode-export.json',
    './content/blog'
    );