Skip to content

Commit 141b51e

Browse files
Merge pull request #8657 from guardian/gb/link-header
Prefetch script assets on HTML page responses
2 parents 7d4abee + 510ae2d commit 141b51e

8 files changed

Lines changed: 99 additions & 81 deletions

dotcom-rendering/src/server/index.allEditorialNewslettersPage.web.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { RequestHandler } from 'express';
22
import { enhanceNewslettersPage } from '../model/enhance-newsletters-page';
33
import { validateAsAllEditorialNewslettersPageType } from '../model/validate';
4+
import { makePrefetchHeader } from './lib/header';
45
import { recordTypeAndPlatform } from './lib/logging-store';
56
import { renderEditorialNewslettersPage } from './render.allEditorialNewslettersPage.web';
67

@@ -11,6 +12,8 @@ export const handleAllEditorialNewslettersPage: RequestHandler = (
1112
recordTypeAndPlatform('newsletters');
1213
const feNewslettersData = validateAsAllEditorialNewslettersPageType(body);
1314
const newslettersPage = enhanceNewslettersPage(feNewslettersData);
14-
const html = renderEditorialNewslettersPage({ newslettersPage });
15-
res.status(200).send(html);
15+
const { html, clientScripts } = renderEditorialNewslettersPage({
16+
newslettersPage,
17+
});
18+
res.status(200).set('Link', makePrefetchHeader(clientScripts)).send(html);
1619
};

dotcom-rendering/src/server/index.article.apps.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,9 @@
11
import type { RequestHandler } from 'express';
22
import { recordTypeAndPlatform } from '../server/lib/logging-store';
33
import { enhanceArticleType } from './index.article.web';
4+
import { makePrefetchHeader } from './lib/header';
45
import { renderArticle } from './render.article.apps';
56

6-
/**
7-
* Formats script paths as a Link header
8-
* @see https://datatracker.ietf.org/doc/html/rfc5988#section-5.5
9-
* @param scriptPaths - the script paths to include in the Link header
10-
*/
11-
const makePrefetchHeader = (scriptPaths: string[]): string =>
12-
scriptPaths.reduce(
13-
(acc, scriptPath) => acc + `<${scriptPath}>; rel=prefetch,`,
14-
'',
15-
);
16-
177
export const handleAppsArticle: RequestHandler = ({ body }, res) => {
188
recordTypeAndPlatform('article', 'apps');
199
const article = enhanceArticleType(body);

dotcom-rendering/src/server/index.article.web.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { validateAsArticleType } from '../model/validate';
1111
import { recordTypeAndPlatform } from '../server/lib/logging-store';
1212
import type { FEArticleBadgeType } from '../types/badge';
1313
import type { FEArticleType, FEBlocksRequest } from '../types/frontend';
14+
import { makePrefetchHeader } from './lib/header';
1415
import {
1516
renderBlocks,
1617
renderHtml,
@@ -75,11 +76,11 @@ export const enhanceArticleType = (body: unknown): FEArticleType => {
7576
export const handleArticle: RequestHandler = ({ body }, res) => {
7677
recordTypeAndPlatform('article', 'web');
7778
const article = enhanceArticleType(body);
78-
const resp = renderHtml({
79+
const { html, clientScripts } = renderHtml({
7980
article,
8081
});
8182

82-
res.status(200).send(resp);
83+
res.status(200).set('Link', makePrefetchHeader(clientScripts)).send(html);
8384
};
8485

8586
export const handleArticleJson: RequestHandler = ({ body }, res) => {
@@ -104,11 +105,11 @@ export const handleArticlePerfTest: RequestHandler = (req, res, next) => {
104105
export const handleInteractive: RequestHandler = ({ body }, res) => {
105106
recordTypeAndPlatform('interactive', 'web');
106107
const article = enhanceArticleType(body);
107-
const resp = renderHtml({
108+
const { html, clientScripts } = renderHtml({
108109
article,
109110
});
110111

111-
res.status(200).send(resp);
112+
res.status(200).set('Link', makePrefetchHeader(clientScripts)).send(html);
112113
};
113114

114115
export const handleBlocks: RequestHandler = ({ body }, res) => {

dotcom-rendering/src/server/index.front.web.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { validateAsFrontType, validateAsTagFrontType } from '../model/validate';
1313
import { recordTypeAndPlatform } from '../server/lib/logging-store';
1414
import type { DCRFrontType, FEFrontType } from '../types/front';
1515
import type { DCRTagFrontType, FETagFrontType } from '../types/tagFront';
16+
import { makePrefetchHeader } from './lib/header';
1617
import { renderFront, renderTagFront } from './render.front.web';
1718

1819
const enhanceFront = (body: unknown): DCRFrontType => {
@@ -91,10 +92,10 @@ const enhanceTagFront = (body: unknown): DCRTagFrontType => {
9192
export const handleFront: RequestHandler = ({ body }, res) => {
9293
recordTypeAndPlatform('front');
9394
const front = enhanceFront(body);
94-
const html = renderFront({
95+
const { html, clientScripts } = renderFront({
9596
front,
9697
});
97-
res.status(200).send(html);
98+
res.status(200).set('Link', makePrefetchHeader(clientScripts)).send(html);
9899
};
99100

100101
export const handleFrontJson: RequestHandler = ({ body }, res) => {
@@ -104,10 +105,10 @@ export const handleFrontJson: RequestHandler = ({ body }, res) => {
104105
export const handleTagFront: RequestHandler = ({ body }, res) => {
105106
recordTypeAndPlatform('tagFront');
106107
const tagFront = enhanceTagFront(body);
107-
const html = renderTagFront({
108+
const { html, clientScripts } = renderTagFront({
108109
tagFront,
109110
});
110-
res.status(200).send(html);
111+
res.status(200).set('Link', makePrefetchHeader(clientScripts)).send(html);
111112
};
112113

113114
export const handleTagFrontJson: RequestHandler = ({ body }, res) => {
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Formats script paths as a Link header
3+
* @see https://datatracker.ietf.org/doc/html/rfc5988#section-5.5
4+
* @param scriptPaths - the script paths to include in the Link header
5+
*/
6+
export const makePrefetchHeader = (scriptPaths: string[]): string =>
7+
scriptPaths.reduce(
8+
(acc, scriptPath) => acc + `<${scriptPath}>; rel=prefetch,`,
9+
'',
10+
);

dotcom-rendering/src/server/render.allEditorialNewslettersPage.web.tsx

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ interface Props {
1818

1919
export const renderEditorialNewslettersPage = ({
2020
newslettersPage,
21-
}: Props): string => {
21+
}: Props): { html: string; clientScripts: string[] } => {
2222
const title = newslettersPage.webTitle;
2323
const NAV = extractNAV(newslettersPage.nav);
2424

@@ -45,17 +45,16 @@ export const renderEditorialNewslettersPage = ({
4545
* Please talk to the dotcom platform team before adding more.
4646
* Scripts will be executed in the order they appear in this array
4747
*/
48-
const scriptTags = generateScriptTags(
49-
[
50-
polyfillIO,
51-
getPathFromManifest(build, 'frameworks.js'),
52-
getPathFromManifest(build, 'index.js'),
53-
getPathFromManifest('web.legacy', 'frameworks.js'),
54-
getPathFromManifest('web.legacy', 'index.js'),
55-
process.env.COMMERCIAL_BUNDLE_URL ??
56-
newslettersPage.config.commercialBundleUrl,
57-
].map((script) => (offerHttp3 ? getHttp3Url(script) : script)),
58-
);
48+
const clientScripts = [
49+
polyfillIO,
50+
getPathFromManifest(build, 'frameworks.js'),
51+
getPathFromManifest(build, 'index.js'),
52+
getPathFromManifest('web.legacy', 'frameworks.js'),
53+
getPathFromManifest('web.legacy', 'index.js'),
54+
process.env.COMMERCIAL_BUNDLE_URL ??
55+
newslettersPage.config.commercialBundleUrl,
56+
].map((script) => (offerHttp3 ? getHttp3Url(script) : script));
57+
const scriptTags = generateScriptTags(clientScripts);
5958

6059
const guardian = createGuardian({
6160
editionId: newslettersPage.editionId,
@@ -75,7 +74,7 @@ export const renderEditorialNewslettersPage = ({
7574
googleRecaptchaSiteKey: newslettersPage.config.googleRecaptchaSiteKey,
7675
});
7776

78-
return htmlPageTemplate({
77+
const pageHtml = htmlPageTemplate({
7978
scriptTags,
8079
css: extractedCss,
8180
html,
@@ -87,4 +86,8 @@ export const renderEditorialNewslettersPage = ({
8786
renderingTarget: 'Web',
8887
weAreHiring: !!newslettersPage.config.switches.weAreHiring,
8988
});
89+
return {
90+
html: pageHtml,
91+
clientScripts,
92+
};
9093
};

dotcom-rendering/src/server/render.article.web.tsx

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ const decideTitle = (article: FEArticleType): string => {
3434
return `${article.headline} | ${article.sectionLabel} | The Guardian`;
3535
};
3636

37-
export const renderHtml = ({ article }: Props): string => {
37+
export const renderHtml = ({
38+
article,
39+
}: Props): { html: string; clientScripts: string[] } => {
3840
const NAV = {
3941
...extractNAV(article.nav),
4042
selectedPillar: getCurrentPillar(article),
@@ -90,21 +92,19 @@ export const renderHtml = ({ article }: Props): string => {
9092
* Please talk to the dotcom platform team before adding more.
9193
* Scripts will be executed in the order they appear in this array
9294
*/
93-
const scriptTags = generateScriptTags(
94-
[
95-
polyfillIO,
96-
getPathFromManifest(build, 'frameworks.js'),
97-
getPathFromManifest(build, 'index.js'),
98-
getPathFromManifest('web.legacy', 'frameworks.js'),
99-
getPathFromManifest('web.legacy', 'index.js'),
100-
process.env.COMMERCIAL_BUNDLE_URL ??
101-
article.config.commercialBundleUrl,
102-
pageHasNonBootInteractiveElements &&
103-
`${ASSET_ORIGIN}static/frontend/js/curl-with-js-and-domReady.js`,
104-
]
105-
.filter(isString)
106-
.map((script) => (offerHttp3 ? getHttp3Url(script) : script)),
107-
);
95+
const clientScripts = [
96+
polyfillIO,
97+
getPathFromManifest(build, 'frameworks.js'),
98+
getPathFromManifest(build, 'index.js'),
99+
getPathFromManifest('web.legacy', 'frameworks.js'),
100+
getPathFromManifest('web.legacy', 'index.js'),
101+
process.env.COMMERCIAL_BUNDLE_URL ?? article.config.commercialBundleUrl,
102+
pageHasNonBootInteractiveElements &&
103+
`${ASSET_ORIGIN}static/frontend/js/curl-with-js-and-domReady.js`,
104+
]
105+
.filter(isString)
106+
.map((script) => (offerHttp3 ? getHttp3Url(script) : script));
107+
const scriptTags = generateScriptTags(clientScripts);
108108

109109
/**
110110
* We escape windowGuardian here to prevent errors when the data
@@ -195,7 +195,7 @@ window.twttr = (function(d, s, id) {
195195

196196
const { canonicalUrl } = article;
197197

198-
return htmlPageTemplate({
198+
const pageHtml = htmlPageTemplate({
199199
linkedData,
200200
scriptTags,
201201
css: extractedCss,
@@ -216,6 +216,8 @@ window.twttr = (function(d, s, id) {
216216
renderingTarget: 'Web',
217217
weAreHiring: !!article.config.switches.weAreHiring,
218218
});
219+
220+
return { html: pageHtml, clientScripts };
219221
};
220222

221223
/**

dotcom-rendering/src/server/render.front.web.tsx

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ const extractFrontNav = (front: DCRFrontType): NavType => {
6969
};
7070
};
7171

72-
export const renderFront = ({ front }: Props): string => {
72+
export const renderFront = ({
73+
front,
74+
}: Props): { html: string; clientScripts: string[] } => {
7375
const title = front.webTitle;
7476
const NAV = extractFrontNav(front);
7577

@@ -93,19 +95,17 @@ export const renderFront = ({ front }: Props): string => {
9395
* Please talk to the dotcom platform team before adding more.
9496
* Scripts will be executed in the order they appear in this array
9597
*/
96-
const scriptTags = generateScriptTags(
97-
[
98-
polyfillIO,
99-
getPathFromManifest(build, 'frameworks.js'),
100-
getPathFromManifest(build, 'index.js'),
101-
getPathFromManifest('web.legacy', 'frameworks.js'),
102-
getPathFromManifest('web.legacy', 'index.js'),
103-
process.env.COMMERCIAL_BUNDLE_URL ??
104-
front.config.commercialBundleUrl,
105-
]
106-
.filter(isString)
107-
.map((script) => (offerHttp3 ? getHttp3Url(script) : script)),
108-
);
98+
const clientScripts = [
99+
polyfillIO,
100+
getPathFromManifest(build, 'frameworks.js'),
101+
getPathFromManifest(build, 'index.js'),
102+
getPathFromManifest('web.legacy', 'frameworks.js'),
103+
getPathFromManifest('web.legacy', 'index.js'),
104+
process.env.COMMERCIAL_BUNDLE_URL ?? front.config.commercialBundleUrl,
105+
]
106+
.filter(isString)
107+
.map((script) => (offerHttp3 ? getHttp3Url(script) : script));
108+
const scriptTags = generateScriptTags(clientScripts);
109109

110110
const guardian = createGuardian({
111111
editionId: front.editionId,
@@ -130,7 +130,7 @@ export const renderFront = ({ front }: Props): string => {
130130

131131
const keywords = front.config.keywords;
132132

133-
return htmlPageTemplate({
133+
const pageHtml = htmlPageTemplate({
134134
scriptTags,
135135
css: extractedCss,
136136
html,
@@ -143,13 +143,18 @@ export const renderFront = ({ front }: Props): string => {
143143
hasPageSkin: front.config.hasPageSkin,
144144
weAreHiring: !!front.config.switches.weAreHiring,
145145
});
146+
147+
return {
148+
html: pageHtml,
149+
clientScripts,
150+
};
146151
};
147152

148153
export const renderTagFront = ({
149154
tagFront,
150155
}: {
151156
tagFront: DCRTagFrontType;
152-
}): string => {
157+
}): { html: string; clientScripts: string[] } => {
153158
const title = tagFront.webTitle;
154159
const NAV = extractNAV(tagFront.nav);
155160

@@ -173,19 +178,18 @@ export const renderTagFront = ({
173178
* Please talk to the dotcom platform team before adding more.
174179
* Scripts will be executed in the order they appear in this array
175180
*/
176-
const scriptTags = generateScriptTags(
177-
[
178-
polyfillIO,
179-
getPathFromManifest(build, 'frameworks.js'),
180-
getPathFromManifest(build, 'index.js'),
181-
getPathFromManifest('web.legacy', 'frameworks.js'),
182-
getPathFromManifest('web.legacy', 'index.js'),
183-
process.env.COMMERCIAL_BUNDLE_URL ??
184-
tagFront.config.commercialBundleUrl,
185-
]
186-
.filter(isString)
187-
.map((script) => (offerHttp3 ? getHttp3Url(script) : script)),
188-
);
181+
const clientScripts = [
182+
polyfillIO,
183+
getPathFromManifest(build, 'frameworks.js'),
184+
getPathFromManifest(build, 'index.js'),
185+
getPathFromManifest('web.legacy', 'frameworks.js'),
186+
getPathFromManifest('web.legacy', 'index.js'),
187+
process.env.COMMERCIAL_BUNDLE_URL ??
188+
tagFront.config.commercialBundleUrl,
189+
]
190+
.filter(isString)
191+
.map((script) => (offerHttp3 ? getHttp3Url(script) : script));
192+
const scriptTags = generateScriptTags(clientScripts);
189193

190194
const guardian = createGuardian({
191195
editionId: tagFront.editionId,
@@ -210,7 +214,7 @@ export const renderTagFront = ({
210214

211215
const keywords = tagFront.config.keywords;
212216

213-
return htmlPageTemplate({
217+
const pageHtml = htmlPageTemplate({
214218
scriptTags,
215219
css: extractedCss,
216220
html,
@@ -222,4 +226,8 @@ export const renderTagFront = ({
222226
renderingTarget: 'Web',
223227
weAreHiring: !!tagFront.config.switches.weAreHiring,
224228
});
229+
return {
230+
html: pageHtml,
231+
clientScripts,
232+
};
225233
};

0 commit comments

Comments
 (0)