Skip to content

Financial-Times/content-tree

Repository files navigation

content-tree

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.

Contents

Introduction

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.

What is content-tree?

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.

How to use the types

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
	}
}

What does it mean to be in transit?

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".

Abstract Types

These abstract helper types define special types a Parent can use as children.

BodyBlock

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.

LayoutWidth

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.

Phrasing

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.

Nodes

Node

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.

Parent

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.

Root

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.

Body

interface Body extends Parent {
	type: "body"
	version: number
	children: BodyBlock[]
}

Body (Parent) represents the body of an article.

(note: bodyTree is just this part)

Text

interface Text extends Node {
	type: "text"
	value: string
}

Text (Literal) represents text.

Break

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.

ThematicBreak

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.

Paragraph

interface Paragraph extends Parent {
	type: "paragraph"
	children: Phrasing[]
}

Paragraph represents a unit of text.

Heading

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.

Strong

interface Strong extends Parent {
	type: "strong"
	children: Phrasing[]
}

Strong represents contents with strong importance, seriousness or urgency.

Emphasis

interface Emphasis extends Parent {
	type: "emphasis"
	children: Phrasing[]
}

Emphasis represents stressed emphasis of its contents.

Strikethrough

interface Strikethrough extends Parent {
	type: "strikethrough"
	children: Phrasing[]
}

Strikethrough represents a piece of text that has been stricken.

Link

interface Link extends Parent {
	type: "link"
	url: string
	title: string
	children: Phrasing[]
}

Link represents a hyperlink.

List

interface List extends Parent {
	type: "list"
	ordered: boolean
	children: ListItem[]
}

List represents a list of items.

ListItem

interface ListItem extends Parent {
	type: "list-item"
	children: (Paragraph | Phrasing)[]
}

Blockquote

interface Blockquote extends Parent {
	type: "blockquote"
	children: (Paragraph | Phrasing)[]
}

Blockquote represents a quotation.

Pullquote

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.

ImageSet

interface ImageSet extends Node {
	type: "image-set"
	id: string
	external picture: ImageSetPicture
}

Image types

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

Image
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].

ImageSource

type ImageSource = {
	url: string
	width: number
	dpr: number
}

ImageSource defines a single resource for an image.

Recommended

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.

Teaser types

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
	}
}

Tweet

interface Tweet extends Node {
	id: string
	type: "tweet"
	external html: string
}

Tweet represents a tweet.

Flourish

interface Flourish extends Node {
	type: "flourish"
	id: string
	layoutWidth: string
	flourishType: string
	description?: string
	timestamp?: string
	fallbackImage?: Image
}

Flourish represents a flourish chart.

BigNumber

interface BigNumber extends Node {
	type: "big-number"
	number: string
	description: string
}

BigNumber represents a big number.

Video

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?

YoutubeVideo

interface YoutubeVideo extends Node {
	type: "youtube-video"
	url: string
}

YoutubeVideo represents a video referenced by a Youtube URL.

ScrollyBlock

interface ScrollyBlock extends Parent {
	type: "scrolly-block"
	theme: "sans" | "serif"
	children: ScrollySection[]
}

ScrollyBlock represents a block for telling stories through scroll position.

ScrollySection

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

ScrollyImage

interface ScrollyImage extends Node {
	type: "scrolly-image"
	id: string
	external picture: ImageSetPicture
}

ScrollyImage represents an image contained in a ScrollySection

ScrollyCopy

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.

Layout

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.

LayoutSlot

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.

LayoutImage

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>.

Table

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.

CustomCodeComponent

interface CustomCodeComponent extends Parent {
  type: "custom-code-component"
  /** Repository for the code of the component in the format "[github org]/[github repo]/[component name]". */
  path: string
  /** Semantic version of the code of the component, e.g. "^0.3.5". */
  versionRange: string
  /** Last date-time where the attributes for this block were modified, in ISO-8601 format. */
  attributesLastModified: string
  /** A unique identifier for this instance */
  id: string
  /** How the component should be presented in the article page according to the column layout system */
  layoutWidth: LayoutWidth
  /** Configuration data to be passed to the component. */
  attributes: {
    [key: string]: string | boolean | undefined
  }
  children: (ImageSet | Paragraph | CustomCodeComponent)[]
}
  • 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).

License

This software is published by the Financial Times under the MIT licence.

Derived from unist © Titus Wormer

About

A structured definition for Financial Times articles, and its utilities

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published