@reuters-graphics/server-client

[![npm version](https://badge.fury.io/js/%40reuters-graphics%2Fserver-client.svg)](https://badge.fury.io/js/%40reuters-graphics%2Fserver-client)

Usage no npm install needed!

<script type="module">
  import reutersGraphicsServerClient from 'https://cdn.skypack.dev/@reuters-graphics/server-client';
</script>

README

@reuters-graphics/server-client

npm version

The server-client uploads graphics to the sphinx graphics server.

Quickstart

Install

$ yarn add @reuters-graphics/server-client

Creating a graphic

const ServerClient = require('@reuters-graphics/server-client');


// Initiate a new client with your server credentials.
const client = new ServerClient({
  username, // Your graphics server username
  password, // Your graphics server password
  apiKey, // The sphinx API key for your environment: test, UAT or prod
});

// Create some metadata for the graphic pack.
const packMetadata = {
  rootSlug: 'HEALTH-CORONAVIRUS',
  wildSlug: 'MAP',
  desk: 'london',
  language: 'en',
  title: 'My test project',
  description: 'A coming-of-age story with explosive action.',
  byline: 'Jon McClure',
  contactEmail: 'jon.mcclure@thomsonreuters.com',
};

// Create a graphic pack w/ your metadata.
await client.createGraphic(packMetadata);

// Create some metadata for an edition
const editionMetadata = {
  language: 'en',
  title: 'My test project',
  description: 'A coming-of-age story with explosive action.',
  embed: {
    declaration: '<div id="embed"></div><script type="text/javascript">new pym.Parent("embed", "https:/.../embed.html", {});</script>',
    dependencies: '<script type="text/javascript" src="//graphics.thomsonreuters.com/pym.min.js"></script>',
  },
};

// Read in an archive with your graphic to a buffer
const fileBuffer = fs.readFileSync('public-de.zip');

// Create editions from the archive
const editions = await client.createEditions('public-de.zip', fileBuffer, editionMetadata);

// The returned object will have the edition IDs and
// any public URLs
editions['public-en.zip']
// {
//   interactive: {
//     id: '...',
//     url: 'https://...'
//   }
// }

Updating a graphic

// For existing graphics, pass a graphic object with an
// ID when initializing the client.
const client = new ServerClient({
  username,
  password,
  apiKey,
  graphic: {
    id: 'XXXXXXXX-XXXX...', // Existing graphic UUID
  },
});

// Update your metadata for the graphic pack
const packMetadata = {
  rootSlug: 'HEALTH-CORONAVIRUS',
  wildSlug: 'MAP',
  desk: 'london',
  language: 'en',
  title: 'My updated test project',
  description: 'A coming-of-age story of revenge with explosive action.',
  byline: 'Jon McClure, Matthew Weber',
  contactEmail: 'jon.mcclure@thomsonreuters.com',
};

// Update your graphic with new pack metadata.
await client.updateGraphic(packMetadata);

// Update metadata for editions.
const editionMetadata = {
  language: 'de',
  title: 'Mein Testprojekt',
  description: 'Eine Coming-of-Age-Geschichte mit explosiver Action.',
  embed: {
    declaration: '<div id="embed"></div><script type="text/javascript">new pym.Parent("embed", "https:/.../embed.html", {});</script>',
    dependencies: '<script type="text/javascript" src="//graphics.thomsonreuters.com/pym.min.js"></script>',
  },
};

// Read in updated archive.
const fileBuffer = fs.readFileSync('public-de.zip');

// Update editions from archive.
const editionURLs = await updateClient.updateEditions('public-de.zip', fileBuffer, editionMetadata);

Publishing a graphic

const client = new ServerClient({
  username,
  password,
  apiKey,
  graphic: {
    id: 'XXXXXXXX-XXXX...', // Existing graphic UUID
  },
});

// Publish all editions in the graphic
await client.publishGraphic();

// Or publish only those editions made from particular archives
await client.publishGraphic(['public-de.zip', 'media-de.zip']);

// Or pass additional publishing locations and correction status
const publishToMedia = true; // default false
const publishToLynx = false; // default false
const isCorrection = false; // default null which will prompt a user
await client.publishGraphic([], publishToMedia, publishToLynx, isCorrection);

// Or you can specify specific editions by name to publish to Media and Lynx
await client.publishGraphic([], ['media-interactive'], ['interactive'], isCorrection);

Metadata reference

Pack

  • rootSlug: A general topic slug. Required.
  • wildSlug: A more specific slug.
  • language: Two-letter ISO 639-1 code for the default language for the pack. Defaults to en but may also be ar, fr, es, de, it, ja, pt or ru.
  • group: One of either rngs or editorcharts.
  • desk: One of london, singapore, bengaluru or new york. Required.
  • title: Title in English. Generally, write for SEO. Required.
  • description: A longer description in English. Generally, write for SEO. Required.
  • byline: Authors for the piece. Separate multiple with commas. Required.
  • contactEmail: Thomson Reuters email for primary contact for the piece; generally, the author. Required.

Edition

  • language: Two-letter ISO 639-1 code for the language the edition is written in. Must be one of en, ar, fr, es, de, it, ja, pt or ru. Required.
  • title: Title in the matching language to overwrite the pack title. Generally, write for SEO. Will automatically be set to null if same as pack title. Required.
  • description: A longer description in the matching language to override the pack description. Generally, write for SEO. Will be automatically set to null if same as pack description. Required.
  • embed
    • declaration: Embed code for parent page, usually a div and pym script to fill it.
    • dependencies: Any embed dependency scripts that should be injected in the head, for example, the pym.js script.

Making your archive

Structure

Your archive should have the following structure:

- public-en/
  - interactive/
    - index.html
    ...

Your archive should contain:

  1. A root folder named after the archive, usually indicating the language of the edition, for example, en, de, etc.

  2. An edition folder within the root folder. If the edition represents a public page, name it interactive. If it is a package of source code for media clients, name it media-interactive. Anything else, name the folder something that describes what that edition contains, for example, PDF or PNG. You may have more than one edition folder in your archive.

  3. A root edition file within the edition folder. The type of file this is will drive particular behavior and validation rules on the graphics server. There is also a hierarchy for determining what is the root edition file if multiple file types are present; generally, .html files take precedence. Other commonly used root edition file types: .jpg, .png, .pdf. Note that if multiple files of the same type are present at the root, the first file added to the zip will be the root edition file. At present, it is not possible to control which file is added first.

For interactive editions

The root file should be index.html.

In most cases, additional HTML files should be at least one directory deeper than the root edition file. CSS, JS and images can be at the same level as index.html. You may put an embed HTML file at the same level as index.html, but at present, it is not possible to control which is the root file.

Include a _gfxpreview.png image in the edition folder, which will be used to preview the edition in the graphics server and Connect.

For media-interactive editions

The root file should be a README.txt file.

Include a _gfxpreview.png image in the edition folder, which will be used to preview the edition in the graphics server and Connect.

Creating the archive in node

If you are creating the archive in node, use the archiver node package.

Known issue

The server is very finicky about the archive file it will accept. For example, you cannot create an archive in memory and then add or change files using archiver's .append method. (🤷)

Instead, build out your archive's directory and file structure on your local file system first, then create the archive from that directory.

Here's a very simple example of doing that:

const fs = require('fs');
const path = require('path');
const archiver = require('archiver');

// The directory with your built files
const SRC_DIR = path.resolve(process.cwd(), 'dist');
// The directory for your archive
const DEST_DIR = path.resolve(process.cwd(), 'public-en');

const createZip = (resolve, reject) => {
  const writer = new Stream.Writable();
  const chunks = [];

  writer._write = (chunk, encoding, next) => {
    chunks.push(chunk); next();
  };

  const archive = archiver('zip');

  archive.on('error', e => reject(e));
  // We'll return a buffer from this function
  archive.on('end', () => resolve(Buffer.concat(chunks)));
  archive.pipe(writer);

  // Construct your archive locally
  fs.copyFileSync(path.join(SRC_DIR, 'index.html'), path.join(DEST_DIR, 'interactive/index.html'))

  // ... then use the directory to create your archive
  archive.directory(path.join(DEST_DIR, 'public-en'), 'public-en');
  archive.finalize();
};

module.exports = async() => new Promise((resolve, reject) =>
  createZip(resolve, reject));

Testing

Clone this repository, if you haven't, and install dependencies:

$ yarn

To test, make a copy of .env.example at .env and fill in the environment variables:

  • USERNAME: Sphinx username
  • PASSWORD: Sphinx password
  • API_KEY: Sphinx API key
  • SPHINX_ENV: one of TEST, UAT, CI or PROD

Then build the library and run tests:

$ yarn build && yarn test