How the blog was built part 5 - import a backup

How the blog was built part 5 - import a backup
Photo by Andy Li / Unsplash

Now I can backup and update the blog but a few times I have had to manually import the rest of the blog data. You saw from the backup scripts in part 1 it will export the blog content as a JSON file and the content (that includes all the images used) as a ZIP file. The blog is manually imported via:

  • The blog content JSON file is imported from the Settings Lab Import button.
  • The images content is uncompressed from the ZIP file into the blog folder.
  • The profile is reset whenever the blog is reset and the details have to be manually updated again, the profile text and pictures.

I've done this enough times and I need to update the Ghost blog and fix any Snyk reported security issues. I need to automate and this article will show the scripts that will help import a backup automatically. These scripts are a work-in-progress because I haven't yet executed them. This code is my educated guess so far and I will update the code once I have confirmed them working.

Importing the blog content

This script will import the blog content using the datetime of the backup datetime of choice. Which is whatever I extracted from AWS S3 blog backup bucket.

What I really need to do is query the S3 bucket and give the user the option to choose which backup to download and import saving myself those extra manual steps of logging into AWS console and listing the bucket contents or using the AWS CLI. This idea is currently commented out.

The current script otherwise calls the script with the datetime and then runs the Cypress automation to import the blog data.


# Set the date time as GMT - No daylight savings time.

echo "${DATETIME}"

# Todo: Get the list of objects and give user choice of backup to restore
#echo "Get list of backup objects from S3"
#aws s3api list-objects-v2 --bucket $GHOST_DOMAIN-backup --region $AWS_REGION --query 'Contents[?ends_with(Key, `.json`)].[Key] | sort(@) | {Folders: join(`, `, @)}' --profile ghost-blogger &
#echo "Download backup"

# Backup folder should exist!
# Pass in name of folder

echo "Import content folder first"
docker compose exec -T app /usr/local/bin/ ${DATETIME}

#echo "Run the UI test to import the blog from JSON files and return to this process"
npx as-a binarydreams-blog cypress run --spec "cypress/e2e/"

Extract the blog data

The first part of the import is to uncompress the blog backup file into the blog content location.


echo "Importing a Ghost backup"



echo "Create backup extraction folder"
if [ ! -d "/$BACKUP_LOCATION/$NOW-extracted" ] 
    cd /
    mkdir $BACKUP_LOCATION/$NOW-extracted
    echo "Created required /$BACKUP_LOCATION/$NOW-extracted folder"

echo "Unarchiving Ghost content"

# x - , v - show verbose progress, 
# f - file name type, z - create compressed gzip archive

Automatically enter the blog data

This script will:

  • log into Ghost
  • check the blog content JSON file exists
  • run the test to import the blog

Currently this script will need the datetime hardcoded until it can be passed in as an argument and both the extraction datetime and the blog content datetime can be the same value. Another item on the TODO list.

Then the profile is imported with these steps:

  • log into Ghost
  • reading the profile JSON file from the expected location
  • Browse to the profile page
  • Upload the cover picture
  • Upload the profile picture
  • Enter the profile details

// Command to use to pass secret to cypress
// as-a local cypress open/run

describe('Import', () => {

  beforeEach(() => {
    // Log into ghost
    const username = Cypress.env('username')
    const password = Cypress.env('password')

    // it is ok for the username to be visible in the Command Log
    expect(username, 'username was set')'string')
    // but the password value should not be shown
    if (typeof password !== 'string' || !password) {
      throw new Error('Missing password value, set using CYPRESS_password=...')

    cy.get('#ember7').type(username).should('have.value', username)
    cy.get('#ember9').type(password, { log: false }).should(el$ => {
      if (el$.val() !== password) {
        throw new Error('Different value of typed password')

    // Click Log in button
    cy.get('#ember11 > span').click()

  it('Content from JSON', () => {

    // TODO: how to get the file dynamically?
    let inputFile = "/backup/2022-11-25-01-31-56-extracted/binarydreams.ghost.2022-11-25-01-32-09.json"

    // Click Settings icon
    cy.get('.gh-nav-bottom-tabicon', { timeout: 10000 }).should('be.visible').click()

    // The Labs link is generated so go via the link

    // Click browse and select the file
    cy.get('.gh-input > span').selectFile(inputFile)

    // Click Import button
    cy.get(':nth-child(1) > .gh-expandable-header > #startupload > span').click()


  it('Profile from JSON', () => {

    // TODO: How to get the file dynamically?
    // Find the file without the timestamp? i.e. profile.ghost.*.json
    // Or dont use timestanmp in file?
    let inputFile = "/backup/2022-11-25-01-31-56-extracted/profile.ghost.2022-12-09-20-49-44.json"
    let profile = cy.readFile(inputFile)

    // Click Settings icon
    cy.get('.gh-nav-bottom-tabicon', { timeout: 10000 }).should('be.visible').click()

    // The profile link is easier to go via the link

    // Cover picture
    cy.get('.gh-btn gh-btn-default user-cover-edit', { timeout: 10000 })

    cy.get('.gh-btn gh-btn-white').click().selectFile(profile.coverpicture)

    // Save the picture
    cy.get('.gh-btn gh-btn-black right gh-btn-icon ember-view').click()

    // Profile picture
    cy.get('.edit-user-image', { timeout: 10000 })

    cy.get('.gh-btn gh-btn-white').click().selectFile(profile.profilepicture)

    // Save the picture
    cy.get('.gh-btn gh-btn-black right gh-btn-icon ember-view').click()

    // Import text from profile file









Once this has been fully tested then I will update this article.