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-treeUse 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
| Flourish
| BigNumber
| CustomCodeComponent
| Layout
| List
| Blockquote
| Pullquote
| ScrollyBlock
| ThematicBreak
| Table
| Recommended
| Tweet
| Video
| YoutubeVideo
| TextBodyBlock 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 | LinkA 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"
fragmentIdentifier?: string
}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[]
styleType?: 'onward-journey'
}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
fragmentIdentifier?: string
}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.
type FlourishLayoutWidth = Extract<LayoutWidth, "full-grid" | "in-line">
interface Flourish extends Node {
type: "flourish"
id: string
layoutWidth: FlourishLayoutWidth
flourishType: string
description?: string
timestamp?: string
external fallbackImage?: Image
fragmentIdentifier?: string
}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
external title: string
}Video represents for an FT video referenced by a URL.
The title can be obtained by fetching the Video from the content API.
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'
}
type TableLayoutWidth = Extract<LayoutWidth,
| 'auto'
| 'full-grid'
| 'inset-left'
| 'inset-right'
| 'full-bleed'>
interface TableCaption extends Parent {
type: 'table-caption'
children: Phrasing[]
}
interface TableCell extends Parent {
type: 'table-cell'
heading?: boolean
columnSpan?: number
rowSpan?: number
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: TableLayoutWidth
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
