Migration notes

Notes to myself, because migrations are a thing I do

Migration notes

I'm capturing some notes from migrations, as the documentation doesn't quite cover any of the complexities of doing a real-life migration to Ghost, from a variety of platforms. The way this usually goes, I'll find this post while searching for something I'm stuck on, six months from now when I've forgotten it. But meanwhile, maybe it'll be useful to you, too!

What anyone doing a non-trivial migration should know:

GitHub - TryGhost/migrate
Contribute to TryGhost/migrate development by creating an account on GitHub.

Lots of command line tools, focused specifically on migrations

gctools/README.md at main · TryGhost/gctools
Command line utilities for working with Ghost content - TryGhost/gctools

Not specifically for migration, but solves a lot of migration-related problems.

Many packages import Ghost content as mobiledoc. This is a problem if you need to interact with the content and are expecting lexical from the API. Here's how to convert it:

async function convertToLexical(postId, updatedAt) {
    try {
        // Construct the API URL with query parameter
        const baseUrl = process.env.API_URL.replace(/\/$/, '');
        const url = `${baseUrl}/ghost/api/admin/posts/${postId}?convert_to_lexical=true`;
        
        // Prepare the request body with only updated_at
        // Ghost Admin API expects { posts: [{ ... }] } format
        const body = JSON.stringify({
            posts: [{
                id: postId,
                updated_at: updatedAt
            }]
        });
        
        // Generate JWT token for authentication
        const token = generateGhostToken();
        const authHeader = `Ghost ${token}`;
        
        // Make the PUT request
        const response = await fetch(url, {
            method: 'PUT',
            headers: {
                'Authorization': authHeader,
                'Content-Type': 'application/json'
            },
            body: body
        });
        
        if (!response.ok) {
            const errorText = await response.text();
            throw new Error(`HTTP ${response.status}: ${errorText}`);
        }
        
        console.log(`✓ Converted post ${postId} to lexical`);
        return true;
    } catch (error) {
        console.error(`Error converting post ${postId}:`, error.message);
        return false;
    }
}

Another option for converting between mobiledoc and lexical is hidden away in Ghost's Koenig package:

Koenig/packages/kg-converters/lib at main · TryGhost/Koenig
Components of Ghost’s Editor. Contribute to TryGhost/Koenig development by creating an account on GitHub.

Super big thanks to Kevin on the Ghost dev team for pointing both of the conversion options above out. I was losing my mind.

More to come!


Hey, before you go... If your finances allow you to keep this tea-drinking ghost and the freelancer behind her supplied with our hot beverage of choice, we'd both appreciate it!