@reuters-graphics/server-client

📡 server-client

npm version

The server client uploads graphic projects to the RNGS Sphinx server, which are then published to reuters.com readers and media clients through Reuters Connect.

Quickstart

Install

npm i -D @reuters-graphics/server-client

Creating a graphic

import { ServerClient } from '@reuters-graphics/server-client';
import type { Graphic, Edition, RNGS } from '@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: Graphic.GraphicMetadata = {
rootSlug: 'HEALTH-CORONAVIRUS',
wildSlug: 'MAP',
desk: 'london' as Graphic.Desk,
language: 'en'as RNGS.Language,
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: Edition.EditionMetadata = {
language: 'en' as RNGS.Language,
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('media-en-page.zip');

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

// The returned object will have the edition IDs and
// any public URLs
editions['media-en-page.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: 'XXXXXXXX-XXXX...', // Existing graphic UUID
});

// Update your metadata for the graphic pack
const packMetadata: Graphic.GraphicMetadata = {
rootSlug: 'HEALTH-CORONAVIRUS',
wildSlug: 'MAP',
desk: 'london' as Graphic.Desk,
language: 'en' as RNGS.Language,
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: Edition.EditionMetadata = {
language: 'de' as RNGS.Language,
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('media-de-page.zip');

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

Publishing a graphic

import type { Publishing } from '@reuters-graphics/server-client';

const client = new ServerClient({
username,
password,
apiKey,
graphic: '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.zip', 'media-en-page.zip']);

// ... or pass additional publishing locations and correction status
const publishToMedia: Publishing.PublishToMedia = true; // default false
const publishToLynx: Publishing.PublishToLynx = false; // default false
const revisionType: Publishing.PublishRevisionType = 'Refresh'; // default null, which will ask a user for revision type

await client.publishGraphic([], publishToMedia, publishToLynx, revisionType);

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

// ... or you can pass arrays of archive file names and editions to target very specific editions
await client.publishGraphic(
[],
[['media-en-page.zip', 'media-interactive'], ['media-en-page.zip', 'PNG']],
[['public.zip', 'interactive']],
revisionType
);

Note: Archives named after our embeddable full-page style (i.e., media-{locale}-page.zip or media-{locale}-{slug}-page.zip) are explicitly excluded from promoting in Lynx. See this issue.

Guide to graphics published in the RNGS server

The Reuters News Graphics Service (RNGS) Sphinx server is a cloud-based app we use to publish Reuters Graphics to the web and to clients.

Structure of graphics in RNGS

Graphics in RNGS are composed of three main parts:

  • Graphic pack: An overall graphics project, which includes at least one editon published to either reuters.com readers or media clients through Reuters Connect.
  • Archive: A ZIP file uploaded to the RNGS server that includes one or more editions.
  • Edition: Collection of files that make up an embeddable or editable graphic or a graphics page.
%%{init:{"theme":"dark"}}%% flowchart LR Pack[Overall pack]---Archive1[public.zip] Pack---Archive2[media-en-page.zip] Archive1---Edition1[interactive] Archive2---Edition2a[interactive] Archive2---Edition2b[media-interactive] Archive2---Edition2c[PNG] subgraph Graphic pack Pack end subgraph Archives Archive1 Archive2 end subgraph Editions Edition1 Edition2a Edition2b Edition2c end
%%{init:{"theme":"default"}}%% flowchart LR Pack[Overall pack]---Archive1[public.zip] Pack---Archive2[media-en-page.zip] Archive1---Edition1[interactive] Archive2---Edition2a[interactive] Archive2---Edition2b[media-interactive] Archive2---Edition2c[PNG] subgraph Graphic pack Pack end subgraph Archives Archive1 Archive2 end subgraph Editions Edition1 Edition2a Edition2b Edition2c end
flowchart LR
  Pack[Overall pack]---Archive1[public.zip]
  Pack---Archive2[media-en-page.zip]
  Archive1---Edition1[interactive]
  Archive2---Edition2a[interactive]
  Archive2---Edition2b[media-interactive]
  Archive2---Edition2c[PNG]
  subgraph Graphic pack
    Pack
  end
  subgraph Archives
    Archive1
    Archive2
  end
  subgraph Editions
    Edition1
    Edition2a
    Edition2b
    Edition2c
  end

Where editions are published

Editions are ultimately published to readers on reuters.com or to media clients through Reuters Connect.

%%{init:{"theme":"dark"}}%% flowchart LR Edition1[interactive]-.->Dotcom[reuters.com] Edition2a[interactive]-.->Dotcom Edition2b[media-interactive]-.->Connect[Reuters Connect] Edition2c[PNG]-.->Connect subgraph Editions Edition1 Edition2a Edition2b Edition2c end subgraph Publish locations Dotcom Connect end
%%{init:{"theme":"default"}}%% flowchart LR Edition1[interactive]-.->Dotcom[reuters.com] Edition2a[interactive]-.->Dotcom Edition2b[media-interactive]-.->Connect[Reuters Connect] Edition2c[PNG]-.->Connect subgraph Editions Edition1 Edition2a Edition2b Edition2c end subgraph Publish locations Dotcom Connect end
flowchart LR
  Edition1[interactive]-.->Dotcom[reuters.com]
  Edition2a[interactive]-.->Dotcom
  Edition2b[media-interactive]-.->Connect[Reuters Connect]
  Edition2c[PNG]-.->Connect
  subgraph Editions
    Edition1
    Edition2a
    Edition2b
    Edition2c
  end
  subgraph Publish locations
    Dotcom
    Connect
  end

Some editions can also be "promoted" in Lynx, which makes them available to be inserted in other reuters.com stories as embeddable graphics.

Archive structure

Archives should be structured with an archive folder and one or more nested editon folders:

📂 public/
📂 interactive/
...
📂 media-interctive/
...

When zipped, your archive must be named according to one of the following naming conventions:

  • public.zip
  • media-{locale code}-{.*}.zip
  • {locale code}.zip
  • public-{locale code}.zip
  • media-{locale code}.zip

Locale codes are any valid Language code.

Your archive should contain:

  1. At least one edition folder within the root archive 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, JPG or EPS. You may have more than one edition folder in your archive.

  2. 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: .txt, .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.

File structure for interactive editions

📂 public/
📂 interactive/
- index.html
📂 more-pages/
...
- styles.css
...
- _gfxpreview.png

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.

File structure for media-interactive editions

📂 media-en-page/
📂 media-interactive/
- README.txt
- src.zip
- _gfxpreview.png
📂 JPG/
- map.jpg
- _gfxpreview.png

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:

import fs from 'fs';
import path from 'path';
import archiver from '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');

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'), 'public');
archive.finalize();
};

export default async() => new Promise((resolve, reject) =>
createZip(resolve, reject));

Testing

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

pnpm

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:

pnpm build && pnpm test

You can run specific tests using the grep option in mocha. For example, here is how to run just tests against the Sphinx portal:

pnpm build && pnpm test -g portal

Generated using TypeDoc