Best way to automate post-publication process

Hello

After each publication, I want to trigger a process which will:

  • Move images in the right directory (publication date ‘year/month’ and not current ‘year/month’)
  • Update image links accordingly
  • Add first landscape image (or first image fallback) as feature image

Actually, I do it with a custom handmade python script, but I’m thinking about:

  • Rewrite in js the 3 steps above (need to learn the language, need some days)
  • Expose this js with ‘slug’ in input
  • Trigger a webhook when a new post has been published to request this page.

Any feedback about those steps?
Any code to help me in this quest?

If someone has the same need and want to co-develop this with me, we can share the code repo for sure. I already know (in python) how to use the admin API, and how to handle the mobiledoc part of the post.

Best

ps : another (better) way to do that is to modify the publication process in the interface, but I’m pretty sure that ghost admin will prefer that we use API to perform such “specific” action.

1 Like

I think it’s an awesome idea to generalize this to actually make a “ghost sidecar” service that is supposed to be run alongside the ghost service, which can be configured to do these things (and others).

Hello again

So I wrote some lines for the last step (setup first landscape image as featured image) here.

update.js file:

const GhostAdminAPI = require('@tryghost/admin-api');
const probe = require('probe-image-size');

arguments = process.argv.slice(2)
const config = require(arguments[0])

const ghost_dir = config.paths.contentPath.replace('/content','')
const ghost_url = config.url.replace(/\/$/, "");

console.log(ghost_url)
console.log(ghost_dir)
console.log(config.scripts.admin_key)

const api = new GhostAdminAPI({
    url: ghost_url,
    key: config.scripts.admin_key,
    version: 'v3'
});

api.posts.browse()
  .then(response => console.log(
    '%s contains %s posts', 
    config.url,
    response.meta.pagination.total));


var post_slug = arguments[1];
console.log('slugs: ', post_slug);


function get_images_from_card(card) {
  let images = [];

  switch (card[0]) {
    case 'gallery':
      card[1].images.forEach(
        element => {
          images.push(element.src)
        }
      );
      break;
    case 'image':
      images.push(card[1].src)
      break;
  }

  // console.log('images : %s', images)
  return images
}

function isEmpty(str) {
  return (!str || str.length === 0 );
}

function update_post(post){
  var mdoc = JSON.parse(post.mobiledoc)
  var cards = mdoc.cards
  let images = []

  // Get all images defined in card sections
  cards.forEach(
    element => images = images.concat(get_images_from_card(element)));

  // Remove duplicates
  images = Array.from(new Set(images)) 

  // Rename and get featured image
  let feature_image = ''
  for(var i=0; i < images.length; i++) {
    images[i] = images[i].replace(ghost_url, ghost_dir);
    let data = require('fs').readFileSync(images[i]);
    if (isEmpty(feature_image)) {
      p_data = probe.sync(data)
      if (p_data.width > p_data.height) {
        feature_image = images[i]
      }
      // console.log(p_data);
    }
   }
  // Fallback first image
  if (isEmpty(feature_image)) {
    feature_image = images[0]
  }

  post.feature_image = feature_image.replace(ghost_dir, ghost_url);

  console.log('images : %s', images)
  console.log('featured : %s', post.feature_image)

  console.log('Updating post %s with %s', post.id, post.feature_image)

  api.posts.edit({id: post.id,
    updated_at: post.updated_at,
    feature_image: post.feature_image})
}

api.posts.read({slug: post_slug})
  .then(
    post => update_post(post)
    )
  .catch(error => {
    console.error('Error or Post %s not found',post_slug)
    console.error(error)
  })

I added the admin token directy in the config.development.json file:

  "scripts": {
    "admin_key": "YOUR_KEY"
  }

And I run this code using

node update.js /path_to/config.production.json slug

This is my first javascript program. Feel free to give me feedback!

Next step: create a dedicated API endpoint with a JS code to do the other steps as well.

Ok,

It may not be perfect as it’s my first lines in javascript, but it seems to work. Here is my code:

const express = require("express")
const GhostAdminAPI = require('@tryghost/admin-api');
const ProbeImageSize = require('probe-image-size');
const fs = require('fs')

const app = express()
const PORT = 3000

arguments = process.argv.slice(2)
const config = require(arguments[0])

const ghostDir = config.paths.contentPath.replace('/content','')
const ghostUrl = config.url.replace(/\/$/, "");

console.log('Site '+ ghostUrl + ' installed in ' + ghostDir)

function checkIP(req) {
    const whitelistedIP = ['::1', '::ffff:127.0.0.1']
    if (whitelistedIP.includes(req.ip)) {
        console.log('Connection GRANTED from %s', req.ip)
        return true
    } else {
        console.log('Connection DROPPED from %s', req.ip)
        return false
    }
} 

// Update

const api = new GhostAdminAPI({
    url: ghostUrl,
    key: config.scripts.admin_key,
    version: 'v3'
});

function get_images_from_card(card) {
    let images = [];
  
    switch (card[0]) {
      case 'gallery':
        card[1].images.forEach(
          element => {
            images.push(element.src)
          }
        );
        break;
      case 'image':
        images.push(card[1].src)
        break;
    }
  
    // console.log('images : %s', images)
    return images
  }
  
  function isEmpty(str) {
    return (!str || str.length === 0 );
  }
  
  function selectFeatureImage(images) {
    let featureImage = ''
  
    for(let i=0; i < images.length; i++) {
      images[i] = images[i].replace(ghostUrl, ghostDir);
      let data = fs.readFileSync(images[i]);
      if (isEmpty(featureImage)) {
        p_data = ProbeImageSize.sync(data)
        if (p_data.width > p_data.height) {
          featureImage = images[i]
        }
        // console.log(p_data);
      }
     }
    // Fallback first image
    if (isEmpty(featureImage)) {
      featureImage = images[0]
    }
  
    return featureImage.replace(ghostDir, ghostUrl);
  }
  
  function updateImageCard(imageCard, imagesPrefix) {
    const targetDir = ghostDir + '/content/images/' + imagesPrefix
    const targetUrl = ghostUrl + '/content/images/' + imagesPrefix
  
    originalImageUrl = imageCard.src
  
    const originalImagePath = originalImageUrl.split('/')
    const imageName = originalImagePath[originalImagePath.length - 1]
  
    const originalImageDir = ghostDir + '/content/images/' 
      + originalImagePath[originalImagePath.length - 3] + '/'
      + originalImagePath[originalImagePath.length - 2]
      + '/' + imageName
  
    const targetImageDir = targetDir + '/'+ imageName
    const targetImageUrl = targetUrl + '/'+ imageName
  
    // console.log('%s -> %s', originalImageDir, targetImageDir)
    
    if (targetImageUrl != originalImageUrl) {
        // Do not overwrite existing image
        if (fs.existsSync(targetImageDir) && fs.existsSync(originalImageDir)) {
          console.error('Cannot move %s to %s', originalImageDir, targetImageDir)
          console.error('Both images exists');
          return null
        } else if (!fs.existsSync(targetImageDir) && fs.existsSync(originalImageDir)) {
          // console.log('Moved %s to %s', originalImageDir, targetImageDir)
          fs.rename(originalImageDir, targetImageDir, function (err) {
            if (err) throw err
            console.log('Moved %s to %s', originalImageDir, targetImageDir)
            imageCard.src = targetImageUrl
            return imageCard
          })
          // imageCard.src = targetImageUrl
          // return imageCard
        } else if (fs.existsSync(targetImageDir) && ! fs.existsSync(originalImageDir)) {
          console.log('Updated %s to %s', originalImageDir, targetImageDir)
          imageCard.src = targetImageUrl
          return imageCard
        } else {
          console.log('Nothing to do')
          return null
        }
    }
  }
  
  function updateCard(card, imagesPrefix) {
  
    switch (card[0]) {
  
      case 'gallery':
        images = card[1].images
        for(let i=0; i < images.length; i++) {
          newCard = updateImageCard(images[i], imagesPrefix)
          if (newCard) {
            images[i] = newCard
          }
        }
        // console.log('new images', images)
        const valImages = {}
        valImages['images'] = images   
        return ['gallery', valImages]
      case 'image':
        // console.log('Image card : %s', card[1])
        newCard = updateImageCard(card[1], imagesPrefix)
        if (newCard) {
          // console.log(newCard)
          return ['image', newCard]
        } else {
          return card
        }
      default:
        return card
    }
  
  }
  
  function updateCards(cards, imagesPrefix) {
    newCards = []
    cards.forEach(
      card => newCards.push(updateCard(card, imagesPrefix)));
  
    return newCards
  }
  
  function createImageDir(postDateYear, postDateMonth) {
    const yearDir = ghostDir + '/content/images/' + postDateYear
    if (!fs.existsSync(yearDir)){
      fs.mkdirSync(yearDir);
      console.log('Directory %s created', yearDir)
    }
  
    const monthDir = yearDir + '/' + postDateMonth
    if (!fs.existsSync(monthDir)){
      fs.mkdirSync(monthDir);
      console.log('Directory %s created', monthDir)
    }
  }
  
  function updatePost(post){
    let mdoc = JSON.parse(post.mobiledoc)
    let cards = mdoc.cards
    let images = []
  
    const postDate = new Date(Date.parse(post.published_at))
    // console.log(post_date)
    const postDateMonth = ('0' + (postDate.getMonth()+1)).slice(-2)
    const postDateYear = postDate.getFullYear();
    const imagesPrefix = postDateYear + '/' + postDateMonth
  
    console.log('%s/%s Updating post %s', postDateMonth, postDateYear, post.slug)
  
    // Create image dir if needed
    createImageDir(postDateYear, postDateMonth)
  
    oldCardsStr = JSON.stringify(cards)
    cards = updateCards(cards, imagesPrefix)
  
    // Get images defined in card sections
    cards.forEach(
      element => images = images.concat(get_images_from_card(element)));
    images = Array.from(new Set(images)) 
    // console.log('images : %s', images)
    featureImage = selectFeatureImage(images)
    // console.log('featured : %s', featureImage)
   
    mdoc.cards = cards
  
    if (oldCardsStr != JSON.stringify(cards)) {
      console.log('Updating post %s with images and feature image to %s', post.id, featureImage)
      api.posts.edit({id: post.id,
        updated_at: post.updated_at,
        mobiledoc: JSON.stringify(mdoc),
        feature_image: featureImage})
    } else if (featureImage != post.feature_image) {
      console.log('Updating post %s feature image to %s', post.id, featureImage)
      api.posts.edit({id: post.id,
        updated_at: post.updated_at,
        feature_image: featureImage})
    } else {
      console.log('Post %s is up to date', post.id)
    }
  }
  
function updatePostReq(postSlug) {
    api.posts.read({slug: postSlug})
    .then(
      post => updatePost(post)
      )
    .catch(error => {
      console.error('Error or Post %s not found',postSlug)
      console.error(error)
    })
}

// API side

app.use(express.json()); 

app.get('/check', (req, res) => {
    res.end('online');
    console.log('server online')
    res.status(200).end()
})


app.post("/update", (req, res) => {
  if (!checkIP(req)) {
      return
  }

  const slug = req.body.post.current.slug
  console.log('Request to update %s', req.query.slug)
  updatePostReq(slug)
  res.status(200).end()
})

app.get('/update', (req, res) => {
    if (!checkIP(req)) {
        return
    }
    if (req.query.slug) {
        res.end('update post '+ req.query.slug);
        console.log('Request from %s to update %s', req.ip, req.query.slug)
        updatePostReq(req.query.slug)
    }
    res.status(200).end()
})
  
app.listen(PORT, () => console.log(`🚀 Server running on port ${PORT}`))

I use pm2 to let this code running.

Create conf file:

{
    "apps": {
        "name": "ghost-tools-update",
        "exec": "index.js",
        "args": "/path_to/config.production.json",
        "watch": true,
        "ignore_watch": [
            "node_modules",
            "logs"
        ],
        "error_file": "/home/ghost/.pm2/logs/ghost-tools-update-err.log",
        "out_file": "/home/ghost/.pm2/logs/ghost-tools-update-out.log",
        "log_date_format": "YYYY-MM-DD HH:mm:ss"
    }
}  

And launch:

$ sudo -u ghost pm2 start index.js pm2.conf

and I created two webhooks to https://localhost:3000/update triggered by ‘Post published’ and ‘Published post updated’

Hope it will help somebody else to do the same thing.

I will continue to improve this code, but if somebody, better in JS than me, want to help, I can share the updates on gitlab.

1 Like