Introduction to Quintype APIs
This repository is a knowledge base for developers working on projects backed by the Quintype platform. The live documentation is available as Swagger
Basic Concepts
Stories- Cards and Elements
Quintype provides a structured CMS system. A story is comprised of cards, and cards contain story elements.
Cards
Cardification is a new paradigm targetted towards mobile-first consumption of news. Short and concise chunked blocks of content tend to have much higher engagement. Quintype stories are split into multiple cards. Frontend applications can choose to represent these cards visually, allowing users to interact with these cards directly.
Any API request for stories contain the cards that comprise the story in the "cards" field (as an array). These cards comprise the body of the story.
Story Elements
Story elements are the smallest logical unit in the Quintype platform. Each story element represents a single paragraph of text, image, video, or other unit of content. Story elements form a card. Quintype also tracks these story elements and derives analytics reports based on user engagement on stories containing specific elements.
For eg: A story on tennis containing more photo story elements might get higher engagement.
The story elements can be found in the "story-elements" field of individual cards.
Story Element Types
Each story element has a "type", and optionally a "subtype". There are currently five major types of story elements, which all front end applications must support. They are as follows:
- text - a paragraph of text
- image - an image
- youtube-video - a video on youtube
- soundcloud-audio - a piece of audio on soundcloud
- title - a title for a card (listicle)
- jsembed - arbitrary, unsafe HTML
- composite - an element depending on other story elements
Story Element Subtypes
Story elements may also have a "subtype" field, which gives hints on rendering for clients that know how to render the subtype. For example, a jsembed element may have the twitter subtype. Looking at the metadata, you will find the tweet-id of the particular tweet. Clients may choose to render this element as a jsembed, or as a native twitter element (and provide optimizations such as ensuring that the twitter SDK is only loaded once).
Story Types
Story type are predefined templates which can be used to write articles of various domains ranging from photo blogs, listicles, video stories, blogs etc. Story types give a definite structure and a "starting point" to authors when they start writing a story. They also help in predictive analytics. Data can be derived and studied based on audience engagement on various story types for different domains, for example, a photo story on wildlife may get higher engagement than a text story.
A story type may give subtle hints to the behaviour of the story. For example, a live-blog may choose to auto update every 30seconds.
Stacks
A stack is a group of stories, which can be displayed anywhere on the homepage or other pages. The grouping of stories is done manually (or programatically), using the Sorters interface on the CMS. A closely related concept is the Story Collection.
Sections
Stacks can have stories in the "Home" section (global), as well as within a section. For information on how this works, please see the Story Groups page.
API requests
A stack can be requested using the story-group parameter to the stories API. The Swagger Documentation contains information on how to fetch stories within a stack.
Story Groups
story-group is the most important parameter to the stories API. This parameter indicates which set of stories you'd like to fetch. Currently, the story group can be top, or published, or stack-{{id}}, corresponding to top stories, stories in published order and stacks respectively.
Story groups that are backed by a sorter can be sorted by
top stories
The 'top stories' group is an infinitely long list. This first manually picks sorted stories from the list, then continues with stories in published order (reverse chronological).
published stories
The 'published stories' group returns stories in published order (reverse chronological), skipping the order from the sorters.
stacks
The stack stories will only return stories in the sorter. As an exception, if a sorter for a (stack / section) combination is empty, it will fall back to the stories in (stack / "Home")
Behavior of various story groups
Various story groups behave slightly differently. Each story group typically has a sorter, except for 'published'.
story-group | "Home" | section |
---|---|---|
top | Stories as per sorter, then stories reverse chronologically | Stories as per sorter, then stories in that section reverse chronologically |
stack-id | Stories as per sorter | If the sorter has stories, then it returns the sorted stories. If the sorter is empty, it will return stories for the same stack in the "Home" section |
Breaking News
Breaking news can be used to quickly push current events to users as they are happening. These stories may not be fully formed, and only require a headline. These Breaking news events can be linked to a story, after the push has happened.
Linking to another story
The breaking news story can be linked to an existing story, or added later. Clicking on the breaking news opens the story. The breaking news API automatically fetches the latest headline and slug of the linked story.
Story Group
The breaking news API accepts a story-group parameter, to give access to another breaking news sorter.
API Documentation
Please see the swagger documentation for details on how to use the API.
Entities
Entity provide structured information which is relevant to a publisher. An entity can be referred in relevant stories.
The representation of linked entities on the front end is contextual to the purpose for which it is referred in stories. Example of entity is a person (referred to as ‘entity type’) which has the structure such as name, avatar, email address, social handle, bio, company associated with it.
Entity types can only be created by the Quintype team, based on a publisher's requirement. Entity type can contain text, numerics, image, or another entity.
Publishers can add values (referred to as entities) to the entity type by using the ‘Entity Manager’ and these values can be referred to in stories. The Entity manager can also be used to modify existing entities.
There are three endpoints to access entities:
1. Getting all the entities
Use the GET ENTITIES API to get all the entities created.
2. Getting particular entity details.
Use the GET ENTITY API with the entity-id.
3. Getting the nested entities linked to an entity
Use the GET ENTITY API with the root entity-id followed by the nested-entity key.
Architecting a front end application
Front End Application on the Quintype platform should be built in such a way that they can be served from a CDN cache. This drastically reduces load time, and the amount of load on the backend server. However, this puts a few constraints on the architecture of many pages.
All pages that want to be cached
- Cannot access the current user, or check if the user is logged in
- Cannot check the current device type
Logged In Content
As a general practice, it's important to render the page as a logged out page, so that it can be served from the CDN. The page can then be updated via AJAX queries. In case the page must be rendered server side, with logged in content (ex: Personalized Home Page), then mark the page as uncacheable. Do keep in mind that all calls to /api/v1/...
will automatically pass the session-cookie
cookie to the API server, which will in turn be able to find out who the user is.
Getting the logged in user on the backend
Sketches and the client app share the session-cookie
cookie. However, it’s an encrypted object only sketches can read. In order to read the current member, call /api/v1/members/me
, passing in the value of session-cookie
as the X-QT-AUTH
header, to get the current user. Don’t bother saving a separate.
Checking Device Type
As the content is cached, it is not permitted to check for Mobile / Tablet / Desktop or other Form Factor. Please use responsive CSS to style the content as required. If it is not possible to avoid device checks, please mark the page as uncacheable.
Cache Headers and Keys
The following HTTP Headers should be returned with every HTTP response which should be cached.
Cache-Control: public,max-age=0
Surrogate-Control: public,max-age=30,stale-while-revalidate=120,stale-if-error=3600
Surrogate-Key: q/55/top/home s/55/d7f8965d s/55/b93cbb75 s/55/2eb36ec1 ss/55/fae74aa1
Vary: Accept-Encoding
The Cache-Control
header is passed on the the browser, while the Surrogate-Key
is processed by the CDN provider. However, they behave very similar. The options are as follows.
public
/private
/no-cache
- Please usepublic
if the page is cacheable, andprivate,no-cache
otherwise.max-age=n
- This controls how long the page is considered fresh in the Databasestale-while-revalidate=n
- During this period, the page is served from CDN, but updated in the background.stale-if-error=n
- During this time, the page is served from CDN in case the backend server crashes for whatever reason.
Cache Keys
Currently, we support the following keys, which will be invalidated by the Quintype editor when a story or group updates. Currently, the following keys are supported:
s/$publisher-id/$story-id
where story-id is the first 8 digits of the story id.q/$publisher-id/$story-group/$location-id
where story-group istop
orstack-$id
, and location ishome
orsection-id
.
If you would like a soft purge on this key, prefix the key above with an s
.
Cookies
It is very important that all cookies are stripped from requests which are to be cached. This can be enforced by the following methods:
Failures
In order for the CDN to serve error pages (in case the site is down), the backend server must return a 5xx
response.
Our CDN treats 404
and other status codes < 500
as intentional failures, and these pages will be served instead of using a page marked with stale-if-error
.
Marking a page uncacheable
To mark a page as uncacheable, please use the following header.
Cache-Control: private,no-cache
Implementing a Front End Application
Images
Quintype currently uses Imgix to display images in various aspect ratios.
Base Url
A image's URL can be obtained by appending the "image-s3-key" to "http://quintype-01.imgix.net/". For example, the hero image whose s3 key is "quintype-demo/1234/foo.png" is "http://quintype-01.imgix.net/quintype-demo/1234/foo.png". This is the image in it's original resolution, and can be transformed using any of Imgix's Transforms.
Focus Point
var FocusedImage = require("quintype-js").FocusedImage;
var image = new FocusedImage('quintype-demo/1234/foo.png', metadata);
var output_url = "http://quintype-01.imgix.net/" + image.path([16, 9], {w: 640});
Our editor allows the placement of a focus point on any image. The placement of a focus point guarantees that that point is always present in the viewport when the image is cropped, across different aspect ratios.
It is recommended that the focus point is used in conjuncture with the picture spec of HTML5, in order to show images at different aspect ratios on different devices. Using an object-fit: cover;
will further center the image.
The refrence implementation for the focus point algorithm can be found in the javascript implementation.
The focus final image url can be calculated with the following pseudo code (for a 16x9 image with final width 640)
Recommended Transforms
var transforms = {
w: 640, // actual width
q: 60, // quality
auto: format // use WebP when available
fm: pjpg // fallback to progressive jpeg
};
In order to save bandwidth, and provide a good experience, we suggest you use the following transforms:
The list of all transforms can be found in the Imgix Documentatation.
Preview
var StoryPreview = (function() {
window.addEventListener("message", function(event){
var template = getLiquidTemplate("pages/story");
var story = event.data['story'];
if (story) {
$("#story-preview").html(template.render({story: story, preview: true}));
// Do other things to make the page work correctly, such as post load JS
}
});
});
Technical Overview
The preview functionality is implemented with a combination of the backend editor, and the front end UI. The front end page has to implement two routes. /preview/story
and /preview/home
. The editor fetches this page via either HTTP or HTTPS, then served in an IFrame on the editor (via HTTPS). As the story is updated in the editor, the story is passed to the page via the message API, and the page is expected to rerender on the Front End.
HTTPs Only
As the content is loaded via HTTPS, any HTTP content (external JS, etc...) will not render.
Example
On the right is sample code explaining how to listen via the IFrame message API.
Testing
window.postMessage({story: {"author-name":"Tapan Bhat","headline":"The Greatest !","slug":"sports/2016/06/08/the-greatest","last-published-at":1465407509866,"alternative":{},"sections":[{"id":82,"name":"Sports"}],"hero-image-metadata":{"width":2133,"height":1906,"focus-point":[988,258]},"published-at":1465407509866,"id":"1d2fc836-4113-4ae1-8735-377167664892","hero-image-s3-key":"quintype-demo/2016-06/cca8f31e-9264-4ee2-9af0-08eb53be2a26/ABP-1.jpg","author-id":2041,"first-published-at":1465407509866,"story-template":"photo"}, "action": "reloadStory"}, window.location.origin)
Head to /preview/home, then paste the following in the console. The page should immediately render with the sample story.
The /preview/story should also behave similarly, but render the story.
Isomorphic Rendering
In order to render from both the server side, and the client side, it is suggested that you use a templating language capable of Isomorphic Rendering. Some suggestions are Liquid Templates or Twig
Voting and Rating
End users have the ability vote on/rate a specific story. Potential applications include upvote/downvote, like/dislike, star rating etc.
Each story has multiple types of ratings ('magnitude') whose value can be incremented using an API.
1. Configuring allowed magnitudes
This is a one-time setup, where the publisher needs to come up with a list of allowed magnitudes for their voting system. It could be ['upvote', 'downvote']
, ['like', 'dislike']
, [1, 2, 3, 4, 5]
, etc.
Currently, there is no API to set this. Please contact us to make this change.
2. Authentication
Make sure the user is logged in, via one of our supported authentication methods.
Currently, we do not support anonymous voting.
3. Vote
Use the POST Vote API to vote on an article. You need to send an allowed magnitude.
4. Get votes for one or more stories
Use any one of the GET Stories APIs, but make sure you pass votes
in the fields list.
5. Get votes for a story for a user
Use the GET Vote API to get the votes for a particular story, for the currently logged-in member. Possible uses include finding if a user has voted or not, and the magnitude of their vote.
Social Logins
Facebook:
- Create an app at https://developers.facebook.com
- Click on ‘Get started’ against Facebook Login.
- Under Valid OAuth redirect URIs in Facebook Login settings, add the following urls:
- site_url/auth
- site_url/auth/facebook/callback
- site_url/admin/add-social.callback
- In the app’s Basic settings, click on Add Platform and add Website with the site_url.
- Insert the App ID and App secret generated in our database.
- Now, to login to our site using Facebook login, call the URL: site_url/login?auth-provider=facebook&remote-host=site_url
- To get the details of the logged in user, call api_host/api/v1/members/me
Twitter:
- Create an app at https://apps.twitter.com
- Enter the basic details
- Insert API key and API secret generated in our database.
- Now, to login to our site using Facebook login, call the URL: site_url/login?auth-provider=twitter&remote-host=site_url
- To get the details of the logged in user, call api_host/api/v1/members/me
Sample Applications and Libraries
We have published a number of starter-kits for various popular languages. Please clone these, and use this as a starting point.
Front End Javascript
- The Image resizing functions is available via the quintype-js npm module
Ruby on Rails
The Ruby on Rails starter kit can be found here: Coconut. The following libraries are used (and patches appreciated)
PHP (with Laravel)
The PHP / Laravel application can be found here: Pina Colada
Node.js (with React)
Node.js application can be found here: Pina Colada
Analytics
Web Tracking (qlitics.js)
<!-- Qlitics Snippet -->
<script>
window.qlitics=window.qlitics||function(){(qlitics.q=qlitics.q||[]).push(arguments);};
qlitics('init');
qlitics('track', 'page-view', {'page-type': <page_type> });
</script>
<script async src='/qlitics.js'></script>
<!-- End Qlitics Snippet -->
The qlitics.js
Javascript library is used for tracking user interaction with the frontend website.
The frontend website needs to implement a route /qlitics.js
that proxies to the same route on API_HOST
. It also needs to embed the following script before the closing </head>
tag (or as early as possible).
<page_type>
is a placeholder for current page's type. The qlitics.js
snippet served by API_HOST
has the correct publisher-id
hardcoded in it.
Google AMP Tracking
<!-- Qlitics Tracking -->
<amp-analytics>
<script type="application/json">
{
"requests": {
"storyview":"<qlitics_host>/api/${random}/amp?publisher-id=${publisherId}&event-type=${eventType}&story-content-id=${storyContentId}&url=${ampdocUrl}&referrer=${documentReferrer}"
}
,
"vars": {
"publisherId": <publisher_id>, "storyContentId": "<story_content_id>"
}
,
"triggers": {
"trackStoryview": {
"on":"visible",
"request":"storyview",
"vars": {
"eventType": "story-view"
}
}
}
}
</script>
</amp-analytics>
<!-- End Qlitics Tracking -->
<publisher_id>
, <qlitics_host>
<story_content_id>
are placeholders and need to be replaced with equivalent placeholders as per the templating language being used.
Automatically collected data
The snippet automatically records the following data
- Current page url
- Geographic location of the user
- Operating System and browser being used
- Referring site
- User agent
API Methods
qlitics('init');
init
Initializes the tracker. Should be the first api to be called. It generates (or reuses) the device tracking id and the session id.
qlitics('set', 'member-id', 1234);
set
Used to set a property on the tracker. Accepts aproperty
and avalue
for that property.property
String. One of the settable properties.value
Any. Value for the property. Check documentation for the specific property.
qlitics('track', 'page-view', {'page-type': 'home'});
qlitics('track', 'story-view', {
'story-content-id': '9b2fe90f-b155-4624-862e-88c981c9da6c',
});
qlitics('track', 'story-element-view', {
'story-content-id': '9b2fe90f-b155-4624-862e-88c981c9da6c',
'story-version-id': 'bc1295de-1b29-4588-8822-3949510b5fd6',
'card-content-id': '505d5c9d-e776-4f17-bd53-8dd8d579122d',
'card-version-id': 'abfcabf3-6dcc-4791-a87e-16a36c1b1ae6',
'story-element-id': '1f97a56d-be01-4a2d-b319-0e88cf9a2259',
'story-element-type': 'youtube-video',
});
qlitics('track', 'story-element-action', {
'story-content-id': '9b2fe90f-b155-4624-862e-88c981c9da6c',
'story-version-id': 'bc1295de-1b29-4588-8822-3949510b5fd6',
'card-content-id': '505d5c9d-e776-4f17-bd53-8dd8d579122d',
'card-version-id': 'abfcabf3-6dcc-4791-a87e-16a36c1b1ae6',
'story-element-id': '1f97a56d-be01-4a2d-b319-0e88cf9a2259',
'story-element-type': 'youtube-video',
'story-element-action': 'play',
});
qlitics('track', 'story-share', {
'story-content-id': '9b2fe90f-b155-4624-862e-88c981c9da6c',
'social-media-type': 'facebook',
'url': 'https://publisher-domain.com/story-slug',
});
track
Used to track one of the following user interaction events.page-view
Should be called on every page load. Shouldn't be called for pages loaded via ajax. Accepts a hashmap specifying thepage-type
. Refer common API fields.story-view
Should be called when a story page is loaded. A story view depends on a page-view and should be called only after apage-view
has been tracked. If additional stories are being loaded via ajax, then this event should be tracked for each of those stories as well. This will reuse the initially triggered page-view's identifier. Refer common API fields.story-element-view
Should be called the first time a story element comes into the browser's viewport. This is used to track which all story elements did the user actually view and how much time was spent on that story. Refer common API fields.story-element-action
Should be called to track any user interaction with a story element. For example, the playing and pausing of a youtube video, as well as whether the user saw the entire video or not can be tracked. Refer common API fields.story-share
Should be called when a user shares a story on social media via the story page. Refer common API fields.
Settable API Properties
Common API Fields
card-content-id
String UUID. The id for the card.
card-version-id
String UUID. The current version of the card.
member-id
Int. A unique identifier for the currently logged in member.
page-type
String. A short identifier for the current page. Standard values for some of the pages,
home
for the home pagestory
for the story pagesection
for the various section and sub-section pagestopic
for the various tag and topic pages
For the remaining pages, any value can be provided.
social-media-type
String. A tag to refer to the social media service. Ex facebook
, twitter
, email
, whatsapp
, google_plusone_share
, linkedin
etc
story-content-id
String UUID. The id for the current story being displayed.
story-element-action
String. The user interaction being tracked with a story element. The value will depend on the element type and iteraction being recorded.
- For the
video
story element, action can be one of the following:play
when the video starts playing or when playback is resumedpause
when the video playback has been pausedcomplete
when the video has finished playing
story-element-id
String UUID. The id for the story element.
story-element-type
String. The type of the story element. Check the docs for available types.
story-version-id
String UUID. The current version of the story.
Testing
The snippet generates a tracking pixel for every api call made. This pixel is embedded in the DOM and deleted immediately after call succeeds. If the api calls are being made properly, then there should be entries for the resource named event.gif
in the network tab of the browser's developer console. The request for this resource should have a data
query parameter whose value should be the base64 encoded payload. Decode the data and verify the correct payload is being passed. The server will return a non 200 response if the payload is incorrect.
Deployments and Infrastructure
Default URLs
The Quintype application is made up of at least 3 host names.
- The editor
- The API server
- The front end application
Staging
Staging urls are typically hosted by Quintype, and will have the following URLs.
- editor -
thefoobar.staging.quintype.com
- api server -
thefoobar.qtstage.io
- front end -
thefoobar-web.qtstage.io
Production
Production urls typically have the editor and API server on the quintype domains, and the front end on the final domain
- editor -
thefoobar.quintype.com
- api server -
thefoobar.quintype.io
/thefoobar.internal.quintype.io
(accessible within QCC) - front end -
www.thefoobar.com
(this should benext.thefoobar.com
before going live) - beta front end -
beta.thefoobar.com
Black Knight
During the process of creating your account, the Quintype team will also create a publisher (The Foobar) on the Black Knight application. You will be able to deploy all your front end applications from the same place.
Production DNS Entries
www.thefoobar.com CNAME thefoobar.publisher.quintype.io
next.thefoobar.com CNAME thefoobar.publisher.quintype.io
beta.thefoobar.com CNAME thefoobar.publisher.quintype.io
thefoobar.com A 174.129.25.170 # Redirect to www.thefoobar.com using http://wwwizer.com/naked-domain-redirect
The ON the right is an example for the DNS entries to be made
We strongly recommend that you serve your content off a www
domain, and use the apex to forward 301 to your www domain. However, do use a provider that sets a Cache-Control
header with the redirect (like GoDaddy).
# If you serve domain from an APEX domain
thefoobar.com A 151.101.0.204
thefoobar.com A 151.101.64.204
thefoobar.com A 151.101.128.204
thefoobar.com A 151.101.192.204
If your main site is on an APEX domain, then you must configure A records to our CDN.
SSL / HTTPS
If your site requires SSL / HTTPS, contact us support@quintype.com
Deploying with Black Knight
The Black Knight application is a tool to deploy your front end code across the Quintype Client Cloud
Overall Workflow
The deployment process has three steps
Compilation Steps
- Changes to the app are made locally, and pushed up to GitHub.
- Docker Hub picks up the changes from GitHub, and builds a docker tag.
Deployment Steps
- Enter the docker tag that is to be deployed. And click deploy.
- Black Knight will automatically copy in config files, and create an 'immutable tag'
- Black Knight will then deploy this 'immutable tag' across the Quintype network.
Setting Up Configuration
Black Knight can be configure to copy in configuration files. These configuration files are per environment, and allow you to have different behavior across environments, and can be used for many use cases, such as
- Changing the API host on staging
- Config to disable SEO on beta / staging
- Changing secret keys
Advanced Topics
RSS Feed
The stories RSS feed can be used to syndicate stories out from the CMS
End point
/stories.rss
-> end point for RSS feed- This end point generates RSS feed for last four hours
Supported parameters
Parameter to fetch stories with different time durations
/stories.rss?time-period=last-24-hours
-> Fetches RSS feed with stories from last 24 hours/stories.rss?time-period=last-7-days
-> Fetches RSS feed with stories from last 7 days/stories.rss?time-period=last-1-month
-> Fetches RSS feed with stories from last 1 month
Parameter to fetch stories by section slug
- section slug can be obtained from
api/v1/config
/stories.rss?section=section-slug
-> Ex:/stories.rss?section=sponsored-content
This end point fetches stories from Sponsored Content section.
- section slug can be obtained from
Parameter to fetch stories by section id(Preferable way to fetch stories by section)
- section id can be obtained from
api/v1/config
/stories.rss?section-id=id
-> Ex:/stories.rss?section-id=2809
This end point fetches stories from Sponsored Content section.
- section id can be obtained from
Parameter to skip syndicated stories
/stories.rss?skip=value
-> This removes syndicated stories from a particular source; Example/stories.rss?skip=bloomberg
on bloombergquint skips stories syndicated from bloomberg/stories.rss?skip=all
-> This all value as parameter removes all syndicated stories- This parameter supports comma separated values; Example
/stories.rss?skip=bloomberg,thequint
-> This filters out stories syndicated from bloomberg, thequint on bloomberg quint
Parameter to fetch stories from a sorter
/stories.rss?story-group=story-group
-> This fetches stories by sorter.- The story group for any sorter can be obtained from
api/v1/config
- This doesn't have the time limit of 4 hours. This just pulls the stories from a sorter.
Parameters to fetch stories by excluding stories from particular section(s).
/stories.rss?exclude-section-ids=section-ids
-> This fetches stories by excluding stories from section-id(s) lister; Example:/stories.rss?exclude-section-ids=2435
this fetches stories and filters out stories(if there are any) with section id 2435.- section-ids can be obtained from
/api/v1/config
- this parameter supports multiple sections ids as comma separated values. This helps filtering out stories based on multiple sections