Tutorial

Zero-Friction Migration: How to Import Your Existing Developer Blog into ZyVOP

Import from DEV, Hashnode, and Medium—or migrate Ghost and Substack through RSS and Hugo or Astro through Markdown ZIP, with canonical and image preservation where supported.

·
5 min read
·
Human-written

AI tooling disclosure

The author reports no meaningful generative-AI assistance.

Series

Building ZyVOP in Public

Part 13 of 13Latest

Prev
Next
Zero-Friction Migration: How to Import Your Existing Developer Blog into ZyVOP

Technical writing is one of the highest-leverage assets in software engineering. Over time, however, developer portfolios become fragmented: early articles on DEV, corporate posts on Medium, a custom domain on Hashnode, or Markdown files in static site generators like Hugo, Astro, or Ghost.

Switching platforms often leads to broken syntax highlighting, mangled math formulas, orphan image references, duplicate cover photos, and canonical SEO dilution.

ZyVOP was built as a sovereign developer publishing engine: write once, maintain canonical ownership, and distribute across ecosystems. But migrations are never one-size-fits-all.

This guide provides an honest, technical look at how ZyVOP's import engine works, what is natively supported versus partially adapted, and how to migrate your catalog cleanly.


1. Migration Support Matrix

Not every platform exposes identical APIs or export formats. ZyVOP supports migrations through four distinct ingestion pipelines:

Source

Migration Pathway

Level of Support

Key Technical Nuances

DEV.to

Native REST API

First-Class

Pulls published articles, markdown body, tags, canonical URLs, and original dates.

Hashnode

GraphQL API / Backup JSON

First-Class

Supports custom publication domains, backup JSON files, and RSS fallback.

Medium

Public RSS / Single URL

Partial

Ingests via public user feed; image re-hosting is disabled by default to avoid rate limits.

Ghost

Generic RSS / Single URL

Partial

Full text via RSS content:encoded. Native Ghost JSON archives are not yet parsed.

Substack

Generic RSS / Single URL

Partial

Imports published posts via RSS. Subscriber lists and paid tiers are not imported.

Hugo / Jekyll

Markdown ZIP Archive

Partial

Ingests .md and .markdown files with relative image re-hosting. Shortcodes require flattening.

Astro / Next.js

Markdown ZIP Archive

Partial

Supports standard Markdown. .mdx files and JSX/Astro components are ignored.


2. Ingestion Pipelines: How It Works Under the Hood

flowchart TD
    subgraph Ingestion ["1. Ingress Pipeline"]
        DEV["DEV REST API"] --> API["Direct API Parser"]
        HN["Hashnode GraphQL / JSON"] --> API
        RSS["RSS Feeds (Ghost / Substack / Medium)"] --> FeedParser["RSS 2.0 / Atom Engine"]
        WEB["Single Article URL"] --> Readability["Mozilla Readability + DOM Filter"]
        ZIP["Markdown ZIP (Hugo / Astro)"] --> ZipReader["Archive Extractor (.md / .markdown)"]
    end

    subgraph Processing ["2. Normalization & Asset Engine"]
        API --> Norm["Content Normalizer"]
        FeedParser --> Norm
        Readability --> Sharp["Perceptual Cover Deduplication (Sharp)"] --> Norm
        ZipReader --> AssetRewrite["Relative Asset Re-hosting (S3/Cloud)"] --> Norm
    end

    subgraph Storage ["3. Database & Canonical Sync"]
        Norm --> Dedup{"Duplicate Check\n(Canonical URL / Title)"}
        Dedup -->|New Post| Draft["Saved Draft + Canonical Preservation"]
        Dedup -->|Exists| Skip["Skipped"]
    end

A. Direct APIs (DEV & Hashnode)

  • DEV.to: Connects via username or API key. Extracts raw Markdown directly, preserving original syntax fences, tags, and canonical links.

  • Hashnode: Queries Hashnode's GraphQL endpoint across either personal subdomains (user.hashnode.dev) or custom publication apex domains. Also accepts native Hashnode JSON export backups (readHashnodeJson).

B. Universal RSS / Atom (Ghost, Substack, Medium)

  • Parses standard RSS 2.0, Atom, and content:encoded payloads.

  • Image Re-hosting Nuance: For bulk RSS imports, image re-hosting is disabled by default (migrateImages = false) to prevent timeout failures and remote CDN rate-limiting across large archives. Remote image URLs are preserved as absolute URLs.

C. Direct Web Article Extractor

  • For standalone articles, ZyVOP executes an outbound fetch with Mozilla Readability, stripping boilerplate noise (.cookie-banner, .advertisement, .newsletter-signup, nav, footer).

  • Perceptual Image Deduplication (Sharp): Many CMSs duplicate the social og:image as the first image in the post body. The single-URL extractor creates a 32×32 sRGB luminance fingerprint of the cover image and compares it against body images:

// Bounded sRGB luminance and chromatic mean comparison in article-import-images.ts
async function createImageFingerprint(data: Buffer): Promise<ImageFingerprint> {
  const pixels = await sharp(data, { limitInputPixels: 40_000_000 })
    .resize(32, 32, { fit: 'fill' })
    .toColourspace('srgb')
    .removeAlpha()
    .raw()
    .toBuffer();
  // Evaluates luminance array and chromatic delta to strip duplicate hero images
}

D. Static-Site Markdown Archives (ZIP)

When migrating from static site generators like Hugo, Astro, Jekyll, or Obsidian:

  • Supported Formats: The reader accepts .md and .markdown files. Files with .mdx extensions or custom templates (.astro, .html) are ignored.

  • Asset Re-hosting: References like ./images/diagram.png or ../assets/flowchart.webp are normalized through resolveArchiveAssetPath, uploaded to persistent storage, and rewritten in the Markdown body.

  • Frontmatter Ingestion: Maps title, canonical_url (or canonicalUrl, url), cover_image, tags, and published_at.


3. Duplicate Detection: Preventing Content Overwrites

ZyVOP avoids duplicate drafts during repeated syncs or CLI runs. Rather than relying on fragile hashes, the database executes a scoped uniqueness query:

// Scoped duplicate check in markdown-zip-import.service.ts
if (canonicalUrl) {
  const normalized = canonicalUrl.replace(/[?#].*$/, '').replace(/\/+$/, '');
  query.andWhere(
    '(post.canonicalUrl = :canonicalUrl OR post.canonicalUrl = :normalized OR post.canonicalUrl = :slash)',
    { canonicalUrl, normalized, slash: `${normalized}/` },
  );
} else {
  query.andWhere('LOWER(TRIM(post.title)) = LOWER(TRIM(:title))', { title });
}

If an article matches an existing post owned by the author, it is safely skipped rather than duplicated or overwritten.


4. How to Migrate: Step-by-Step

Option 1: Web Dashboard (Zero Installation)

  1. Go to Dashboard → Migrate (/migrate).

  2. Select your source:

    • DEV.to: Enter your username (devto/yourusername).

    • Hashnode: Enter your publication domain (blog.yourdomain.com).

    • Medium / Ghost / Substack: Paste your public profile or RSS feed URL (https://yourblog.com/rss.xml).

    • Single URL: Paste a single article link to test readability extraction.

  3. Preview extracted articles, tags, and publication dates before committing them as drafts.

Option 2: Uploading a Static Site ZIP

  1. In your Hugo/Astro/Jekyll repository, locate your Markdown directory.

  2. Ensure posts use standard Markdown (.md or .markdown). Convert .mdx component imports into standard code fences or markdown blocks.

  3. Zip the directory containing your posts and local image assets:

    zip -r blog-migration.zip content/posts/ assets/images/
  4. Upload via Dashboard → Import → Upload Markdown ZIP.

Option 3: Terminal Migration via the CLI (zyvop import)

For developers who prefer terminal workflows:

# Authenticate
npx zyvop login

# Import from DEV
npx zyvop import devto yourusername

# Import from Hashnode
npx zyvop import hashnode blog.yourdomain.com

5. Preflight Audits: The ZyVOP Compatibility Lab

Before publishing or cross-posting imported drafts, inspect them in the ZyVOP Compatibility Lab (/compatibility-lab).

The Compatibility Lab parses your article in-browser without modifying the database and runs preflight checks across destinations:

  • Tag Truncation: Flags when your post exceeds destination limits (4 tags on DEV, 5 on Hashnode and Medium).

  • Social Announcement Limits: Checks title and excerpt lengths against announcement constraints (180 characters for Bluesky, 320 for Mastodon).

  • Local Asset Detection: Warns if an imported article still references non-public image paths (localhost or un-migrated relative paths).

  • Syntax Adaptation: Reports how Mermaid diagrams and KaTeX formulas will adapt across each destination.


6. Post-Migration: Establishing Sovereign GitOps

Once imported into ZyVOP, your articles are no longer locked into any single platform:

  1. Sovereign Canonical URL: Your ZyVOP publication acts as the primary authoritative source (<link rel="canonical">).

  2. Multi-Platform Distribution: Syndicate updates to DEV, Hashnode, Medium, and WordPress with one click or via npx zyvop publish.

  3. Automated Git Backup: Connect your GitHub account to have every post automatically committed to your personal repository via GitHub's Git Database API.

Take sovereign ownership of your technical writing today without losing your hard-earned SEO equity or past article catalog.

Series

Building ZyVOP in Public

Part 13 of 13Latest

Prev
Next

Comments(0)

Join the discussion by logging into your account.

No comments yet. Be the first to comment!

Sanju Singh

Passionate developer sharing knowledge about modern web technologies and best practices.

Subscribe to Sanju Singh's Newsletter

Direct email dispatches when new stories are published. Zero algorithms.