Hugo Drag & Drop

Basic Configuration

To enable dragging both images and documents from huGO+ into Livingdocs you first need to configure hugo.resource in the server configuration

Image Drag and Drop

Images can be dragged onto a Livingdocs document directly from huGO+ after the basic configuration is set up.

You can configure mappings form Hugo fields to the metadata of your MediaLibraryEntries in the property hugoExtraction in your image mediaType.

Note: you can restrict from which sources images can be uploaded, e.g. you could allow only uploads from huGO+. See Image Source Policy

Document Drag and Drop

In case you use huGO+ as Digital Assets Management software, you can import articles from huGO+ by dragging and dropping them onto a Livingdocs dashboard.

There are two types of articles available for imports:

  • Agency articles These are imported from established news agencies like DPA, Reuters, etc.
  • Archive articles They come from sources you specify on your own: If you have a print system or any other system and wish to feed articles into huGO+ you would get Archive articles.

Preparation

Think about what types of articles you want to create from an imported hugo document and which contentType you’d like them to have. You can specify multiple target documents with different contentTypes.

Configuration

This configuration is needed to let the server know what kind of target designs and layouts are available, which transformations handle them and where these transformations are located.

// conf/environments/all.js

hugo: {
  targets: {
    // basePath is the root directory for transformations
    basePath: path.resolve('./plugins/hugo-import-transformations'),
    // huGO+ provides two types of articles, encoded as 'articleAgency' and 'articleArchive'
    articleAgency: {
      // Specify the directory that contains transformations for agency articles
      dir: 'agency',
      // Arbitrary number of targets possible
      layouts: [{
        // You'd typically want to specify your own design here
        design: 'timeline',
        // This can be any contentType from your project.
        layout: 'regular',
        // This corresponds to a file named 'regular.js' that holds code for this particular transformation
        transformation: 'regular'
      }, {
        design: 'limestone',
        layout: 'report',
        transformation: 'lime_report'
      }]
    },
    articleArchive: {
      dir: 'archive'
      layouts: [
        design: 'timeline'
        layout: 'magazine'
        transformation: 'magazine'
      ]
    }
  }
}

Important: Every item in the configuration object is required for the feature to work.

Transformations

Now that you have configured the feature you’ll want to provide transformations so the huGO+ document can be converted to a document that corresponds to your design and layout.

A transformation is a single function that is expected to return an object containing a livingdoc and metadata and should have following signature:

// E.g. ./plugins/hugo-import-transformations/agency/regular.js

module.exports = function ({hugoArticle, design, layout, metadata, imagesApi}, callback) {
  // ...
  // Create a livingdoc and collect metadata
  // ...
  callback(null, {livingdoc, metadata})
}

You are provided with the hugoArticle you imported, the design and layout you have specified in your config and the imagesApi which is a service you can use to handle images, e.g. uploading them to your storage. You also have the possibility to pass additional metadata which must have been defined beforehand (see below).

Example transformation

const async = require('async')
const framework = require('@livingdocs/server/framework')

module.exports = function ({hugoArticle, design, layout, metadata, imagesApi}, callback) {
  transform({hugoArticle, design, layout, imagesApi}, function (err, livingdoc) {
    if (err) return callback(err)
    const metadata = getMetadata(hugoArticle)
    callback(null, {livingdoc, metadata})
  })
}

function transform ({hugoArticle, design, layout, imagesApi}, callback) {
  const imageService = conf.get('image_service') // You'll need to configure an imageservice like 'imgix' if you'd like to use images
  framework.design.add(design)
  const livingdoc = createEmptyLivingdoc({name: design.name, version: design.version}, layout)
  const tree = livingdoc.componentTree

  // Header
  const header = createHeader(hugoArticle.title, tree)
  tree.append(header)

  // Body as a collection of paragraphs
  for (const text of hugoArticle.text) {
    const p = createParagraph(text, tree)
    tree.append(p)
  }

  // Handle images
  const imageUploader = function (image, cb) {
    const job = imagesApi.createImageJob({url: image.url})
    imagesApi.processJob(job, (err, imageInfo) => {
      if (err) return cb(err)
      return cb(null, {imageInfo, image})
    })
  }

  const hugoImages = hugoArticle.images || []
  // the images have to be added to the document in order
  async.map(hugoImages, imageUploader, function (err, livingdocsImages) {
    if (err) return callback(err)
    for (const {imageInfo, image} of livingdocsImages) {
      const imageComponent = createImage(imageInfo, image, imageService, tree)
      tree.append(imageComponent)
    }

    callback(null, livingdoc)
  })
}

const createEmptyLivingdoc = function (targetDesign, layout) {
  return framework.create({
    content: [],
    design: targetDesign,
    layoutName: layout
  })
}

const createHeader = function (title, tree) {
  const headerComponent = tree.createComponent('header')
  const titleDirective = headerComponent.directives.get('title')
  titleDirective.setContent(title)
  return headerComponent
}

const createParagraph = function (text, tree) {
  const paragraphComponent = tree.createComponent('p')
  paragraphComponent.setContent('text', text)
  return paragraphComponent
}

const createImage = function ({url, height, width, size, mime: mimeType},
  hugoImage, imageService, tree) {
  const imageComponent = tree.createComponent('image')
  imageComponent.setContent('image', {url, height, width, size, mimeType, imageService})
  imageComponent.setContent('caption', hugoImage.caption)
  imageComponent.setContent('source', hugoImage.agency)
  return imageComponent
}

// Extract metadata
function getMetadata (hugoArticle) {
  const hugoMetadata = {
    id: hugoArticle.id,
    category: hugoArticle.category,
    urgency: hugoArticle.urgency,
    source: hugoArticle.source,
    timestamp: new Date(hugoArticle.hugoTimestamp).toISOString(),
    service: hugoArticle.service,
    keywords: hugoArticle.keywords
  }

  const metadata = {
    hugo: hugoMetadata,
    title: hugoArticle.title
  }

  return metadata
}

Metadata

You might want to store data that’s embedded in each hugoArticle thus you need to specify that data in order for it to be valid.

Metadata plugin

// plugins/metadata/hugo.js
module.exports = {
  name: 'hugo',
  storageSchema: {
    additionalProperties: false,
    properties: {
      id: {
        type: 'string'
      },
      category: {
        type: 'string'
      },
      urgency: {
        type: 'string'
      },
      source: {
        type: 'string'
      },
      timestamp: {
        type: 'string',
        format: 'date-time'
      },
      service: {
        type: 'string'
      },
      keywords: {
        type: 'string'
      },
      note: {
        type: 'string'
      }
    }
  }
}

Elasticsearch metadata

The metadata you have specified should be made known to Elasticsearch as well.

// app/search/custom-mappings/document_metadata.json

// ...
"hugo": {
  "properties": {
    "id": {
      "type": "string",
      "index": "not_analyzed"
    },
    "category": {
      "type": "string",
      "index": "not_analyzed"
    },
    "urgency": {
      "type": "integer",
      "index": "not_analyzed"
    },
    "source": {
      "type": "string",
      "index": "not_analyzed"
    },
    "timestamp": {
      "type": "date",
      "format": "strict_date_time",
      "index": "not_analyzed"
    },
    "service": {
      "type": "string",
      "index": "not_analyzed"
    },
    "keywords": {
      "type": "string",
      "index": "not_analyzed"
    },
    "note": {
      "type": "string",
      "index": "not_analyzed"
    }
  }
}
// ...

Configure articles

At last you have to configure all your possible huGO+ targets with the metadata plugin you created before.

  // E.g. conf/channels/web/article/all.js
  //  or conf/channels/web/magazine/all.js

  hugo: {plugin: 'hugo'}