A tree for Financial Times article content.
content-tree is a specification for representing Financial Times article content as an abstract tree. It implements the unist spec.
This document defines a format for representing Financial Times article content
as a tree. This specification is written in a
typescript-like grammar, augmented by the
addition of the external
property modifier.
The external
property modifier indicates that the specified field is absent
when the content-tree
is in
transit, and required when the
content-tree is at rest.
content-tree
extends unist, a format for syntax trees, to benefit
from its ecosystem of utilities.
content-tree
relates to JavaScript in that it has an ecosystem of
utilities for working with trees in JavaScript. However,
content-tree
is not limited to JavaScript and can be used in other programming
languages.
We provide two namespaces in content-tree.d.ts
, which is automatically
generated from this README. ContentTree
and ContentTree.transit
.
Install this repository as a dependency:
npm install https://github.com/Financial-Times/content-tree
Use it in your code:
import type {ContentTree} from "@financial-times/content-tree"
function makeBigNumber(): ContentTree.BigNumber {
return {
type: "|<tab>"
+--------------+
| "big-number" |
+--------------+
}
}
function makeImageSetNoFixins(): ContentTree.transit.ImageSet {
return {
type: "image-set",
id: string,
// if you try to add a `picture` here it will get mad
}
}
When a content-tree
is being rendered visually, external resources have been
fetched and added to the tree. When the content-tree
is being transmitted
across the network, these external resources are referenced only by their id
.
It is the state of the tree in the network that we call "in transit".
These abstract helper types define special types a Parent can use as children.
type BodyBlock =
| Paragraph
| Heading
| ImageSet
| BigNumber
| CustomCodeComponent
| Layout
| List
| Blockquote
| Pullquote
| ScrollyBlock
| ThematicBreak
| Table
| Recommended
| Tweet
| Video
| YoutubeVideo
BodyBlock
nodes are the only things that are valid as the top level of a Body
.
type LayoutWidth =
| "auto"
| "in-line"
| "inset-left"
| "inset-right"
| "full-bleed"
| "full-grid"
| "mid-grid"
| "full-width"
LayoutWidth
defines how the component should be presented in the article page according to the column layout system.
type Phrasing = Text | Break | Strong | Emphasis | Strikethrough | Link
A phrasing node cannot have ancestor of the same type.
i.e. a Strong will never be inside another Strong, or inside any other node that is inside a Strong.
interface Node {
type: string
data?: any
}
The abstract node. The data field is for internal implementation information and will never be defined in the content-tree spec.
interface Parent extends Node {
children: Node[]
}
Parent (UnistParent) represents a node in content-tree containing other nodes (said to be children).
Its content is limited to only other content-tree content.
interface Root extends Node {
type: "root"
body: Body
}
Root (Parent) represents the root of a content-tree.
Root can be used as the root of a tree.
interface Body extends Parent {
type: "body"
version: number
children: BodyBlock[]
}
Body (Parent) represents the body of an article.
(note: bodyTree
is just this part)
interface Text extends Node {
type: "text"
value: string
}
Text (Literal) represents text.
interface Break extends Node {
type: "break"
}
Break Node represents a break in the text, such as in a poem.
Non-normative note: this would normally be represented by a <br>
in the
html.
interface ThematicBreak extends Node {
type: "thematic-break"
}
ThematicBreak Node represents a break in the text, such as in a shift of topic within a section.
Non-normative note: this would be represented by an <hr>
in the html.
interface Paragraph extends Parent {
type: "paragraph"
children: Phrasing[]
}
Paragraph represents a unit of text.
interface Heading extends Parent {
type: "heading"
children: Text[]
level: "chapter" | "subheading" | "label"
}
Heading represents a unit of text that marks the beginning of an article section.
interface Strong extends Parent {
type: "strong"
children: Phrasing[]
}
Strong represents contents with strong importance, seriousness or urgency.
interface Emphasis extends Parent {
type: "emphasis"
children: Phrasing[]
}
Emphasis represents stressed emphasis of its contents.
interface Strikethrough extends Parent {
type: "strikethrough"
children: Phrasing[]
}
Strikethrough represents a piece of text that has been stricken.
interface Link extends Parent {
type: "link"
url: string
title: string
children: Phrasing[]
}
Link represents a hyperlink.
interface List extends Parent {
type: "list"
ordered: boolean
children: ListItem[]
}
List represents a list of items.
interface ListItem extends Parent {
type: "list-item"
children: (Paragraph | Phrasing)[]
}
interface Blockquote extends Parent {
type: "blockquote"
children: (Paragraph | Phrasing)[]
}
Blockquote represents a quotation.
interface Pullquote extends Node {
type: "pullquote"
text: string
source?: string
}
Pullquote represents a brief quotation taken from the main text of an article.
non normative note: the reason this is string properties and not children is that it is more confusing if a pullquote falls back to text than if it doesn't. The text is taken from elsewhere in the article.
interface ImageSet extends Node {
type: "image-set"
id: string
external picture: ImageSetPicture
}
type ImageSetPicture = {
layoutWidth: string
imageType: "image" | "graphic"
alt: string
caption: string
credit: string
images: Image[]
fallbackImage: Image
}
ImageSetPicture
defines the data associated with an ImageSet
type Image = {
id: string
width: number
height: number
format:
| "desktop"
| "mobile"
| "square"
| "square-ftedit"
| "standard"
| "wide"
| "standard-inline"
url: string
sourceSet?: ImageSource[]
}
Image
defines a single use-case of a Picture[#ImageSetPicture].
type ImageSource = {
url: string
width: number
dpr: number
}
ImageSource defines a single resource for an image.
interface Recommended extends Node {
type: "recommended"
id: string
heading?: string
teaserTitleOverride?: string
external teaser: Teaser
}
- Recommended represents a reference to an FT content that has been recommended by editorial.
- The
heading
, when present, is used where the purpose of the link is more specific than being "Recommended" (an example might be "In depth") - The
teaserTitleOverride
, when present, is used in place of the content title of the link.
non normative note: historically, recommended links used to be a list of up to three content items. Testing later showed that having one more prominent link was more engaging, and Spark (and therefore content-tree)now only supports that use case.
These types were extracted from x-dash's x-teaser.
type TeaserConcept = {
apiUrl: string
directType: string
id: string
predicate: string
prefLabel: string
type: string
types: string[]
url: string
}
type Teaser = {
id: string
url: string
type:
| "article"
| "video"
| "podcast"
| "audio"
| "package"
| "liveblog"
| "promoted-content"
| "paid-post"
title: string
publishedDate: string
firstPublishedDate: string
metaLink?: TeaserConcept
metaAltLink?: TeaserConcept
metaPrefixText?: string
metaSuffixText?: string
indicators: {
accessLevel: "premium" | "subscribed" | "registered" | "free"
isOpinion?: boolean
isColumn?: boolean
isPodcast?: boolean
isEditorsChoice?: boolean
isExclusive?: boolean
isScoop?: boolean
}
image: {
url: string
width: number
height: number
}
}
interface Tweet extends Node {
id: string
type: "tweet"
external html: string
}
Tweet represents a tweet.
interface Flourish extends Node {
type: "flourish"
id: string
layoutWidth: string
flourishType: string
description?: string
timestamp?: string
fallbackImage?: Image
}
Flourish represents a flourish chart.
interface BigNumber extends Node {
type: "big-number"
number: string
description: string
}
BigNumber represents a big number.
interface Video extends Node {
type: "video"
id: string
embedded: boolean
}
Video represents for an FT video referenced by a URL.
TODO: Figure out how Clips work, how they are different?
interface YoutubeVideo extends Node {
type: "youtube-video"
url: string
}
YoutubeVideo represents a video referenced by a Youtube URL.
interface ScrollyBlock extends Parent {
type: "scrolly-block"
theme: "sans" | "serif"
children: ScrollySection[]
}
ScrollyBlock represents a block for telling stories through scroll position.
interface ScrollySection extends Parent {
type: "scrolly-section"
display: "dark-background" | "light-background"
noBox?: true,
position: "left" | "center" | "right"
transition?: "delay-before" | "delay-after"
children: [ScrollyImage, ...ScrollyCopy[]]
}
ScrollySection represents a section of a ScrollyBlock
interface ScrollyImage extends Node {
type: "scrolly-image"
id: string
external picture: ImageSetPicture
}
ScrollyImage represents an image contained in a ScrollySection
interface ScrollyCopy extends Parent {
type: "scrolly-copy"
children: (ScrollyHeading | Paragraph)[]
}
ScrollyCopy represents a collection of ScrollyHeading or Paragraph nodes.
interface ScrollyHeading extends Parent {
type: "scrolly-heading"
level: "chapter" | "heading" | "subheading"
children: Text[]
}
ScrollyHeading represents a heading within a ScrollyCopy block.
interface Layout extends Parent {
type: "layout"
layoutName: "auto" | "card" | "timeline"
layoutWidth: string
children: [Heading, LayoutImage, ...LayoutSlot[]] | [Heading, ...LayoutSlot[]] | LayoutSlot[]
}
Layout nodes are a generic component used to display a combination of other nodes (headings, images and paragraphs) in a visually distinctive way.
The layoutName
acts as a sort of theme for the component.
interface LayoutSlot extends Parent {
type: "layout-slot"
children: (Heading | Paragraph | LayoutImage)[]
}
A Layout can contain a number of LayoutSlots, which can be arranged visually
Non-normative note: typically these would be displayed as flex items, so they would appear next to each other taking up equal width.
interface LayoutImage extends Node {
type: "layout-image"
id: string
alt: string
caption: string
credit: string
external picture: ImageSetPicture
}
- LayoutImage is a workaround to handle pre-existing articles that were
published using
<img>
tags rather than<ft-content>
images. The reason for this was that in the bodyXML, layout nodes were inside an<experimental>
tag, and that didn't support publishing<ft-content>
.
type TableColumnSettings = {
hideOnMobile: boolean
sortable: boolean
sortType: 'text' | 'number' | 'date' | 'currency' | 'percent'
}
interface TableCaption extends Parent {
type: 'table-caption'
children: Phrasing[]
}
interface TableCell extends Parent {
type: 'table-cell'
heading?: boolean
children: Phrasing[]
}
interface TableRow extends Parent {
type: 'table-row'
children: TableCell[]
}
interface TableBody extends Parent {
type: 'table-body'
children: TableRow[]
}
interface TableFooter extends Parent {
type: 'table-footer'
children: Phrasing[]
}
interface Table extends Parent {
type: 'table'
stripes: boolean
compact: boolean
layoutWidth:
| 'auto'
| 'full-grid'
| 'inset-left'
| 'inset-right'
| 'full-bleed'
collapseAfterHowManyRows?: number
responsiveStyle: 'overflow' | 'flat' | 'scroll'
children: [TableCaption, TableBody, TableFooter] | [TableCaption, TableBody] | [TableBody, TableFooter] | [TableBody]
columnSettings: TableColumnSettings[]
}
Table represents 2d data.
type CustomCodeComponentAttributes = {
[key: string]: string | boolean | undefined
}
interface CustomCodeComponent extends Node {
/** Component type */
type: "custom-code-component"
/** Id taken from the CAPI url */
id: string
/** How the component should be presented in the article page according to the column layout system */
layoutWidth: LayoutWidth
/** Repository for the code of the component in the format "[github org]/[github repo]/[component name]". */
external path: string
/** Semantic version of the code of the component, e.g. "^0.3.5". */
external versionRange: string
/** Last date-time when the attributes for this block were modified, in ISO-8601 format. */
external attributesLastModified: string
/** Configuration data to be passed to the component. */
external attributes: CustomCodeComponentAttributes
}
- The CustomCodeComponent* allows for more experimental forms of journalism, allowing editors to provide properties via Spark.
- The component itself lives off-platform, and an example might be a git repository with a standard structure. This structure would include the rendering instructions, and the data structure that is expected to be provided to the component for it to render if necessary.
- The basic interface in Spark to make reference to this system above (eg. the git repo URL or a public S3 bucket), and provide some data for it if necessary. This will be the Custom Component storyblock.
- The data Spark receives from entering a specific ID will be used to render dynamic fields (the
attributes
).
This software is published by the Financial Times under the MIT licence.
Derived from unist © Titus Wormer