@financial-times/cp-content-pipeline-schema 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/.toolkitrc.yml +12 -0
  2. package/CHANGELOG.md +72 -0
  3. package/jest.config.js +3 -0
  4. package/lib/concept.d.ts +7 -0
  5. package/lib/concept.js +39 -0
  6. package/lib/concept.js.map +1 -0
  7. package/lib/constants/contentTypes.d.ts +2 -0
  8. package/lib/constants/contentTypes.js +3 -0
  9. package/lib/constants/contentTypes.js.map +1 -0
  10. package/lib/content.d.ts +55 -0
  11. package/lib/content.js +133 -0
  12. package/lib/content.js.map +1 -0
  13. package/lib/content.test.d.ts +1 -0
  14. package/lib/content.test.js +149 -0
  15. package/lib/content.test.js.map +1 -0
  16. package/lib/datasources/capi.d.ts +10 -0
  17. package/lib/datasources/capi.js +28 -0
  18. package/lib/datasources/capi.js.map +1 -0
  19. package/lib/datasources/index.d.ts +9 -0
  20. package/lib/datasources/index.js +9 -0
  21. package/lib/datasources/index.js.map +1 -0
  22. package/lib/datasources/origami-image.d.ts +8 -0
  23. package/lib/datasources/origami-image.js +11 -0
  24. package/lib/datasources/origami-image.js.map +1 -0
  25. package/lib/datasources/url-management.d.ts +11 -0
  26. package/lib/datasources/url-management.js +40 -0
  27. package/lib/datasources/url-management.js.map +1 -0
  28. package/lib/datasources/url-management.test.d.ts +1 -0
  29. package/lib/datasources/url-management.test.js +69 -0
  30. package/lib/datasources/url-management.test.js.map +1 -0
  31. package/lib/helpers/byline.d.ts +1 -0
  32. package/lib/helpers/byline.js +5 -0
  33. package/lib/helpers/byline.js.map +1 -0
  34. package/lib/helpers/imageService.d.ts +8 -0
  35. package/lib/helpers/imageService.js +13 -0
  36. package/lib/helpers/imageService.js.map +1 -0
  37. package/lib/helpers/metadata.d.ts +12 -0
  38. package/lib/helpers/metadata.js +60 -0
  39. package/lib/helpers/metadata.js.map +1 -0
  40. package/lib/helpers/syntaxTree.d.ts +23 -0
  41. package/lib/helpers/syntaxTree.js +23 -0
  42. package/lib/helpers/syntaxTree.js.map +1 -0
  43. package/lib/image.d.ts +25 -0
  44. package/lib/image.js +123 -0
  45. package/lib/image.js.map +1 -0
  46. package/lib/image.test.d.ts +1 -0
  47. package/lib/image.test.js +235 -0
  48. package/lib/image.test.js.map +1 -0
  49. package/lib/index.d.ts +8 -0
  50. package/lib/index.js +69 -0
  51. package/lib/index.js.map +1 -0
  52. package/lib/picture.d.ts +22 -0
  53. package/lib/picture.js +80 -0
  54. package/lib/picture.js.map +1 -0
  55. package/lib/richText.d.ts +14 -0
  56. package/lib/richText.js +48 -0
  57. package/lib/richText.js.map +1 -0
  58. package/lib/tags.d.ts +13 -0
  59. package/lib/tags.js +178 -0
  60. package/lib/tags.js.map +1 -0
  61. package/lib/topper.d.ts +7 -0
  62. package/lib/topper.js +196 -0
  63. package/lib/topper.js.map +1 -0
  64. package/lib/unified-plugins/extract-references.d.ts +7 -0
  65. package/lib/unified-plugins/extract-references.js +36 -0
  66. package/lib/unified-plugins/extract-references.js.map +1 -0
  67. package/lib/unified-plugins/map-to-abstract-types.d.ts +4 -0
  68. package/lib/unified-plugins/map-to-abstract-types.js +17 -0
  69. package/lib/unified-plugins/map-to-abstract-types.js.map +1 -0
  70. package/package.json +43 -0
  71. package/src/__snapshots__/content.test.ts.snap +118 -0
  72. package/src/concept.ts +58 -0
  73. package/src/constants/contentTypes.ts +4 -0
  74. package/src/content.test.ts +163 -0
  75. package/src/content.ts +146 -0
  76. package/src/datasources/capi.ts +28 -0
  77. package/src/datasources/index.ts +11 -0
  78. package/src/datasources/origami-image.ts +10 -0
  79. package/src/datasources/url-management.test.ts +92 -0
  80. package/src/datasources/url-management.ts +65 -0
  81. package/src/helpers/byline.ts +4 -0
  82. package/src/helpers/imageService.ts +31 -0
  83. package/src/helpers/metadata.ts +88 -0
  84. package/src/helpers/syntaxTree.ts +26 -0
  85. package/src/image.test.ts +339 -0
  86. package/src/image.ts +154 -0
  87. package/src/index.ts +87 -0
  88. package/src/picture.ts +98 -0
  89. package/src/richText.ts +62 -0
  90. package/src/tags.ts +237 -0
  91. package/src/topper.ts +228 -0
  92. package/src/types/internal-content.d.ts +78 -0
  93. package/src/types/n-concept-ids.d.ts +16 -0
  94. package/src/types/n-display-metadata.d.ts +1 -0
  95. package/src/types/n-url-management-api-read-client.d.ts +11 -0
  96. package/src/types/next-metrics.d.ts +1 -0
  97. package/src/unified-plugins/extract-references.ts +50 -0
  98. package/src/unified-plugins/map-to-abstract-types.ts +21 -0
  99. package/tsconfig.json +10 -0
  100. package/tsconfig.tsbuildinfo +1 -0
package/src/picture.ts ADDED
@@ -0,0 +1,98 @@
1
+ import { gql } from 'graphql-tag'
2
+ import { ImageSet } from './types/internal-content.js'
3
+ import { getImageDimensions } from './image.js'
4
+
5
+ import type { DataSources } from './datasources/index.js'
6
+
7
+ export const ImageType = {
8
+ Image: 'http://www.ft.com/ontology/content/Image',
9
+ Graphic: 'http://www.ft.com/ontology/content/Graphic',
10
+ } as const
11
+
12
+ const pictureFields = `
13
+ images: PictureImages
14
+ alt: String!
15
+ caption: String
16
+ imageType: ImageType!
17
+ `
18
+
19
+ export const typeDef = gql`
20
+ enum ImageType {
21
+ Image
22
+ Graphic
23
+ }
24
+
25
+ type PictureImages {
26
+ standard: Image!
27
+ small: Image
28
+ large: Image
29
+ }
30
+
31
+ interface Picture {
32
+ ${pictureFields}
33
+ }
34
+
35
+ type PictureStandard implements Picture {
36
+ ${pictureFields}
37
+ }
38
+
39
+ type PictureInline implements Picture {
40
+ ${pictureFields}
41
+ }
42
+
43
+ type PictureFullBleed implements Picture {
44
+ ${pictureFields}
45
+ }
46
+ `
47
+
48
+ export const resolvers = {
49
+ Picture: {
50
+ async __resolveType(
51
+ parent: ImageSet,
52
+ context: { dataSources: DataSources }
53
+ ) {
54
+ //TODO: actually work out the types
55
+ if (parent.members.length === 3) return 'PictureFullBleed'
56
+ const standard = parent.members[0]
57
+ const { width, height } = await getImageDimensions(
58
+ standard,
59
+ context.dataSources
60
+ )
61
+ if (width < 350 || (width < height && width < 580)) {
62
+ return 'PictureInline'
63
+ }
64
+ return 'PictureStandard'
65
+ },
66
+ images(parent: ImageSet) {
67
+ const standard = parent.members[0]
68
+ const large =
69
+ parent.members.find((member) => member.minDisplayWidth === '980px') ||
70
+ standard
71
+ const small =
72
+ parent.members.find((member) => member.maxDisplayWidth === '490px') ||
73
+ standard
74
+
75
+ return { standard, small, large }
76
+ },
77
+ caption(parent: ImageSet) {
78
+ const standard = parent.members[0]
79
+ const elements = [standard.title, standard.copyright?.notice].filter(
80
+ Boolean
81
+ )
82
+ return elements.length ? elements.join(' ') : null
83
+ },
84
+
85
+ alt(parent: ImageSet) {
86
+ const standard = parent.members[0]
87
+ const fallback =
88
+ standard.type === ImageType.Graphic
89
+ ? 'A graphic with no description'
90
+ : ''
91
+ return (standard.description || fallback).replace(/"/g, '&quot;')
92
+ },
93
+
94
+ imageType(parent: ImageSet) {
95
+ return parent.members[0].type
96
+ },
97
+ },
98
+ }
@@ -0,0 +1,62 @@
1
+ import { unified, Plugin, PluggableList } from 'unified'
2
+ import { gql } from 'graphql-tag'
3
+ import rehypeParse from 'rehype-parse'
4
+ import extractReferences from './unified-plugins/extract-references.js'
5
+ import mapToAbstractTypes from './unified-plugins/map-to-abstract-types.js'
6
+
7
+ import type { InternalContent } from './types/internal-content.js'
8
+
9
+ type RichTextResolverData = {
10
+ source: string
11
+ value: string
12
+ contentApiData?: InternalContent
13
+ }
14
+
15
+ export const typeDef = gql`
16
+ type StructuredContent {
17
+ tree: JSON!
18
+ references: [Reference!]!
19
+ }
20
+
21
+ enum Source {
22
+ standfirst
23
+ summary
24
+ }
25
+
26
+ type RichText {
27
+ source: Source!
28
+ raw: String!
29
+ structured: StructuredContent!
30
+ }
31
+ `
32
+
33
+ export const resolvers = {
34
+ RichText: {
35
+ raw(parent: RichTextResolverData) {
36
+ return parent.value
37
+ },
38
+
39
+ async structured(parent: RichTextResolverData) {
40
+ const formatOutput: Plugin<[], Element, Element> =
41
+ function formatOutput() {
42
+ this.Compiler = (tree) => {
43
+ const references = this.data('references')
44
+ return { tree, references }
45
+ }
46
+ }
47
+
48
+ const pluggableList: PluggableList = [
49
+ mapToAbstractTypes,
50
+ extractReferences({ contentApiData: parent.contentApiData }),
51
+ formatOutput,
52
+ ]
53
+
54
+ const body = await unified()
55
+ .use(rehypeParse, { fragment: true })
56
+ .use(pluggableList)
57
+ .process(parent.value)
58
+
59
+ return body.result
60
+ },
61
+ },
62
+ }
package/src/tags.ts ADDED
@@ -0,0 +1,237 @@
1
+ import { select, matches } from 'hast-util-select'
2
+ import { toText } from 'hast-util-to-text'
3
+ import { gql } from 'graphql-tag'
4
+ import { uuidFromUrl } from './helpers/metadata.js'
5
+ import type * as hast from 'hast'
6
+ import type { IObjectTypeResolver } from '@graphql-tools/utils'
7
+ import type { DocumentNode } from 'graphql'
8
+ import type {
9
+ InternalContent,
10
+ ImageSet as CAPIImageSet,
11
+ } from './types/internal-content.js'
12
+
13
+ export type Tag = object
14
+
15
+ interface TagConstructor {
16
+ new (node: hast.Element, contentApiData?: InternalContent): Tag
17
+ match(node: hast.Element): boolean
18
+ typeDef?: DocumentNode
19
+ resolve?: IObjectTypeResolver<any, any, any>
20
+ }
21
+
22
+ function createTagWithoutReference(
23
+ name: string,
24
+ selector: string
25
+ ): TagConstructor {
26
+ const tag = class implements Tag {
27
+ static match(node: hast.Element) {
28
+ return matches(selector, node)
29
+ }
30
+ }
31
+
32
+ Object.defineProperty(tag, 'name', { value: name })
33
+
34
+ return tag
35
+ }
36
+
37
+ const Recommended: TagConstructor = class Recommended implements Tag {
38
+ uuid: string
39
+ title: string
40
+ teaserTitleOverride: string | null
41
+
42
+ static match(node: hast.Element) {
43
+ return node.tagName === 'recommended'
44
+ }
45
+
46
+ constructor(node: hast.Element) {
47
+ const ftContent = select('FTContent', node)
48
+ const recommendedTitle = select('RecommendedTitle', node)
49
+ if (
50
+ ftContent?.properties?.url !== undefined &&
51
+ typeof ftContent.properties.url === 'string'
52
+ ) {
53
+ this.uuid = uuidFromUrl(ftContent.properties.url)
54
+ } else {
55
+ this.uuid = ''
56
+ }
57
+
58
+ this.title = recommendedTitle ? toText(recommendedTitle) : 'Recommended'
59
+ this.teaserTitleOverride = ftContent ? toText(ftContent) : null
60
+ }
61
+
62
+ static resolve = {
63
+ async teaser(parent: Recommended, args, context) {
64
+ const content = await context.dataSources.capi.getContent(parent.uuid)
65
+ return {
66
+ ...content,
67
+ title: parent.teaserTitleOverride || content.title,
68
+ }
69
+ },
70
+ }
71
+
72
+ static typeDef = gql`
73
+ type Recommended {
74
+ teaser: Content!
75
+ title: String!
76
+ }
77
+ `
78
+ }
79
+
80
+ const ImageSet: TagConstructor = class ImageSet implements Tag {
81
+ static match(node: hast.Element) {
82
+ return (
83
+ node.tagName === 'ft-content' &&
84
+ node.properties?.type === 'http://www.ft.com/ontology/content/ImageSet'
85
+ )
86
+ }
87
+
88
+ url = ''
89
+ imageSet: CAPIImageSet | undefined
90
+ constructor(node: hast.Element, contentApiData?: InternalContent) {
91
+ if (typeof node?.properties?.url === 'string') {
92
+ this.url = node.properties.url ?? ''
93
+ }
94
+
95
+ const embed = contentApiData?.embeds?.find(
96
+ (embed: CAPIImageSet) => uuidFromUrl(embed.id) === uuidFromUrl(this.url)
97
+ )
98
+ this.imageSet = embed
99
+ }
100
+
101
+ static resolve = {
102
+ picture: (parent: ImageSet) => parent.imageSet,
103
+ }
104
+
105
+ static typeDef = gql`
106
+ type ImageSet {
107
+ picture: Picture!
108
+ }
109
+ `
110
+ }
111
+
112
+ const FTContent: TagConstructor = class FTContent implements Tag {
113
+ static match(node: hast.Element) {
114
+ return node.tagName === 'ft-content'
115
+ }
116
+
117
+ url: string
118
+ type: string
119
+
120
+ constructor(node: hast.Element) {
121
+ if (typeof node?.properties?.url === 'string') {
122
+ this.url = node.properties.url ?? ''
123
+ } else {
124
+ this.url = ''
125
+ }
126
+
127
+ if (typeof node?.properties?.type === 'string') {
128
+ this.type = node.properties.type ?? ''
129
+ } else {
130
+ this.type = ''
131
+ }
132
+ }
133
+
134
+ static typeDef = gql`
135
+ type FTContent {
136
+ type: String
137
+ url: String
138
+ }
139
+ `
140
+ }
141
+
142
+ const LayoutImage: TagConstructor = class LayoutImage implements Tag {
143
+ static match(node: hast.Element) {
144
+ return matches('img', node)
145
+ }
146
+
147
+ id: string
148
+ src: string
149
+ alt: string
150
+ longDesc: string
151
+ copyright: string
152
+
153
+ constructor(node: hast.Element) {
154
+ this.src = node.properties?.src as string
155
+ this.id = uuidFromUrl(this.src) || ''
156
+ this.alt = node.properties?.alt as string
157
+ this.longDesc = node.properties?.longDesc as string
158
+ this.copyright = node.properties?.dataCopyright as string
159
+ }
160
+
161
+ static resolve = {
162
+ picture(parent: LayoutImage): CAPIImageSet {
163
+ return {
164
+ id: 'layout-imageset',
165
+ description: parent.alt,
166
+ type: 'http://www.ft.com/ontology/content/ImageSet',
167
+ members: [
168
+ {
169
+ id: parent.id,
170
+ description: parent.alt,
171
+ title: parent.longDesc,
172
+ binaryUrl: parent.src,
173
+ copyright: {
174
+ notice: parent.copyright,
175
+ },
176
+ type: 'http://www.ft.com/ontology/content/Image',
177
+ },
178
+ ],
179
+ }
180
+ },
181
+ }
182
+
183
+ static typeDef = gql`
184
+ type LayoutImage {
185
+ picture: Picture!
186
+ }
187
+ `
188
+ }
189
+
190
+ const Link: TagConstructor = class Link implements Tag {
191
+ href: string
192
+
193
+ static match(node: hast.Element) {
194
+ return node.tagName === 'a'
195
+ }
196
+
197
+ constructor(node: hast.Element) {
198
+ if (typeof node?.properties?.href === 'string') {
199
+ this.href = node.properties.href ?? ''
200
+ } else {
201
+ this.href = ''
202
+ }
203
+ }
204
+
205
+ static typeDef = gql`
206
+ type Link {
207
+ href: String
208
+ }
209
+ `
210
+ }
211
+
212
+ const mapping: Record<string, TagConstructor> = {
213
+ Link,
214
+ Experimental: createTagWithoutReference('Experimental', 'experimental'),
215
+ ImageSet,
216
+ FTContent,
217
+ Recommended,
218
+ RecommendedTitle: createTagWithoutReference(
219
+ 'RecommendedTitle',
220
+ 'recommended-title'
221
+ ),
222
+ Paragraph: createTagWithoutReference('Paragraph', 'p'),
223
+ UnorderedList: createTagWithoutReference('UnorderedList', 'ul'),
224
+ ListItem: createTagWithoutReference('ListItem', 'li'),
225
+ Layout: createTagWithoutReference('Layout', '.n-content-layout, .layout'),
226
+ LayoutContainer: createTagWithoutReference(
227
+ 'LayoutContainer',
228
+ '.n-content-layout__container, .layout-container'
229
+ ),
230
+ LayoutSlot: createTagWithoutReference(
231
+ 'LayoutSlot',
232
+ '.n-content-layout__slot, .layout-slot'
233
+ ),
234
+ LayoutImage,
235
+ }
236
+
237
+ export default mapping
package/src/topper.ts ADDED
@@ -0,0 +1,228 @@
1
+ import { gql } from 'graphql-tag'
2
+ import { IResolvers } from '@graphql-tools/utils'
3
+ import type { DataSources } from './datasources/index.js'
4
+ import {
5
+ isOpinion,
6
+ getAuthor,
7
+ getDisplayConcept,
8
+ getBrandConcept,
9
+ } from './helpers/metadata.js'
10
+ import imageServiceUrl from './helpers/imageService.js'
11
+ import type {
12
+ InternalContent,
13
+ Image,
14
+ ImageSet,
15
+ } from './types/internal-content.js'
16
+
17
+ const BasicTopperFields = `
18
+ headline: String!
19
+ intro: RichText
20
+ backgroundColour: TopperBackgroundColour
21
+ displayConcept: Concept
22
+ `
23
+
24
+ const TopperWithImagesFields = `
25
+ images: TopperImages
26
+ `
27
+
28
+ const TopperWithThemeFields = `
29
+ isLargeHeadline: Boolean
30
+ layout: String
31
+ `
32
+
33
+ const TopperWithHeadshotFields = `
34
+ headshot(width: Int, dpr: Int): String
35
+ `
36
+
37
+ /*
38
+ * List of acceptable topper background colours as supported in Origami https://github.com/Financial-Times/origami/tree/main/components/o-topper#colors
39
+ *
40
+ */
41
+ export const typeDef = gql`
42
+ enum TopperBackgroundColour {
43
+ paper
44
+ wheat
45
+ white
46
+ black
47
+ claret
48
+ oxford
49
+ slate
50
+ crimson
51
+ sky
52
+ }
53
+
54
+ type TopperImages {
55
+ standard: Image
56
+ square: Image
57
+ wide: Image
58
+ fallback: Image
59
+ }
60
+
61
+ interface Topper {
62
+ ${BasicTopperFields}
63
+ }
64
+
65
+ interface TopperWithImages {
66
+ ${TopperWithImagesFields}
67
+ }
68
+
69
+ interface TopperWithTheme {
70
+ ${TopperWithThemeFields}
71
+ }
72
+
73
+ interface TopperWithHeadshot {
74
+ ${TopperWithHeadshotFields}
75
+ }
76
+
77
+ type BasicTopper implements Topper {
78
+ ${BasicTopperFields}
79
+ }
80
+
81
+ type SplitTextTopper implements Topper & TopperWithImages & TopperWithTheme {
82
+ ${BasicTopperFields}
83
+ ${TopperWithImagesFields}
84
+ ${TopperWithThemeFields}
85
+ }
86
+
87
+ type FullBleedTopper implements Topper & TopperWithImages & TopperWithTheme {
88
+ ${BasicTopperFields}
89
+ ${TopperWithImagesFields}
90
+ ${TopperWithThemeFields}
91
+ }
92
+
93
+ type OpinionTopper implements Topper & TopperWithTheme & TopperWithHeadshot {
94
+ ${BasicTopperFields}
95
+ ${TopperWithThemeFields}
96
+ ${TopperWithHeadshotFields}
97
+ columnist: Concept
98
+ }
99
+
100
+ type BrandedTopper implements Topper & TopperWithTheme {
101
+ ${BasicTopperFields}
102
+ ${TopperWithThemeFields}
103
+ brandConcept: Concept
104
+ }
105
+ `
106
+
107
+ export const resolvers: IResolvers<
108
+ unknown,
109
+ { systemCode: string; dataSources: DataSources },
110
+ never
111
+ > = {
112
+ Topper: {
113
+ __resolveType(content: InternalContent): string {
114
+ if (content.topper?.layout?.startsWith('split')) {
115
+ return 'SplitTextTopper'
116
+ } else if (content.topper?.layout?.startsWith('full-bleed')) {
117
+ return 'FullBleedTopper'
118
+ } else if (isOpinion(content)) {
119
+ return 'OpinionTopper'
120
+ } else if (getBrandConcept(content)) {
121
+ return 'BrandedTopper'
122
+ } else {
123
+ return 'BasicTopper'
124
+ }
125
+ },
126
+ headline(content: InternalContent): string {
127
+ return content.topper?.headline || content.title
128
+ },
129
+
130
+ intro(content: InternalContent) {
131
+ return content.summary
132
+ ? {
133
+ value: content.summary.bodyXML,
134
+ source: 'summary',
135
+ }
136
+ : {
137
+ value: content.standfirst,
138
+ source: 'standfirst',
139
+ }
140
+ },
141
+ backgroundColour(content: InternalContent): string {
142
+ if (
143
+ content.topper?.backgroundColour &&
144
+ content.topper?.backgroundColour !== 'auto'
145
+ ) {
146
+ return content.topper.backgroundColour
147
+ } else {
148
+ return 'paper'
149
+ }
150
+ },
151
+ displayConcept(content: InternalContent) {
152
+ return getDisplayConcept(content)
153
+ },
154
+ },
155
+
156
+ TopperWithImages: {
157
+ images(content: InternalContent) {
158
+ const groupedImages: { [type: string]: Image } = {}
159
+
160
+ content.leadImages.forEach((leadImage) => {
161
+ groupedImages[leadImage.type] = leadImage.image
162
+ })
163
+ if (
164
+ content.mainImage?.type ===
165
+ 'http://www.ft.com/ontology/content/ImageSet'
166
+ ) {
167
+ groupedImages.fallback = (content.mainImage as ImageSet).members[0]
168
+ } else if (
169
+ content.mainImage?.type === 'http://www.ft.com/ontology/content/Image'
170
+ ) {
171
+ groupedImages.fallback = content.mainImage
172
+ }
173
+
174
+ return groupedImages
175
+ },
176
+ },
177
+
178
+ TopperWithTheme: {
179
+ isLargeHeadline: () => false,
180
+ layout(content: InternalContent): string {
181
+ return content.topper?.layout || 'branded'
182
+ },
183
+ },
184
+
185
+ SplitTextTopper: {
186
+ isLargeHeadline: () => true,
187
+ },
188
+
189
+ FullBleedTopper: {
190
+ isLargeHeadline: () => true,
191
+ },
192
+
193
+ OpinionTopper: {
194
+ backgroundColour: () => 'sky',
195
+ async headshot(
196
+ content: InternalContent,
197
+ args,
198
+ context
199
+ ): Promise<string | undefined> {
200
+ const author = getAuthor(content)
201
+ if (!author) return
202
+ const authorId = author.id.replace(
203
+ /^https?:\/\/(?:www|api)\.ft\.com\/things?\//,
204
+ ''
205
+ )
206
+ const peopleData = await context.dataSources.capi.getPerson(authorId)
207
+
208
+ if (!peopleData || !peopleData._imageUrl) return
209
+
210
+ return imageServiceUrl({
211
+ url: peopleData._imageUrl,
212
+ systemCode: context.systemCode,
213
+ width: args.width || 150,
214
+ dpr: args.dpr,
215
+ })
216
+ },
217
+ columnist(content: InternalContent) {
218
+ return getAuthor(content)
219
+ },
220
+ },
221
+
222
+ BrandedTopper: {
223
+ backgroundColour: () => 'wheat',
224
+ brandConcept(content: InternalContent) {
225
+ return getBrandConcept(content)
226
+ },
227
+ },
228
+ }
@@ -0,0 +1,78 @@
1
+ export type Topper = {
2
+ headline: string
3
+ standfirst: string
4
+ backgroundColour: string
5
+ layout: string
6
+ }
7
+
8
+ export type ImageSet = {
9
+ id: string
10
+ type: 'http://www.ft.com/ontology/content/ImageSet'
11
+ description: string
12
+ members: Image[]
13
+ }
14
+
15
+ export type LeadImage = {
16
+ type: 'standard' | 'square' | 'wide'
17
+ id: string
18
+ image: Image
19
+ }
20
+
21
+ export type Annotation = {
22
+ id: string
23
+ apiUrl: string
24
+ directType: string
25
+ predicate: string
26
+ prefLabel: string
27
+ type: string
28
+ types: string[]
29
+ }
30
+
31
+ export type Image = {
32
+ id: string
33
+ type:
34
+ | 'http://www.ft.com/ontology/content/Image'
35
+ | 'http://www.ft.com/ontology/content/Graphic'
36
+ title: string
37
+ description: string
38
+ binaryUrl: string
39
+ copyright?: {
40
+ notice: string
41
+ }
42
+ minDisplayWidth?: string
43
+ maxDisplayWidth?: string
44
+ pixelWidth?: number
45
+ pixelHeight?: number
46
+ }
47
+ export type AccessLevel = 'premium' | 'subscribed' | 'registered' | 'free'
48
+ export type AltTitle = { promotionalTitle?: string }
49
+ export type AltStandfirst = { promotionalStandfirst?: string }
50
+
51
+ export type InternalContent = {
52
+ id: string
53
+ type: string
54
+ types: string[]
55
+ webUrl: string
56
+ title: string
57
+ standfirst: string
58
+ byline: string
59
+ topper?: Topper
60
+ bodyXML: string
61
+ annotations: Annotation[]
62
+ summary?: {
63
+ bodyXML: string
64
+ }
65
+ mainImage: ImageSet | Image
66
+ leadImages: LeadImage[]
67
+ accessLevel: AccessLevel
68
+ embeds: ImageSet[]
69
+ alternativeTitles?: AltTitle
70
+ alternativeStandfirsts?: AltStandfirst
71
+ standout?: {
72
+ editorsChoice: boolean
73
+ exclusive: boolean
74
+ scoop: boolean
75
+ }
76
+ publishedDate: string
77
+ firstPublishedDate: string
78
+ }
@@ -0,0 +1,16 @@
1
+ type ConceptIdMapping = Record<string, string>
2
+
3
+ type ConceptTypeNames =
4
+ | 'genre'
5
+ | 'brand'
6
+ | 'topic'
7
+ | 'person'
8
+ | 'location'
9
+ | 'organisation'
10
+
11
+ type ConceptTypeMappings = Record<ConceptTypeNames, ConceptIDMapping>
12
+
13
+ declare module '@financial-times/n-concept-ids' {
14
+ const map: ConceptTypeMappings
15
+ export default map
16
+ }
@@ -0,0 +1 @@
1
+ declare module '@financial-times/n-display-metadata'