Need a quick dev site?

I wrote a tool to make it easier to make a quick copy of Ghost for development work.

Need a quick dev site?

Local development is a great way to test out theme changes without impacting your theme.

💡
For Windows users, I strongly recommend that you install WSL (use Ubuntu 22.04). That lets you run a Linux environment, and everything Ghost-related works better. In particular, there's a bug with asset generation that will make it very hard to develop a theme on Windows. Do not do theme development on Windows without WSL. You'll be frustrated.

OK, with that warning out of the way, you'll likely want to have your site's content available, so that you can see how your own content works with your theme changes. There are two parts to this.

Get the site content:

/ghost > settings (the gear icon) > import/export. This gets you .json file that you can upload on your local copy of Ghost, by visiting /ghost > settings (the gear icon) > import/export.

Get the images:

Unfortunately, that .json doesn't have your images. If you use Unsplash images, the links will copy over without issues, but if you're uploading your own images, your dev install is going to be full of broken image links. There are options for getting your images:

Get the images from the server.

If you have FTP/shell access, you can copy the whole /content/images folder from your production Ghost setup over to your development Ghost setup. Note that many managed and semi-managed hosting options for Ghost do not include this level of access, so this isn't going to work for many users.

Ask your host for the images

If you're on Ghost Pro or another managed host, you can ask them to provide a .zip of your content/images directory.

Grab some images for yourself

The code below visits the site homepage, grabs all the images on the homepage, then visits each page linked from the homepage and grabs those images. It downloads the images into the correct directory structure, so that all you have to do is zip up the content folder and upload that .zip to the Ghost universal importer.

let basedomain = 'https://demo.ghost.io'
let starturl = basedomain
let mypath = 'retrieved_images/'

const cheerio = require('cheerio');
const fs = require('fs');
const path = require('path');
const { Readable } = require('stream');
const { finished } = require('stream');
const { promisify } = require('util');

const finishedAsync = promisify(finished);

let grabThese = new Set ;
let crawlQueue = new Set ; 

async function processPage(url, base = false) {
    // read the first page and record image URLs.
    let page = await fetch(url);
    let html = await page.text();
    let $ = cheerio.load(html);
    let images = $('img');
    console.log('found images at:', url, images.length);

    images.each((i, image) => {
        let src = $(image).attr('src');
        // if src is a relative path, append the base url
        if (src.startsWith('/')) {
            // remove /size/w600/ from src
            src = src.replace(/\/size\/w\d+/, '');
            grabThese.add(src);
        }
    });

    if (base) {
    let urls = $('a');
    urls.each((i, url) => {
        let href = $(url).attr('href');
        if (href.startsWith('/')) {
            crawlQueue.add(href);
        }
    });
    }
}

async function getImages() {
    // process all the images
    for (let one of grabThese ) {
        let res = await fetch(basedomain + one);

        let folder = one.split('/').slice(0, -1).join('/');
        // make these folders if they don't exist
        if (!fs.existsSync(mypath + folder)) {
            fs.mkdirSync(mypath + folder, { recursive: true });
        }
        const destination = path.resolve(mypath + one);
        const fileStream = fs.createWriteStream(destination, { flags: 'wx' });
        await finishedAsync(Readable.fromWeb(res.body).pipe(fileStream));
    }
}

async function main() {
    await processPage(starturl, true);

    for (let one of crawlQueue) {
        await processPage(basedomain + one);
    }

    await getImages();
}

main();

grabimages.js

You'll also need a package.json:

{
  "name": "image-grabber",
  "version": "1.0.0",
  "description": "",
  "main": "grabimages.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "cheerio": "^1.0.0-rc.12"
  }
}

package.json

How to use:

  1. You need Node installed. (I'm using v18.)
  2. Put the two files above in an empty directory together, and make a /retrieved_images/ subdirectory.
  3. In that directory, run 'npm install', which will read package.json and go get teh dependencies you need.
  4. Adjust the first three lines as needed, then run it: node ./grabimages.js
  5. Zip the content directory, and upload it to Ghost.
⚠️
This is NOT a full site backup. My goal was just to grab 'enough' images to allow me to do theme development, not to fully clone a site. (Some sites are really big!)

Other things to move

If your site has one, you'll also want to download your routes.yaml and upload it through the Ghost dashboard.


Happy Ghosting!

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!