Migration from Hugo to Ghost

Hello people!

Is there any way to migrate from hugo to ghost?
I just found plugins for jekyll.

Check this: Migrating from Hugo to Ghost

I’m trying to use this code, but when I run, I got this error:

ode migra.js 
/Users/diego.eis/Sites/migra.js:57
    const image = imageMatches[1]
                              ^

TypeError: Cannot read properties of null (reading '1')
    at createPostDataFromFileContent (/Users/diego.eis/Sites/migra.js:57:31)
    at /Users/diego.eis/Sites/migra.js:75:41
    at Array.map (<anonymous>)
    at Object.<anonymous> (/Users/diego.eis/Sites/migra.js:75:6)
    at Module._compile (node:internal/modules/cjs/loader:1101:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:17:47

I changed the pattern of YAML instead use +++, I use in my markdowns —, so, I changed this in the Regexp of JS. But, this is the only change.

I got this error even when I don’t change the JS.
Screen Shot 2022-07-31 at 11.32.44

Thanks for the answer.

It’s also hard to understand for me. Maybe someone will help with it.

Update:
I found this script
saltfactory/node-jekyll-to-ghost: jekyll to ghost exporter (github.com)

What I did: install jekyll, copy all posts to _posts folder of jekyll and run the script.
Apparently this works fine, but, when I tried to import the json in ghost, I got an error:

Please install Ghost 1.0, import the file and then update your blog to the latest Ghost version. Visit https://ghost.org/docs/update/ or ask for help in our https://forum.ghost.org.: Detected unsupported file structure.

I’m trying to install this oldest version of Ghost, but I need to downgrade all my stack.

Any update, I change this thread.

Tried to get the old JSON and change to modern pattern, inserting DB object…
After that, I tried import the json again, and a got the error:

Value in [posts.title] cannot be blank.

Dont know what it is, but I think can be related to mobiledoc.

Tried follow the instructions here ( Importing content from other platforms to Ghost), but I got this error.

node:internal/modules/cjs/loader:959
  throw err;
  ^

Error: Cannot find module '@tryghost/mg-mediascraper'
Require stack:
- /opt/homebrew/lib/node_modules/@tryghost/migrate/sources/ghost.js
- /opt/homebrew/lib/node_modules/@tryghost/migrate/commands/ghost.js
- /opt/homebrew/lib/node_modules/@tryghost/migrate/node_modules/sywac/api.js
- /opt/homebrew/lib/node_modules/@tryghost/migrate/node_modules/@tryghost/pretty-cli/lib/pretty-cli.js
- /opt/homebrew/lib/node_modules/@tryghost/migrate/node_modules/@tryghost/pretty-cli/index.js
- /opt/homebrew/lib/node_modules/@tryghost/migrate/bin/cli.js
    at Module._resolveFilename (node:internal/modules/cjs/loader:956:15)
    at Module._load (node:internal/modules/cjs/loader:804:27)
    at Module.require (node:internal/modules/cjs/loader:1022:19)
    at require (node:internal/modules/cjs/helpers:102:18)
    at Object.<anonymous> (/opt/homebrew/lib/node_modules/@tryghost/migrate/sources/ghost.js:4:24)
    at Module._compile (node:internal/modules/cjs/loader:1120:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1174:10)
    at Module.load (node:internal/modules/cjs/loader:998:32)
    at Module._load (node:internal/modules/cjs/loader:839:12)
    at Module.require (node:internal/modules/cjs/loader:1022:19)
    at require (node:internal/modules/cjs/helpers:102:18)
    at Object.<anonymous> (/opt/homebrew/lib/node_modules/@tryghost/migrate/commands/ghost.js:1:15)
    at Module._compile (node:internal/modules/cjs/loader:1120:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1174:10)
    at Module.load (node:internal/modules/cjs/loader:998:32)
    at Module._load (node:internal/modules/cjs/loader:839:12) {
  code: 'MODULE_NOT_FOUND',
  requireStack: [
    '/opt/homebrew/lib/node_modules/@tryghost/migrate/sources/ghost.js',
    '/opt/homebrew/lib/node_modules/@tryghost/migrate/commands/ghost.js',
    '/opt/homebrew/lib/node_modules/@tryghost/migrate/node_modules/sywac/api.js',
    '/opt/homebrew/lib/node_modules/@tryghost/migrate/node_modules/@tryghost/pretty-cli/lib/pretty-cli.js',
    '/opt/homebrew/lib/node_modules/@tryghost/migrate/node_modules/@tryghost/pretty-cli/index.js',
    '/opt/homebrew/lib/node_modules/@tryghost/migrate/bin/cli.js'
  ]
}

Node.js v18.7.0

Here are some of the scripts I tried, but I got many errors… I think all area outdated, since the latest update was 4,5,6 years ago.

Well, I don’t know how to write all what I did to this work, but I finally made this.

I modified this script - https://github.com/saltfactory/node-jekyll-to-ghost to help.

Because the script is very old, I needed to solve many small problems of patters and sanitization to get the correct final json. Every time I tried to import the json to ghost, I discovery some errors pattern. I solved that one by one.

This post (Ghost and Markdown: Getting everything consistent on the backend) describe in details some of my problems. The main problem was solve the content that was not being imported to ghost. This was resolved by individually handling each of the errors when importing the json into the platform. A lot of work and a lot of small different problems.

In this post, I read the problem of \n that we need to substitute to \\n.

Some good links to read:

If you are facing the problem of content not showing in the ghost, the a-ha moment for me was treat the patter problem in that line in the script:

mobiledoc: "{\"version\":\"0.3.1\",\"atoms\":[],\"cards\":[[\"markdown\",{\"markdown\":\"" + matter.content.replaceAll("\"","_").replaceAll("\\---","---") + "\"}]],\"markups\":[],\"sections\":[[10,0],[1,\"p\",[]]]}",

Here I tried to replace and remove the parts that was breaking the import matter.content.replaceAll("\"","_").replaceAll("\\---","---")

My final script follow. I hope this can help someone in the feature.

'use strict';

const fs = require('fs');
const path = require('path');
const grayMatter = require('gray-matter');
const uuid = require('node-uuid');
const markdown = require('markdown').markdown;
const extend = require('util')._extend;


let tagList = [];
let matterList = [];

let ghostData = {
  meta: {
    exported_on: Date.now(),
    version: "5.7.0"
  },
  data: {
    posts: [],
    tags: [],
    posts_tags: []
  }
};



function getMatter(filePath) {
  let fileContent = String(fs.readFileSync(filePath));
  return grayMatter(fileContent);
}


function createSlug(filename) {
  return filename.substring(0, filename.lastIndexOf('.'));
  // return Path.parse(filename).name  
  // const filename = Path.parse('/home/user/avatar.png').name  
}

function createDate(filename) {
  return Date.parse(filename.substring(0, 10));
}

function addUniqueTags(tags, index) {
  let postIndex = index+1;

  let addUniqueTag = (tag) => {
    if (tagList.indexOf(tag) === -1){
      tagList.push(tag);
    }

    createPostsTags(postIndex, tagList.indexOf(tag)+1);

  };

  if (typeof tags === 'string') {
    addUniqueTag(tags);
  } else if(typeof tags === 'object') {
    tags.map(tag => addUniqueTag(tag));
  }

}

function createGhostPost(index, matter, filename) {
  let importUserId = 1;
  let post = {
    featured: 0,
    page: 0,
    status: 'published',
    language: 'pt_BR',
    meta_description: matter.data.excerpt,
    author_id: matter.data.author,
    created_by: importUserId,
    updated_by: importUserId,
    published_by: importUserId,
  };

  post = extend(post, {
    id: index + 1,
    uuid: uuid.v4(),
    title: matter.data.title ? matter.data.title : createSlug(filename).replaceAll("-"," ").toLowerCase(),
    slug: createSlug(filename),
    // markdown: matter.content,
    // html: markdown.toHTML(matter.content),
    mobiledoc: "{\"version\":\"0.3.1\",\"atoms\":[],\"cards\":[[\"markdown\",{\"markdown\":\"" + matter.content.replaceAll("\"","_").replaceAll("\\---","---") + "\"}]],\"markups\":[],\"sections\":[[10,0],[1,\"p\",[]]]}",
    image: matter.data.images ? matter.data.images.title : null,
    meta_title: matter.data.title,
    created_at: createDate(filename),
    updated_at: createDate(filename),
    published_at: matter.data.date
    // publised_at: createDate(filename)
  });

  return post;
}

function createGhostTag(index, tag) {
  return {
    id: index + 1,
    name: tag,
    slug: tag,
    description: ""
  };
}

function createPostsTags(postId, tagId){
  ghostData.data.posts_tags.push({post_id:postId, tag_id:tagId});
}


function convert(src, dest){
  fs.readdir(src, (err, filenames) => {
    filenames.forEach((filename, index) => {
      let filePath = path.join(src, filename);
      let matter = getMatter(filePath);
      matterList.push(matter);

      let ghostPost = createGhostPost(index, matter, filename);
      ghostData.data.posts.push(ghostPost);
      addUniqueTags(matter.data.tags, index);
    });

    tagList.forEach((tag, i) => {
      let ghostTag = createGhostTag(i, tag);
      ghostData.data.tags.push(ghostTag);
    });

    createPostsTags();
    fs.writeFile(dest, JSON.stringify(ghostData), err => err ? console.error(err) : process.exit(0));
  });
}

if ( process.argv.length < 4) {
  console.error('You need to specify a path to Jekyll posts.');
} else {
  let src = process.argv[2];
  let dest = process.argv[3];

  convert(src, dest);
}







1 Like

Awesome, thanks so much @diegoeis!

I was still struggling with some missing content, so I tweaked your JS script a little bit to handle newlines and quotes. This seemed to work for me (with a few other issues that I needed to fix manually.)

"use strict";

const fs = require("fs");
const path = require("path");
const grayMatter = require("gray-matter");
const uuid = require("node-uuid");
const markdown = require("markdown").markdown;
const extend = require("util")._extend;

let tagList = [];
let matterList = [];

let ghostData = {
  meta: {
    exported_on: Date.now(),
    version: "5.7.0",
  },
  data: {
    posts: [],
    tags: [],
    posts_tags: [],
  },
};

function getMatter(filePath) {
  let fileContent = String(fs.readFileSync(filePath));
  return grayMatter(fileContent);
}

function createSlug(filename) {
  return filename.substring(0, filename.lastIndexOf("."));
  // return Path.parse(filename).name
  // const filename = Path.parse('/home/user/avatar.png').name
}

function createDate(filename) {
  return Date.parse(filename.substring(0, 10));
}

function addUniqueTags(tags, index) {
  let postIndex = index + 1;

  let addUniqueTag = (tag) => {
    if (tagList.indexOf(tag) === -1) {
      tagList.push(tag);
    }

    createPostsTags(postIndex, tagList.indexOf(tag) + 1);
  };

  if (typeof tags === "string") {
    addUniqueTag(tags);
  } else if (typeof tags === "object") {
    tags.map((tag) => addUniqueTag(tag));
  }
}

function createGhostPost(index, matter, filename) {
  let importUserId = 1;
  let post = {
    featured: 0,
    page: 0,
    status: "published",
    language: "pt_BR",
    meta_description: matter.data.excerpt,
    author_id: matter.data.author,
    created_by: importUserId,
    updated_by: importUserId,
    published_by: importUserId,
  };

  post = extend(post, {
    id: index + 1,
    uuid: uuid.v4(),
    title: matter.data.title
      ? matter.data.title
      : createSlug(filename).replaceAll("-", " ").toLowerCase(),
    slug: createSlug(filename),
    // markdown: matter.content,
    // html: markdown.toHTML(matter.content),
    mobiledoc:
      '{"version":"0.3.1","atoms":[],"cards":[["markdown",{"markdown":"' +
      matter.content
        .replaceAll('"', '\\"')
        .replaceAll("\n", "\\n")
        .replaceAll("\\---", "---") +
      '"}]],"markups":[],"sections":[[10,0],[1,"p",[]]]}',
    image: matter.data.images ? matter.data.images.title : null,
    meta_title: matter.data.title,
    created_at: createDate(filename),
    updated_at: createDate(filename),
    published_at: matter.data.date,
    // publised_at: createDate(filename)
  });

  return post;
}

function createGhostTag(index, tag) {
  return {
    id: index + 1,
    name: tag,
    slug: tag,
    description: "",
  };
}

function createPostsTags(postId, tagId) {
  ghostData.data.posts_tags.push({ post_id: postId, tag_id: tagId });
}

function convert(src, dest) {
  fs.readdir(src, (err, filenames) => {
    // const filename = filenames[0];
    // const index = 0;
    filenames.forEach((filename, index) => {
      let filePath = path.join(src, filename);
      let matter = getMatter(filePath);
      matterList.push(matter);

      let ghostPost = createGhostPost(index, matter, filename);
      ghostData.data.posts.push(ghostPost);
      addUniqueTags(matter.data.tags, index);
    });

    tagList.forEach((tag, i) => {
      let ghostTag = createGhostTag(i, tag);
      ghostData.data.tags.push(ghostTag);
    });

    createPostsTags();
    fs.writeFile(dest, JSON.stringify(ghostData), (err) =>
      err ? console.error(err) : process.exit(0)
    );
  });
}

if (process.argv.length < 4) {
  console.error("You need to specify a path to Jekyll posts.");
} else {
  let src = process.argv[2];
  let dest = process.argv[3];

  convert(src, dest);
}