A Python CLI tool for converting Hashnode blog exports to 11ty-compatible format. This is a one-time migration tool to move your Hashnode data to 11ty.
- Parse Hashnode exports - Load and validate JSON export files
- API enrichment - Fetch missing tag names, series info via Hashnode GraphQL API
- Content transformation - Convert to 11ty markdown with proper frontmatter
- Image processing - Download image from Hashnode server and organize locally
- Offline capability - Works without API using fallback mode
- Progress tracking - Visual progress bars and detailed logging
- Dry run mode - Preview changes without writing files
# Create virtual environment
python3 -m venv env
source env/bin/activate # On Windows: env\Scripts\activate
# Install dependencies
pip install -r requirements.txtπ― For best results, set up API access first - this converts tag IDs to readable names!
# Copy the example environment file
cp .env.example .env
# Edit .env and add your API key
# Get your API key at: https://hashnode.com/settings/developerYour .env file should look like:
HASHNODE_API_KEY=your_actual_api_key_here--skip-enrichment flag.
- Go to your Hashnode Dashboard
- Navigate to Blog Settings β Export
- Click "Download all your articles" to get your blog data as JSON
- Save the file in this directory (e.g.,
my-export.json)
# Quick test with 2 posts (recommended first run)
python h2e.py my-export.json --output ./test --limit 2 --dry-run
# Full conversion with API enrichment
python h2e.py my-export.json --output ./blog
# Offline mode (no API key needed)
python h2e.py my-export.json --output ./blog --skip-enrichment --skip-imagespython h2e.py [EXPORT_FILE] [OPTIONS]--output, -o PATH- Output directory (default:./output)--limit, -l N- Limit posts for testing (e.g.,--limit 5)--skip-enrichment- Skip API calls (tags show as IDs)--skip-images- Skip image downloads (use remote URLs)--api-key TEXT- Hashnode API key (overrides env var)--dry-run- Preview without writing files--verbose, -v- Enable detailed output
# Test with sample data (uses included example)
python h2e.py hashnode-export-example.json --output ./test --limit 2 --dry-run
# Test your export (recommended workflow)
python h2e.py my-export.json --output ./test --limit 2 --skip-enrichment --skip-images --dry-run
# Full test with API but no images
python h2e.py my-export.json --output ./test --limit 5 --skip-images --dry-run
# Production conversion
python h2e.py my-export.json --output ./my-blogThe tool generates 11ty-compatible files:
output/
βββ content/
β βββ posts/
β βββ posts.json # Collection configuration
β βββ my-first-post.md # Individual post files
β βββ another-post.md
βββ _data/
β βββ metadata.json # Site metadata
β βββ allTags.json # Tags data
β βββ allSeries.json # Series data
βββ images/ # Only if images downloaded
βββ posts/
βββ my-first-post/ # Post-specific images
β βββ cover.jpg
β βββ content_1.png
βββ another-post/
βββ cover.jpg
Each post gets proper 11ty frontmatter:
---
title: "My Blog Post"
date: 2024-01-15
permalink: "/my-blog-post/"
layout: "post"
excerpt: "A brief description of the post"
coverImage: "/images/posts/my-blog-post/cover.jpg"
readTime: 8
tags: ["JavaScript", "Tutorial"]
series: "Web Development Basics"
---
Post content in clean markdown...# Remove test outputs
rm -rf test* output* blog*
# Clean Python cache
find . -name "__pycache__" -delete
find . -name "*.pyc" -delete# Use the included reset script
./reset.shWhen you provide a Hashnode API key:
- β Converts tag IDs to readable names (e.g., "JavaScript" instead of "6547328...")
- β Fetches series descriptions and metadata
- β Gets accurate publication information
- β Ensures data consistency
Using --skip-enrichment:
β οΈ Tags show as shortened IDs (e.g., "Tag-6547328")β οΈ Series info is limited to basic data- β All other features work normally
- β Faster processing, works offline
- Downloads cover images and content images
- Organizes them into
images/posts/{slug}/structure - Updates markdown to use local paths
- Shows download progress and statistics
- Handles failed downloads gracefully
- Uses original remote URLs from Hashnode
- No local image storage
- Faster processing
- Depends on Hashnode servers remaining online
β "HASHNODE_API_KEY environment variable is required"
# Problem: No API key set
# Solution: Either set key or skip enrichment
echo "HASHNODE_API_KEY=your_key" > .env
# OR
python h2e.py export.json --skip-enrichmentβ "GraphQL errors" or "400 Client Error"
# Problem: API key invalid or posts not accessible
# Solution: Use fallback mode
python h2e.py export.json --skip-enrichmentFound a bug or have a feature request? Feel free to submit an issue.
MIT License - feel free to use and modify for your blog migration needs.