{"_id":"@alvincrespo/hashnode-content-converter","_rev":"3-d81893b4bfaf58ec3556e748ec4473b6","name":"@alvincrespo/hashnode-content-converter","dist-tags":{"latest":"0.2.2"},"versions":{"0.2.0":{"name":"@alvincrespo/hashnode-content-converter","version":"0.2.0","keywords":["hashnode","content","migration","blog","markdown","converter","export"],"author":{"name":"Alvin Crespo"},"license":"MIT","_id":"@alvincrespo/hashnode-content-converter@0.2.0","maintainers":[{"name":"alvincrespo","email":"alvin.crespo@gmail.com"}],"homepage":"https://github.com/alvincrespo/hashnode-content-converter#readme","bugs":{"url":"https://github.com/alvincrespo/hashnode-content-converter/issues"},"bin":{"hashnode-converter":"dist/cli/convert.js"},"dist":{"shasum":"dc28c08246d35f2c4b3c35aa18851a3a11833dd0","tarball":"https://registry.npmjs.org/@alvincrespo/hashnode-content-converter/-/hashnode-content-converter-0.2.0.tgz","fileCount":64,"integrity":"sha512-wImTco2uwuvd7SQzWv+2khIk5inb+o2SMguXMJZcz10ZXakC4hCotoaPjWaJ9mYFI8YAPISdmt7cBHhfF1CA6A==","signatures":[{"sig":"MEUCIQC0mrpV4jqFlmaScY3FgTLr48Yon5ry54qqKhgnW0PhFAIgdShTUOovC8QRTAPag5Y3NUxLk8R9T4vb7bO3/RMkQ6o=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":212527},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":"^24.4.0"},"gitHead":"fff72c61b5c94174c810a8a4025e33f90d25e8c4","scripts":{"dev":"tsc --watch","lint":"eslint src tests --ext .ts","test":"vitest run --coverage","build":"tsc --project tsconfig.build.json","test:ui":"vitest --ui","lint:fix":"eslint src tests --ext .ts --fix","test:watch":"vitest --watch","type-check":"tsc --noEmit","test:coverage":"vitest --coverage","prepublishOnly":"npm run build && npm test","generate-issues":"npx ts-node scripts/generate-issues.ts","generate-issues:dry":"npx ts-node scripts/generate-issues.ts --dry-run"},"_npmUser":{"name":"alvincrespo","email":"alvin.crespo@gmail.com"},"repository":{"url":"git+https://github.com/alvincrespo/hashnode-content-converter.git","type":"git"},"_npmVersion":"11.4.2","description":"Convert Hashnode blog exports to framework-agnostic Markdown with YAML frontmatter","directories":{},"_nodeVersion":"24.4.0","dependencies":{"commander":"^14.0.0"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.0.0","vitest":"^4.0.0","@vitest/ui":"^4.0.0","typescript":"^5.0.0","@types/node":"^24.0.0","@vitest/coverage-v8":"^4.0.3","@typescript-eslint/parser":"^8.0.0","@typescript-eslint/eslint-plugin":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/hashnode-content-converter_0.2.0_1766502896443_0.8071950969273851","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@alvincrespo/hashnode-content-converter","version":"0.2.1","keywords":["hashnode","content","migration","blog","markdown","converter","export"],"author":{"name":"Alvin Crespo"},"license":"MIT","_id":"@alvincrespo/hashnode-content-converter@0.2.1","maintainers":[{"name":"alvincrespo","email":"alvin.crespo@gmail.com"}],"homepage":"https://github.com/alvincrespo/hashnode-content-converter#readme","bugs":{"url":"https://github.com/alvincrespo/hashnode-content-converter/issues"},"bin":{"hashnode-converter":"dist/cli/convert.js"},"dist":{"shasum":"bfd942556832dea5f97844042148b40b3ad3e49e","tarball":"https://registry.npmjs.org/@alvincrespo/hashnode-content-converter/-/hashnode-content-converter-0.2.1.tgz","fileCount":65,"integrity":"sha512-Jv3x9/gLeRwIlrvRcbB5g5fQjw4kKJJG4RhtLsnXwIhpEbak8izpzhnV/Lxy5IYSoxsxvN9Ul60/Rk4BoLA/zg==","signatures":[{"sig":"MEUCICjfrHi1C0bUqoyfJTU2Q9JWYm6iW/082CAaCMcSSwytAiEA8uJx5qwT2rzamT4YBNgwB6tKVvOk/yahzZ5pG3HPxlo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":207246},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":"^24.4.0"},"gitHead":"3c8adb5d9d8ae12d2ab97853c8e0a0be2d9bfd45","scripts":{"dev":"tsc --watch","docs":"typedoc","lint":"eslint src tests --ext .ts","test":"vitest run --coverage","build":"tsc --project tsconfig.build.json","test:ui":"vitest --ui","lint:fix":"eslint src tests --ext .ts --fix","docs:serve":"npx serve docs-site","docs:watch":"typedoc --watch","test:watch":"vitest --watch","type-check":"tsc --noEmit","test:coverage":"vitest --coverage","prepublishOnly":"npm run build && npm test","generate-issues":"npx ts-node scripts/generate-issues.ts","generate-issues:dry":"npx ts-node scripts/generate-issues.ts --dry-run"},"_npmUser":{"name":"alvincrespo","email":"alvin.crespo@gmail.com"},"repository":{"url":"git+https://github.com/alvincrespo/hashnode-content-converter.git","type":"git"},"_npmVersion":"11.4.2","description":"Convert Hashnode blog exports to framework-agnostic Markdown with YAML frontmatter","directories":{},"_nodeVersion":"24.4.0","dependencies":{"commander":"^14.0.0"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.0.0","vitest":"^4.0.0","typedoc":"^0.28.15","@vitest/ui":"^4.0.0","typescript":"^5.0.0","@types/node":"^24.0.0","@vitest/coverage-v8":"^4.0.3","@typescript-eslint/parser":"^8.0.0","@typescript-eslint/eslint-plugin":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/hashnode-content-converter_0.2.1_1767656893911_0.23883963044353784","host":"s3://npm-registry-packages-npm-production"}},"0.2.2":{"name":"@alvincrespo/hashnode-content-converter","version":"0.2.2","description":"Convert Hashnode blog exports to framework-agnostic Markdown with YAML frontmatter","type":"module","main":"dist/index.js","types":"dist/index.d.ts","bin":{"hashnode-converter":"dist/cli/convert.js"},"scripts":{"build":"tsc --project tsconfig.build.json","dev":"tsc --watch","lint":"eslint src tests --ext .ts","lint:fix":"eslint src tests --ext .ts --fix","test":"vitest run --coverage","test:watch":"vitest --watch","test:ui":"vitest --ui","test:coverage":"vitest --coverage","type-check":"tsc --noEmit","generate-issues":"npx ts-node scripts/generate-issues.ts","generate-issues:dry":"npx ts-node scripts/generate-issues.ts --dry-run","prepublishOnly":"npm run build && npm test","docs":"typedoc","docs:watch":"typedoc --watch","docs:serve":"npx serve docs-site"},"keywords":["hashnode","content","migration","blog","markdown","converter","export"],"author":{"name":"Alvin Crespo"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/alvincrespo/hashnode-content-converter.git"},"homepage":"https://github.com/alvincrespo/hashnode-content-converter#readme","bugs":{"url":"https://github.com/alvincrespo/hashnode-content-converter/issues"},"devDependencies":{"@types/node":"^24.0.0","@typescript-eslint/eslint-plugin":"^8.0.0","@typescript-eslint/parser":"^8.0.0","@vitest/coverage-v8":"^4.0.3","@vitest/ui":"^4.0.0","eslint":"^9.0.0","typedoc":"^0.28.15","typescript":"^5.0.0","vitest":"^4.0.0"},"dependencies":{"commander":"^14.0.0"},"engines":{"node":"^24.4.0"},"_id":"@alvincrespo/hashnode-content-converter@0.2.2","gitHead":"3365650ece0d9f4cdf5491e969da1a85377146a5","_nodeVersion":"24.4.0","_npmVersion":"11.4.2","dist":{"integrity":"sha512-0e161d86Yh1eNDojuOhhKNISBvnoONK0Zjt/QRjH436BkOXd2rgu2lBNw4xSfNpplM00E/ar7MeD/K9gErsyfQ==","shasum":"58ad053a5656fdfae9001540b11a5debb14b585a","tarball":"https://registry.npmjs.org/@alvincrespo/hashnode-content-converter/-/hashnode-content-converter-0.2.2.tgz","fileCount":65,"unpackedSize":207246,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDZ9eFFC1UzW63hncvIPV7cxPaot+I6iqXakw6HKDcuLAIhANkkv2iE9/yuCKq9Stkv9SGEqPdhFHonzSxX3yJA9SDw"}]},"_npmUser":{"name":"alvincrespo","email":"alvin.crespo@gmail.com"},"directories":{},"maintainers":[{"name":"alvincrespo","email":"alvin.crespo@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/hashnode-content-converter_0.2.2_1767657572878_0.14892884587214916"},"_hasShrinkwrap":false}},"time":{"created":"2025-12-23T15:14:56.358Z","modified":"2026-01-05T23:59:33.218Z","0.2.0":"2025-12-23T15:14:56.623Z","0.2.1":"2026-01-05T23:48:14.069Z","0.2.2":"2026-01-05T23:59:33.034Z"},"bugs":{"url":"https://github.com/alvincrespo/hashnode-content-converter/issues"},"author":{"name":"Alvin Crespo"},"license":"MIT","homepage":"https://github.com/alvincrespo/hashnode-content-converter#readme","keywords":["hashnode","content","migration","blog","markdown","converter","export"],"repository":{"type":"git","url":"git+https://github.com/alvincrespo/hashnode-content-converter.git"},"description":"Convert Hashnode blog exports to framework-agnostic Markdown with YAML frontmatter","maintainers":[{"name":"alvincrespo","email":"alvin.crespo@gmail.com"}],"readme":"# @alvincrespo/hashnode-content-converter\n\n[![codecov](https://codecov.io/gh/alvincrespo/hashnode-content-converter/graph/badge.svg)](https://codecov.io/gh/alvincrespo/hashnode-content-converter)\n[![Tests](https://github.com/alvincrespo/hashnode-content-converter/actions/workflows/ci.yml/badge.svg)](https://github.com/alvincrespo/hashnode-content-converter/actions/workflows/ci.yml)\n\nConvert Hashnode blog exports to framework-agnostic Markdown with YAML frontmatter. This TypeScript package transforms your Hashnode content into portable Markdown files with proper frontmatter, localized images, and cleaned formatting—ready for any static site generator or blog platform.\n\n> **Status**: Production-ready with 99.36% test coverage. All core components, CLI, and programmatic API are complete.\n\n## Features\n\n- **Metadata Extraction**: Parse Hashnode exports and extract essential post metadata (title, slug, dates, tags, cover image)\n- **Markdown Transformation**: Clean Hashnode-specific formatting quirks (align attributes, trailing whitespace)\n- **Image Localization**: Download CDN images and replace URLs with local paths\n- **Intelligent Retry**: Marker-based strategy to skip already-downloaded images and permanent failures\n- **YAML Frontmatter**: Generate framework-agnostic frontmatter from post metadata\n- **Atomic File Operations**: Safe, atomic writes with directory traversal protection\n- **Comprehensive Logging**: Dual-channel output (console + file) with detailed error tracking\n- **Type-Safe**: Full TypeScript with strict mode and comprehensive test coverage (98%+)\n\n## Installation\n\n```bash\nnpm install @alvincrespo/hashnode-content-converter\n```\n\n**Requirements**: Node.js >= 18.0.0 (Unix-like systems only: macOS, Linux)\n\n## Usage\n\n### CLI\n\nThe CLI provides a simple interface for converting Hashnode exports:\n\n```bash\n# Basic usage\nnpx @alvincrespo/hashnode-content-converter convert \\\n  --export ./hashnode/export-articles.json \\\n  --output ./blog\n\n# With all options\nnpx @alvincrespo/hashnode-content-converter convert \\\n  --export ./hashnode/export-articles.json \\\n  --output ./blog \\\n  --log-file ./conversion.log \\\n  --verbose\n\n# Overwrite existing posts (default is to skip)\nnpx @alvincrespo/hashnode-content-converter convert \\\n  --export ./export.json \\\n  --output ./blog \\\n  --no-skip-existing\n```\n\n**Options**:\n\n| Option | Short | Description | Default |\n|--------|-------|-------------|---------|\n| `--export <path>` | `-e` | Path to Hashnode export JSON file | Required |\n| `--output <path>` | `-o` | Output directory for converted posts | Required |\n| `--log-file <path>` | `-l` | Path to log file | Optional |\n| `--skip-existing` | | Skip posts that already exist | `true` |\n| `--no-skip-existing` | | Overwrite existing posts | |\n| `--verbose` | `-v` | Show detailed output including image downloads | `false` |\n| `--quiet` | `-q` | Suppress all output except errors | `false` |\n\n**Exit Codes**:\n- `0` - Conversion completed successfully\n- `1` - Conversion completed with errors, or validation failed\n\n### Programmatic API\n\n#### Quick Start\n\nThe simplest way to convert a Hashnode export:\n\n```typescript\nimport { Converter } from '@alvincrespo/hashnode-content-converter';\n\n// One-liner conversion\nconst result = await Converter.fromExportFile('./export.json', './blog');\nconsole.log(`Converted ${result.converted} posts in ${result.duration}`);\n```\n\n#### With Progress Tracking\n\nTrack conversion progress with a simple callback:\n\n```typescript\nimport { Converter } from '@alvincrespo/hashnode-content-converter';\n\nconst converter = Converter.withProgress((current, total, title) => {\n  console.log(`[${current}/${total}] Converting: ${title}`);\n});\n\nconst result = await converter.convertAllPosts('./export.json', './blog');\n```\n\n#### Full Control with Events\n\nFor complete control, use the event-driven API:\n\n```typescript\nimport { Converter } from '@alvincrespo/hashnode-content-converter';\n\nconst converter = new Converter();\n\n// Progress tracking\nconverter.on('conversion-starting', ({ index, total, post }) => {\n  console.log(`[${index}/${total}] Starting: ${post.title}`);\n});\n\nconverter.on('conversion-completed', ({ result, durationMs }) => {\n  console.log(`Completed in ${durationMs}ms: ${result.title}`);\n});\n\n// Error handling\nconverter.on('conversion-error', ({ type, slug, message }) => {\n  console.error(`[${type}] ${slug}: ${message}`);\n});\n\n// Image tracking\nconverter.on('image-downloaded', ({ filename, success, is403 }) => {\n  if (!success) console.warn(`Failed to download: ${filename}`);\n});\n\nconst result = await converter.convertAllPosts('./export.json', './blog', {\n  skipExisting: true,\n  downloadOptions: { downloadDelayMs: 100 }\n});\n```\n\n#### Advanced: Custom Processors\n\nFor custom pipelines, use individual processors:\n\n```typescript\nimport {\n  PostParser,\n  MarkdownTransformer,\n  ImageProcessor,\n  FrontmatterGenerator,\n  FileWriter\n} from '@alvincrespo/hashnode-content-converter';\n\n// Parse metadata\nconst parser = new PostParser();\nconst metadata = parser.parse(hashnodePost);\n\n// Transform markdown\nconst transformer = new MarkdownTransformer({ trimTrailingWhitespace: true });\nconst cleanedMarkdown = transformer.transform(metadata.contentMarkdown);\n\n// Process images\nconst imageProcessor = new ImageProcessor({ downloadDelayMs: 100 });\nconst imageResult = await imageProcessor.process(cleanedMarkdown, './blog/my-post');\n\n// Generate frontmatter\nconst generator = new FrontmatterGenerator();\nconst frontmatter = generator.generate(metadata);\n\n// Write file\nconst writer = new FileWriter();\nawait writer.writePost('./blog', metadata.slug, frontmatter, imageResult.markdown);\n```\n\n## Current Status\n\nAll components are **feature-complete** with 99.36% test coverage (363 tests):\n\n| Component | Description | Coverage |\n|-----------|-------------|----------|\n| Converter | Main orchestrator with event-driven progress tracking | 99.27% |\n| PostParser | Extract metadata from Hashnode posts | 100% |\n| MarkdownTransformer | Clean Hashnode-specific formatting | 100% |\n| ImageProcessor | Download and localize images with marker-based retry | 98%+ |\n| FrontmatterGenerator | Generate YAML frontmatter from metadata | 100% |\n| ImageDownloader | HTTP downloads with retry logic and 403 tracking | 98.36% |\n| FileWriter | Atomic file operations with path validation | 97.77% |\n| Logger | Dual-channel logging with error tracking | 98.85% |\n| CLI | Command-line interface with progress display | 98%+ |\n\nSee [docs/TRANSITION.md](docs/TRANSITION.md) for the complete implementation history.\n\n## Architecture\n\nThe package uses a modular, service-oriented design with clear separation of concerns:\n\n```\nHashnode Export JSON\n    ↓\nPostParser (extract metadata)\n    ↓\nMarkdownTransformer (fix formatting)\n    ↓\nImageProcessor (download & localize images)\n    ↓\nFrontmatterGenerator (create YAML frontmatter)\n    ↓\nFileWriter (persist to disk)\n    ↓\nLogger (track results & errors)\n```\n\n**Key Directories**:\n- [src/types/](src/types/) - TypeScript interfaces and type definitions\n- [src/processors/](src/processors/) - Content transformation classes\n- [src/services/](src/services/) - Infrastructure services (HTTP, filesystem, logging)\n- [src/cli/](src/cli/) - Command-line interface\n- [tests/](tests/) - Unit and integration tests (363 tests, 99.36% coverage)\n\n## Development\n\n### Setup\n\nThis project uses nvm for Node.js version management:\n\n```bash\n# Set correct Node version\nnvm use $(cat .node-version)\n\n# Install dependencies\nnpm install\n```\n\n### Common Commands\n\n```bash\n# Build TypeScript to dist/\nnpm run build\n\n# Watch mode (auto-rebuild on changes)\nnpm run dev\n\n# Run tests with coverage\nnpm test\n\n# Watch tests\nnpm run test:watch\n\n# Interactive test dashboard\nnpm run test:ui\n\n# Type-check without emitting\nnpm run type-check\n\n# Lint code\nnpm run lint\n\n# Full pre-publication checks\nnpm run prepublishOnly\n```\n\n### Testing\n\nThe project uses Vitest with comprehensive test coverage:\n\n- **363 tests** with **99.36% code coverage**\n- **Test patterns**: AAA (Arrange-Act-Assert), mocked dependencies, comprehensive edge cases\n\n| Test Suite | Tests |\n|------------|-------|\n| Unit Tests | 305 |\n| Integration Tests | 58 |\n\n```bash\nnpm run test:coverage  # Generate detailed coverage report\n```\n\n## Releasing\n\nThis package uses GitHub Actions for automated npm publishing.\n\n### Automated Release (Recommended)\n\n1. **Update version** in `package.json`:\n   ```bash\n   npm version patch  # or minor, major\n   ```\n\n2. **Push the tag** to trigger the release workflow:\n   ```bash\n   git push origin main --tags\n   ```\n\n3. The GitHub Action will automatically:\n   - Run lint and type-check\n   - Run tests\n   - Build the package\n   - Publish to npm\n   - Create a GitHub Release with auto-generated notes\n\n### Manual Release\n\nFor manual publishing:\n\n```bash\n# Build and test\nnpm run prepublishOnly\n\n# Login to npm (first time only)\nnpm login\n\n# Publish\nnpm publish --access public\n```\n\n### Pre-release Checklist\n\n- [ ] All tests passing (`npm test`)\n- [ ] CHANGELOG.md updated with new version\n- [ ] Version bumped in package.json\n- [ ] No uncommitted changes\n\n## Migrating from convert-hashnode.js\n\nIf you're migrating from the original `convert-hashnode.js` script, here are the key differences:\n\n### Configuration Changes\n\n| Original Script | This Package |\n|-----------------|--------------|\n| Environment variables (`EXPORT_DIR`, `READ_DIR`) | CLI arguments (`--export`, `--output`) |\n| Hardcoded paths | User-specified paths |\n| Single output format | Same output format, more control |\n\n### Migration Steps\n\n1. **Install the package**:\n   ```bash\n   npm install @alvincrespo/hashnode-content-converter\n   ```\n\n2. **Replace script invocation**:\n   ```bash\n   # Old way (convert-hashnode.js)\n   EXPORT_DIR=blog READ_DIR=blog node convert-hashnode.js\n\n   # New way\n   npx @alvincrespo/hashnode-content-converter convert \\\n     --export ./hashnode/export-articles.json \\\n     --output ./blog\n   ```\n\n3. **Output format**: The generated Markdown files maintain the same structure:\n   - YAML frontmatter with title, date, description, cover image\n   - Cleaned markdown content (align attributes removed)\n   - Downloaded images in post directories\n\n### Programmatic Migration\n\nIf you were importing functions from the original JavaScript script, you can now use the new typed API:\n\n```javascript\n// Old: CommonJS JavaScript (no type information)\nconst { processPost, downloadImage } = require('./convert-hashnode');\n```\n\n```typescript\n// New: ESM TypeScript with full type support\nimport { Converter, PostParser, ImageProcessor } from '@alvincrespo/hashnode-content-converter';\n```\n\n> **Note**: This package uses ESM (ECMAScript Modules). If your project uses CommonJS, you'll need to use dynamic imports:\n> ```javascript\n> const { Converter } = await import('@alvincrespo/hashnode-content-converter');\n> ```\n\n## Documentation\n\n**API Reference**: [alvincrespo.github.io/hashnode-content-converter](https://alvincrespo.github.io/hashnode-content-converter/)\n\nAdditional documentation:\n- [Getting Started Guide](https://alvincrespo.github.io/hashnode-content-converter/documents/getting-started.html) - Installation and basic usage\n- [CLI Reference](https://alvincrespo.github.io/hashnode-content-converter/documents/cli-usage.html) - Command-line options\n- [Programmatic API](https://alvincrespo.github.io/hashnode-content-converter/documents/programmatic-api.html) - Using the converter in code\n- [Advanced Usage](https://alvincrespo.github.io/hashnode-content-converter/documents/advanced.html) - Custom processors and events\n\nInternal documentation:\n- [docs/TRANSITION.md](docs/TRANSITION.md) - Architecture and implementation history\n- [CLAUDE.md](CLAUDE.md) - Project guidelines for development\n- [docs/phases/](docs/phases/) - Phase-by-phase implementation plans\n\n## Contributing\n\nThis project follows strict TypeScript and testing standards:\n\n- **TypeScript**: Strict mode, no `any` types in critical paths\n- **Testing**: 90%+ coverage required for new implementations\n- **Documentation**: JSDoc on all public APIs\n- **Code Style**: ESLint enforced\n\nSee [CLAUDE.md](CLAUDE.md) for detailed development guidelines.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}