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.
npm i -D @reuters-graphics/server-client
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://...'
// }
// }
// 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);
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.
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.
Graphics in RNGS are composed of three main parts:
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
Editions are ultimately published to readers on reuters.com or to media clients through Reuters Connect.
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.
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:
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.
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.
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.
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.
If you are creating the archive in node, use the archiver node package.
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));
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 usernamePASSWORD
: Sphinx passwordAPI_KEY
: Sphinx API keySPHINX_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