@financial-times/dotcom-ui-shell 7.3.1 → 7.3.2
Sign up to get free protection for your applications and to get access to all the features.
- package/package.json +6 -2
- package/src/__test__/components/Content.test.tsx +23 -0
- package/src/__test__/components/DocumentHead.test.tsx +38 -0
- package/src/__test__/components/GTMBody.test.tsx +36 -0
- package/src/__test__/components/GTMHead.test.tsx +27 -0
- package/src/__test__/components/LinkedData.test.tsx +31 -0
- package/src/__test__/components/OpenGraph.test.tsx +25 -0
- package/src/__test__/components/ResourceHints.test.tsx +18 -0
- package/src/__test__/components/Shell.test.tsx +29 -0
- package/src/__test__/components/Stylesheets.test.tsx +22 -0
- package/src/__test__/components/__snapshots__/Content.test.tsx.snap +22 -0
- package/src/__test__/components/__snapshots__/DocumentHead.test.tsx.snap +87 -0
- package/src/__test__/components/__snapshots__/GTMBody.test.tsx.snap +21 -0
- package/src/__test__/components/__snapshots__/GTMHead.test.tsx.snap +17 -0
- package/src/__test__/components/__snapshots__/LinkedData.test.tsx.snap +41 -0
- package/src/__test__/components/__snapshots__/OpenGraph.test.tsx.snap +42 -0
- package/src/__test__/components/__snapshots__/ResourceHints.test.tsx.snap +54 -0
- package/src/__test__/components/__snapshots__/Shell.test.tsx.snap +317 -0
- package/src/__test__/components/__snapshots__/Stylesheets.test.tsx.snap +50 -0
- package/src/__test__/lib/flattenOpenGraphData.spec.ts +37 -0
- package/src/__test__/lib/formatAttributeNames.spec.ts +38 -0
- package/src/__test__/lib/getResourceType.spec.ts +28 -0
- package/src/components/Content.tsx +25 -0
- package/src/components/DocumentHead.tsx +103 -0
- package/src/components/GTMBody.tsx +29 -0
- package/src/components/GTMHead.tsx +23 -0
- package/src/components/LinkedData.tsx +38 -0
- package/src/components/OpenGraph.tsx +20 -0
- package/src/components/ResourceHints.tsx +63 -0
- package/src/components/Shell.tsx +97 -0
- package/src/components/StyleSheets.tsx +35 -0
- package/src/index.ts +1 -0
- package/src/lib/flattenOpenGraphData.ts +24 -0
- package/src/lib/formatAttributeNames.ts +36 -0
- package/src/lib/getResourceType.ts +34 -0
- package/src/lib/imageServiceIconURL.ts +24 -0
- package/src/lib/loadAsyncStylesheets.ts +27 -0
@@ -0,0 +1,317 @@
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
2
|
+
|
3
|
+
exports[`dotcom-ui-shell/src/components/Shell renders the GTM script when the enableGTM flag is on 1`] = `
|
4
|
+
<html
|
5
|
+
className="no-js core o-typography--loading-sans o-typography--loading-sans-bold o-typography--loading-display o-typography--loading-display-bold"
|
6
|
+
data-o-component="o-typography"
|
7
|
+
lang="en-GB"
|
8
|
+
style={
|
9
|
+
Object {
|
10
|
+
"backgroundColor": "#fff1e5",
|
11
|
+
"color": "#33302e",
|
12
|
+
"overflowX": "hidden",
|
13
|
+
}
|
14
|
+
}
|
15
|
+
>
|
16
|
+
<head>
|
17
|
+
<meta
|
18
|
+
charSet="utf-8"
|
19
|
+
/>
|
20
|
+
<meta
|
21
|
+
content="IE=edge"
|
22
|
+
httpEquiv="X-UA-Compatible"
|
23
|
+
/>
|
24
|
+
<meta
|
25
|
+
content="width=device-width, initial-scale=1"
|
26
|
+
name="viewport"
|
27
|
+
/>
|
28
|
+
<title>
|
29
|
+
Foo | Financial Times
|
30
|
+
</title>
|
31
|
+
<meta
|
32
|
+
content="News, analysis and comment from the Financial Times, the worldʼs leading global business publication"
|
33
|
+
name="description"
|
34
|
+
/>
|
35
|
+
<meta
|
36
|
+
content="index,follow,max-snippet:200,max-image-preview:large"
|
37
|
+
name="robots"
|
38
|
+
/>
|
39
|
+
<meta
|
40
|
+
content="4-t8sFaPvpO5FH_Gnw1dkM28CQepjzo8UjjAkdDflTw"
|
41
|
+
name="google-site-verification"
|
42
|
+
/>
|
43
|
+
<script
|
44
|
+
dangerouslySetInnerHTML={
|
45
|
+
Object {
|
46
|
+
"__html": "{\\"@context\\":\\"http://schema.org\\",\\"@type\\":\\"WebSite\\",\\"name\\":\\"Financial Times\\",\\"alternateName\\":\\"FT.com\\",\\"url\\":\\"http://www.ft.com\\"}",
|
47
|
+
}
|
48
|
+
}
|
49
|
+
type="application/ld+json"
|
50
|
+
/>
|
51
|
+
<meta
|
52
|
+
content="8860325749"
|
53
|
+
property="fb:pages"
|
54
|
+
/>
|
55
|
+
<meta
|
56
|
+
content="@FinancialTimes"
|
57
|
+
property="twitter:site"
|
58
|
+
/>
|
59
|
+
<meta
|
60
|
+
content="app-id=1200842933"
|
61
|
+
name="apple-itunes-app"
|
62
|
+
/>
|
63
|
+
<link
|
64
|
+
href="https://www.ft.com/__origami/service/image/v2/images/raw/ftlogo-v1%3Abrand-ft-logo-square-coloured?source=update-logos&format=svg"
|
65
|
+
rel="icon"
|
66
|
+
type="image/svg+xml"
|
67
|
+
/>
|
68
|
+
<link
|
69
|
+
href="https://www.ft.com/__origami/service/image/v2/images/raw/ftlogo-v1%3Abrand-ft-logo-square-coloured?source=update-logos&format=png&width=32&height=32"
|
70
|
+
rel="alternate icon"
|
71
|
+
sizes="32x32"
|
72
|
+
type="image/png"
|
73
|
+
/>
|
74
|
+
<link
|
75
|
+
href="https://www.ft.com/__origami/service/image/v2/images/raw/ftlogo-v1%3Abrand-ft-logo-square-coloured?source=update-logos&format=png&width=194&height=194"
|
76
|
+
rel="alternate icon"
|
77
|
+
sizes="194x194"
|
78
|
+
type="image/png"
|
79
|
+
/>
|
80
|
+
<link
|
81
|
+
href="https://www.ft.com/__origami/service/image/v2/images/raw/ftlogo-v1%3Abrand-ft-logo-square-coloured?source=update-logos&format=png&width=180&height=180"
|
82
|
+
rel="apple-touch-icon"
|
83
|
+
sizes="180x180"
|
84
|
+
/>
|
85
|
+
<link
|
86
|
+
href="https://www.ft.com/__assets/creatives/manifest/manifest-v6.json"
|
87
|
+
rel="manifest"
|
88
|
+
/>
|
89
|
+
<link
|
90
|
+
href="https://spoor-api.ft.com"
|
91
|
+
rel="preconnect"
|
92
|
+
/>
|
93
|
+
<link
|
94
|
+
crossOrigin="use-credentials"
|
95
|
+
href="https://session-next.ft.com"
|
96
|
+
rel="preconnect"
|
97
|
+
/>
|
98
|
+
<link
|
99
|
+
href="https://ads-api.ft.com"
|
100
|
+
rel="preconnect"
|
101
|
+
/>
|
102
|
+
<link
|
103
|
+
href="https://securepubads.g.doubleclick.net"
|
104
|
+
rel="preconnect"
|
105
|
+
/>
|
106
|
+
<link
|
107
|
+
as="script"
|
108
|
+
href="https://polyfill.io/v3/polyfill.min.js?features=default%2Ces5%2Ces2015%2Ces2016%2Ces2017%2CEventSource%2Cfetch%2CHTMLPictureElement%2CIntersectionObserver%2CNodeList.prototype.forEach&source=next"
|
109
|
+
rel="preload"
|
110
|
+
type={null}
|
111
|
+
/>
|
112
|
+
<link
|
113
|
+
as="font"
|
114
|
+
crossOrigin="anonymous"
|
115
|
+
href="https://www.ft.com/__origami/service/build/v3/font?font_format=woff2&font_name=MetricWeb-Regular&system_code=origami&version=1.12"
|
116
|
+
rel="preload"
|
117
|
+
type="font/woff2"
|
118
|
+
/>
|
119
|
+
<link
|
120
|
+
as="font"
|
121
|
+
crossOrigin="anonymous"
|
122
|
+
href="https://www.ft.com/__origami/service/build/v3/font?font_format=woff2&font_name=MetricWeb-Semibold&system_code=origami&version=1.12"
|
123
|
+
rel="preload"
|
124
|
+
type="font/woff2"
|
125
|
+
/>
|
126
|
+
<link
|
127
|
+
as="font"
|
128
|
+
crossOrigin="anonymous"
|
129
|
+
href="https://www.ft.com/__origami/service/build/v3/font?font_format=woff2&font_name=FinancierDisplayWeb-Regular&system_code=origami&version=1.12"
|
130
|
+
rel="preload"
|
131
|
+
type="font/woff2"
|
132
|
+
/>
|
133
|
+
<link
|
134
|
+
as="font"
|
135
|
+
crossOrigin="anonymous"
|
136
|
+
href="https://www.ft.com/__origami/service/build/v3/font?font_format=woff2&font_name=FinancierDisplayWeb-Bold&system_code=origami&version=1.12"
|
137
|
+
rel="preload"
|
138
|
+
type="font/woff2"
|
139
|
+
/>
|
140
|
+
<script
|
141
|
+
dangerouslySetInnerHTML={
|
142
|
+
Object {
|
143
|
+
"__html": undefined,
|
144
|
+
}
|
145
|
+
}
|
146
|
+
id="initial-props"
|
147
|
+
type="application/json"
|
148
|
+
/>
|
149
|
+
<script
|
150
|
+
dangerouslySetInnerHTML={
|
151
|
+
Object {
|
152
|
+
"__html": "{\\"trackErrors\\":true,\\"core\\":[\\"https://polyfill.io/v3/polyfill.min.js?features=HTMLPictureElement&source=next\\"],\\"enhanced\\":[\\"https://polyfill.io/v3/polyfill.min.js?features=default%2Ces5%2Ces2015%2Ces2016%2Ces2017%2CEventSource%2Cfetch%2CHTMLPictureElement%2CIntersectionObserver%2CNodeList.prototype.forEach&source=next\\"]}",
|
153
|
+
}
|
154
|
+
}
|
155
|
+
id="page-kit-bootstrap-config"
|
156
|
+
type="application/json"
|
157
|
+
/>
|
158
|
+
<script
|
159
|
+
dangerouslySetInnerHTML={
|
160
|
+
Object {
|
161
|
+
"__html": ";(function () {
|
162
|
+
var doc = document.documentElement
|
163
|
+
var isEnhanced = isEnhancedBrowser()
|
164
|
+
var scriptsConfig = getScriptsConfig()
|
165
|
+
var scriptsToLoad = []
|
166
|
+
var currentScript = document.scripts[document.scripts.length - 1]
|
167
|
+
|
168
|
+
doc.className = doc.className.replace('no-js', 'js')
|
169
|
+
|
170
|
+
if (isEnhanced) {
|
171
|
+
doc.className = doc.className.replace('core', 'enhanced')
|
172
|
+
Array.prototype.push.apply(scriptsToLoad, scriptsConfig.enhanced)
|
173
|
+
} else {
|
174
|
+
Array.prototype.push.apply(scriptsToLoad, scriptsConfig.core)
|
175
|
+
}
|
176
|
+
|
177
|
+
for (var i = 0, len = scriptsToLoad.length; i < len; i++) {
|
178
|
+
loadScript(scriptsToLoad[i])
|
179
|
+
}
|
180
|
+
|
181
|
+
function scriptLoadError(error) {
|
182
|
+
var script = error.target ? error.target.src : null
|
183
|
+
|
184
|
+
if (script) {
|
185
|
+
console.error('The script ' + script + ' failed to load') // eslint-disable-line no-console
|
186
|
+
}
|
187
|
+
|
188
|
+
if (/enhanced/.test(doc.className)) {
|
189
|
+
console.warn('Script loading failed, reverting to core experience') // eslint-disable-line no-console
|
190
|
+
doc.className = doc.className.replace('enhanced', 'core')
|
191
|
+
}
|
192
|
+
|
193
|
+
if (scriptsConfig.trackErrors) {
|
194
|
+
addErrorTrackingPixel(script)
|
195
|
+
}
|
196
|
+
}
|
197
|
+
|
198
|
+
function loadScript(src) {
|
199
|
+
var script = document.createElement('script')
|
200
|
+
script.onerror = scriptLoadError
|
201
|
+
script.async = false
|
202
|
+
script.src = src
|
203
|
+
currentScript.parentNode.insertBefore(script, currentScript)
|
204
|
+
}
|
205
|
+
|
206
|
+
// \\"Cut the mustard\\" test
|
207
|
+
// by Maggie Allen and Matt Hinchliffe November 2018
|
208
|
+
function isEnhancedBrowser() {
|
209
|
+
var script = document.createElement('script')
|
210
|
+
var input = document.createElement('input')
|
211
|
+
|
212
|
+
return (
|
213
|
+
'visibilityState' in document && // not supported by old Android (4.0-4.4) without a prefix
|
214
|
+
'indeterminate' in input && // not supported by BB 10
|
215
|
+
'flex' in doc.style && // not supported by old Safari (< 9) or IE 6-10
|
216
|
+
'async' in script // not supported by old Opera (Presto engine < 15)
|
217
|
+
)
|
218
|
+
}
|
219
|
+
|
220
|
+
function getScriptsConfig() {
|
221
|
+
var scriptsConfigEl = document.getElementById('page-kit-bootstrap-config')
|
222
|
+
var scriptsConfig = { core: [], enhanced: [], trackErrors: false }
|
223
|
+
|
224
|
+
if (scriptsConfigEl) {
|
225
|
+
try {
|
226
|
+
scriptsConfig = JSON.parse(scriptsConfigEl.innerHTML)
|
227
|
+
} catch (error) {
|
228
|
+
console.error('Bootstrap configuration error', error) // eslint-disable-line no-console
|
229
|
+
}
|
230
|
+
}
|
231
|
+
|
232
|
+
return scriptsConfig
|
233
|
+
}
|
234
|
+
|
235
|
+
function addErrorTrackingPixel(script) {
|
236
|
+
var img = new Image()
|
237
|
+
|
238
|
+
var data = JSON.stringify({
|
239
|
+
category: 'javascript',
|
240
|
+
action: 'load-error',
|
241
|
+
system: {
|
242
|
+
source: 'page-kit'
|
243
|
+
},
|
244
|
+
context: {
|
245
|
+
script: script
|
246
|
+
}
|
247
|
+
})
|
248
|
+
|
249
|
+
img.src = 'https://spoor-api.ft.com/px.gif?data=' + encodeURIComponent(data)
|
250
|
+
}
|
251
|
+
})()
|
252
|
+
",
|
253
|
+
}
|
254
|
+
}
|
255
|
+
/>
|
256
|
+
<script
|
257
|
+
dangerouslySetInnerHTML={
|
258
|
+
Object {
|
259
|
+
"__html": "(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
260
|
+
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
261
|
+
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
262
|
+
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
263
|
+
})(window,document,'script','dataLayer','GTM-NWQJW68');",
|
264
|
+
}
|
265
|
+
}
|
266
|
+
/>
|
267
|
+
<script
|
268
|
+
dangerouslySetInnerHTML={
|
269
|
+
Object {
|
270
|
+
"__html": "(function loadCustomFonts() {
|
271
|
+
var rootElement = document.documentElement;
|
272
|
+
if (/(^|\\\\s)o-typography-fonts-loaded=1(;|$)/.test(document.cookie)) {
|
273
|
+
var fontLabels = ['sans', 'sans-bold', 'display', 'display-bold'];
|
274
|
+
for (var i = 0; i < fontLabels.length; i++) {
|
275
|
+
rootElement.className = rootElement.className.replace('o-typography--loading-' + fontLabels[i], '');
|
276
|
+
}
|
277
|
+
}
|
278
|
+
}());",
|
279
|
+
}
|
280
|
+
}
|
281
|
+
/>
|
282
|
+
</head>
|
283
|
+
<body>
|
284
|
+
<noscript>
|
285
|
+
<iframe
|
286
|
+
height="0"
|
287
|
+
src="https://www.googletagmanager.com/ns.html?id=GTM-NWQJW68"
|
288
|
+
style={
|
289
|
+
Object {
|
290
|
+
"display": "none",
|
291
|
+
"visibility": "hidden",
|
292
|
+
}
|
293
|
+
}
|
294
|
+
width="0"
|
295
|
+
/>
|
296
|
+
</noscript>
|
297
|
+
<script
|
298
|
+
dangerouslySetInnerHTML={
|
299
|
+
Object {
|
300
|
+
"__html": undefined,
|
301
|
+
}
|
302
|
+
}
|
303
|
+
id="page-kit-app-context"
|
304
|
+
type="application/json"
|
305
|
+
/>
|
306
|
+
<script
|
307
|
+
dangerouslySetInnerHTML={
|
308
|
+
Object {
|
309
|
+
"__html": "{\\"enableGTM\\":true}",
|
310
|
+
}
|
311
|
+
}
|
312
|
+
id="page-kit-flags-embed"
|
313
|
+
type="application/json"
|
314
|
+
/>
|
315
|
+
</body>
|
316
|
+
</html>
|
317
|
+
`;
|
@@ -0,0 +1,50 @@
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
2
|
+
|
3
|
+
exports[`dotcom-ui-shell/src/components/StyleSheets renders JavaScript for adding asyncronous stylesheets if appropriate 1`] = `
|
4
|
+
Array [
|
5
|
+
<noscript>
|
6
|
+
<link
|
7
|
+
href="path/to/async-styles.css"
|
8
|
+
rel="stylesheet"
|
9
|
+
/>
|
10
|
+
</noscript>,
|
11
|
+
<script
|
12
|
+
dangerouslySetInnerHTML={
|
13
|
+
Object {
|
14
|
+
"__html": "(function loadAsyncStylesheets() {
|
15
|
+
var currentScript = document.scripts[document.scripts.length - 1];
|
16
|
+
var stylesheets = currentScript.getAttribute('data-stylesheets').split(',');
|
17
|
+
for (var i = 0, len = stylesheets.length; i < len; i++) {
|
18
|
+
var link = document.createElement('link');
|
19
|
+
link.href = stylesheets[i];
|
20
|
+
link.rel = 'stylesheet';
|
21
|
+
link.media = 'print'; // <-- 'print' is intentional; on load, it changes to 'all'.
|
22
|
+
link.onload = function (event) {
|
23
|
+
var target = event.target;
|
24
|
+
target.media = 'all';
|
25
|
+
};
|
26
|
+
currentScript.parentNode.insertBefore(link, currentScript);
|
27
|
+
}
|
28
|
+
})()",
|
29
|
+
}
|
30
|
+
}
|
31
|
+
data-stylesheets="path/to/async-styles.css"
|
32
|
+
/>,
|
33
|
+
]
|
34
|
+
`;
|
35
|
+
|
36
|
+
exports[`dotcom-ui-shell/src/components/StyleSheets renders the given stylesheets and critical styles 1`] = `
|
37
|
+
Array [
|
38
|
+
<style
|
39
|
+
dangerouslySetInnerHTML={
|
40
|
+
Object {
|
41
|
+
"__html": "html { margin: 0 } body { font-family: \\"Comic Sans\\" }",
|
42
|
+
}
|
43
|
+
}
|
44
|
+
/>,
|
45
|
+
<link
|
46
|
+
href="path/to/styles.css"
|
47
|
+
rel="stylesheet"
|
48
|
+
/>,
|
49
|
+
]
|
50
|
+
`;
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import subject from '../../lib/flattenOpenGraphData'
|
2
|
+
|
3
|
+
const fixture = Object.freeze({
|
4
|
+
og: {
|
5
|
+
title: 'Open Graph title',
|
6
|
+
video: 'https://my.site/video.mp4',
|
7
|
+
'video:type': 'video/mp4',
|
8
|
+
'video:width': 640,
|
9
|
+
'video:height': 360
|
10
|
+
},
|
11
|
+
article: {
|
12
|
+
published_time: '2019-02-27T06:59:35.000Z',
|
13
|
+
modified_time: '2019-02-27T16:01:21.000Z',
|
14
|
+
author: ['Joe Bloggs', 'Jane Doe']
|
15
|
+
}
|
16
|
+
})
|
17
|
+
|
18
|
+
describe('dotcom-ui-shell/src/lib/flattenOpenGraphData', () => {
|
19
|
+
it('returns an array', () => {
|
20
|
+
const result = subject(fixture)
|
21
|
+
expect(result).toEqual(expect.any(Array))
|
22
|
+
})
|
23
|
+
|
24
|
+
it('flattens the given object into property/value pairs', () => {
|
25
|
+
const result = subject(fixture)
|
26
|
+
|
27
|
+
expect(result[1]).toEqual(['og:video', fixture.og.video])
|
28
|
+
expect(result[5]).toEqual(['article:published_time', fixture.article.published_time])
|
29
|
+
})
|
30
|
+
|
31
|
+
it('can handle nested data', () => {
|
32
|
+
const result = subject(fixture)
|
33
|
+
|
34
|
+
expect(result[7]).toEqual(['article:author', fixture.article.author[0]])
|
35
|
+
expect(result[8]).toEqual(['article:author', fixture.article.author[1]])
|
36
|
+
})
|
37
|
+
})
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import subject from '../../lib/formatAttributeNames'
|
2
|
+
|
3
|
+
const fixture = Object.freeze({
|
4
|
+
dataVersion: 123,
|
5
|
+
dataAppName: 'test',
|
6
|
+
userLoggedIn: true,
|
7
|
+
lang: 'en-GB'
|
8
|
+
})
|
9
|
+
|
10
|
+
describe('dotcom-ui-shell/src/lib/formatAttributeNames', () => {
|
11
|
+
it('returns a new object', () => {
|
12
|
+
const result = subject(fixture)
|
13
|
+
expect(result).not.toBe(fixture)
|
14
|
+
})
|
15
|
+
|
16
|
+
it('converts camelCase property names to kebab-case', () => {
|
17
|
+
const result = subject(fixture)
|
18
|
+
expect(result).toHaveProperty('data-version', 123)
|
19
|
+
expect(result).toHaveProperty('data-app-name', 'test')
|
20
|
+
expect(result).toHaveProperty('user-logged-in', true)
|
21
|
+
expect(result).toHaveProperty('lang', 'en-GB')
|
22
|
+
})
|
23
|
+
|
24
|
+
it('converts boolean data properties to boolean data attributes', () => {
|
25
|
+
const fixture = { dataIsProduction: true, dataIsDevelopment: false, userIsLoggedIn: true }
|
26
|
+
const result = subject(fixture)
|
27
|
+
|
28
|
+
// where react is concerned, a `true` boolean data attribute
|
29
|
+
// is one where the attribute value is an empty string (because
|
30
|
+
// it is not possible to render the attribute without a value),
|
31
|
+
// and a `false` boolean data attribute is one where the attribute
|
32
|
+
// has not been specified altogether
|
33
|
+
expect(result).toEqual({
|
34
|
+
'data-is-production': '',
|
35
|
+
'user-is-logged-in': true
|
36
|
+
})
|
37
|
+
})
|
38
|
+
})
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import subject from '../../lib/getResourceType'
|
2
|
+
|
3
|
+
describe('dotcom-ui-shell/src/lib/getResourceType', () => {
|
4
|
+
it('uses the file extension to match to a resource type', () => {
|
5
|
+
expect(subject('style.css')).toEqual('style')
|
6
|
+
expect(subject('script.js')).toEqual('script')
|
7
|
+
expect(subject('image.png')).toEqual('image')
|
8
|
+
expect(
|
9
|
+
subject(
|
10
|
+
'https://www.ft.com/__origami/service/build/v3/font?font_format=woff2&font_name=MetricWeb-Regular&system_code=origami&version=1.12'
|
11
|
+
)
|
12
|
+
).toEqual('font')
|
13
|
+
})
|
14
|
+
|
15
|
+
it('throws if the file extension cannot be matched', () => {
|
16
|
+
expect(() => subject('style.doc')).toThrow()
|
17
|
+
})
|
18
|
+
|
19
|
+
it('supports URLs', () => {
|
20
|
+
expect(subject('www.example.com/assets/style.css')).toEqual('style')
|
21
|
+
expect(subject('www.example.com/images/graphic.svg#icon')).toEqual('image')
|
22
|
+
expect(subject('http://polyfill.io/v3/bundle.min.js?features=es5,es6')).toEqual('script')
|
23
|
+
})
|
24
|
+
|
25
|
+
it('supports file paths', () => {
|
26
|
+
expect(subject('/assets/public/style.as83hd99.css')).toEqual('style')
|
27
|
+
})
|
28
|
+
})
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import React from 'react'
|
2
|
+
|
3
|
+
export type TContentProps = {
|
4
|
+
contents?: string | React.ReactNode
|
5
|
+
}
|
6
|
+
|
7
|
+
const styles = {
|
8
|
+
display: 'contents'
|
9
|
+
}
|
10
|
+
|
11
|
+
function Content({ contents }: TContentProps) {
|
12
|
+
if (typeof contents === 'string') {
|
13
|
+
return <div style={styles} dangerouslySetInnerHTML={{ __html: contents }} />
|
14
|
+
}
|
15
|
+
|
16
|
+
// We could try and validate this but there are so many possibilities
|
17
|
+
// of node types and potentially nested arrays etc.
|
18
|
+
if (contents) {
|
19
|
+
return <React.Fragment>{contents}</React.Fragment>
|
20
|
+
}
|
21
|
+
|
22
|
+
return null
|
23
|
+
}
|
24
|
+
|
25
|
+
export default Content
|
@@ -0,0 +1,103 @@
|
|
1
|
+
import React from 'react'
|
2
|
+
import imageServiceIconURL from '../lib/imageServiceIconURL'
|
3
|
+
import OpenGraph, { TOpenGraphProps } from './OpenGraph'
|
4
|
+
import LinkedData, { TLinkedDataProps } from './LinkedData'
|
5
|
+
|
6
|
+
export type TDocumentHeadProps = TOpenGraphProps &
|
7
|
+
TLinkedDataProps & {
|
8
|
+
description?: string
|
9
|
+
facebookPage?: string
|
10
|
+
googleSiteVerification?: string
|
11
|
+
metaTags?: Array<{ [key: string]: any }>
|
12
|
+
pageTitle: string
|
13
|
+
robots?: string
|
14
|
+
siteTitle?: string
|
15
|
+
twitterSite?: string
|
16
|
+
canonicalURL?: string
|
17
|
+
manifestFile?: string
|
18
|
+
additionalMetadata?: React.ReactNode,
|
19
|
+
showSmartBanner?: boolean
|
20
|
+
}
|
21
|
+
|
22
|
+
const DocumentHead = (props: TDocumentHeadProps) => (
|
23
|
+
<React.Fragment>
|
24
|
+
<meta charSet="utf-8" />
|
25
|
+
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
26
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
27
|
+
|
28
|
+
<title>{props.pageTitle ? `${props.pageTitle} | ${props.siteTitle}` : props.siteTitle}</title>
|
29
|
+
|
30
|
+
{props.description && <meta name="description" content={props.description} />}
|
31
|
+
|
32
|
+
{props.canonicalURL && <link rel="canonical" href={props.canonicalURL} />}
|
33
|
+
|
34
|
+
{/* SEO */}
|
35
|
+
<meta name="robots" content={props.robots} />
|
36
|
+
<meta name="google-site-verification" content={props.googleSiteVerification} />
|
37
|
+
{props.metaTags.map((attributes, i) => (
|
38
|
+
<meta key={`meta-${i}`} {...attributes} />
|
39
|
+
))}
|
40
|
+
<LinkedData jsonLd={props.jsonLd} />
|
41
|
+
|
42
|
+
{/* social media */}
|
43
|
+
<meta property="fb:pages" content={props.facebookPage} />
|
44
|
+
<meta property="twitter:site" content={props.twitterSite} />
|
45
|
+
<OpenGraph openGraph={props.openGraph} />
|
46
|
+
|
47
|
+
{/* native apps */}
|
48
|
+
{props.showSmartBanner &&
|
49
|
+
(
|
50
|
+
<meta
|
51
|
+
name="apple-itunes-app"
|
52
|
+
content={props.canonicalURL ? `app-id=1200842933, app-argument=${props.canonicalURL}` : 'app-id=1200842933'}
|
53
|
+
/>
|
54
|
+
)
|
55
|
+
}
|
56
|
+
|
57
|
+
{/* packaging */}
|
58
|
+
<link
|
59
|
+
rel="icon"
|
60
|
+
type="image/svg+xml"
|
61
|
+
href={imageServiceIconURL('ftlogo-v1:brand-ft-logo-square-coloured', 0, 'svg')}
|
62
|
+
/>
|
63
|
+
<link
|
64
|
+
rel="alternate icon"
|
65
|
+
type="image/png"
|
66
|
+
href={imageServiceIconURL('ftlogo-v1:brand-ft-logo-square-coloured', 32)}
|
67
|
+
sizes="32x32"
|
68
|
+
/>
|
69
|
+
<link
|
70
|
+
rel="alternate icon"
|
71
|
+
type="image/png"
|
72
|
+
href={imageServiceIconURL('ftlogo-v1:brand-ft-logo-square-coloured', 194)}
|
73
|
+
sizes="194x194"
|
74
|
+
/>
|
75
|
+
<link
|
76
|
+
rel="apple-touch-icon"
|
77
|
+
href={imageServiceIconURL('ftlogo-v1:brand-ft-logo-square-coloured', 180)}
|
78
|
+
sizes="180x180"
|
79
|
+
/>
|
80
|
+
|
81
|
+
{props.manifestFile ? <link rel="manifest" href={props.manifestFile} /> : null}
|
82
|
+
|
83
|
+
{/* We can't add an option for every single metadata option so allow custom elements to be inserted*/}
|
84
|
+
{props.additionalMetadata}
|
85
|
+
</React.Fragment>
|
86
|
+
)
|
87
|
+
|
88
|
+
DocumentHead.defaultProps = {
|
89
|
+
description:
|
90
|
+
'News, analysis and comment from the Financial Times, the worldʼs leading global business publication',
|
91
|
+
facebookPage: '8860325749',
|
92
|
+
googleSiteVerification: '4-t8sFaPvpO5FH_Gnw1dkM28CQepjzo8UjjAkdDflTw',
|
93
|
+
metaTags: [],
|
94
|
+
jsonLd: [],
|
95
|
+
robots: 'index,follow,max-snippet:200,max-image-preview:large',
|
96
|
+
siteTitle: 'Financial Times',
|
97
|
+
twitterSite: '@FinancialTimes',
|
98
|
+
manifestFile: 'https://www.ft.com/__assets/creatives/manifest/manifest-v6.json',
|
99
|
+
additionalMetadata: null,
|
100
|
+
showSmartBanner: true
|
101
|
+
}
|
102
|
+
|
103
|
+
export default DocumentHead
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import React from 'react'
|
2
|
+
import { TFlagsData } from '@financial-times/dotcom-ui-flags/src/types'
|
3
|
+
|
4
|
+
// This component is maintained by the ads team
|
5
|
+
const GTMBody = ({ flags }: { flags: TFlagsData }) => {
|
6
|
+
if (!flags.enableGTM) {
|
7
|
+
return null
|
8
|
+
}
|
9
|
+
|
10
|
+
return (
|
11
|
+
<noscript>
|
12
|
+
<iframe
|
13
|
+
src="https://www.googletagmanager.com/ns.html?id=GTM-NWQJW68"
|
14
|
+
height="0"
|
15
|
+
width="0"
|
16
|
+
style={{
|
17
|
+
display: 'none',
|
18
|
+
visibility: 'hidden'
|
19
|
+
}}
|
20
|
+
/>
|
21
|
+
</noscript>
|
22
|
+
)
|
23
|
+
}
|
24
|
+
|
25
|
+
GTMBody.defaultProps = {
|
26
|
+
flags: {}
|
27
|
+
}
|
28
|
+
|
29
|
+
export default GTMBody
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import React from 'react'
|
2
|
+
import { TFlagsData } from '@financial-times/dotcom-ui-flags/src/types'
|
3
|
+
|
4
|
+
// This component is maintained by the ads team
|
5
|
+
const GTMHead = ({ flags }: { flags: TFlagsData }) => {
|
6
|
+
if (!flags.enableGTM) {
|
7
|
+
return null
|
8
|
+
}
|
9
|
+
|
10
|
+
const tagManager = `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
11
|
+
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
12
|
+
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
13
|
+
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
14
|
+
})(window,document,'script','dataLayer','GTM-NWQJW68');`
|
15
|
+
|
16
|
+
return <script dangerouslySetInnerHTML={{ __html: tagManager }} />
|
17
|
+
}
|
18
|
+
|
19
|
+
GTMHead.defaultProps = {
|
20
|
+
flags: {}
|
21
|
+
}
|
22
|
+
|
23
|
+
export default GTMHead
|