Skip to content

Commit 0bf6185

Browse files
gjung56Rich-Harris
andauthored
feat: add link header when preloading font (#14200)
* Add link header when preloading font * add test * beef up test * only inject `<link>` tags when prerendering * remove .only * fix test * changeset --------- Co-authored-by: Rich Harris <[email protected]> Co-authored-by: Rich Harris <[email protected]>
1 parent 948d6c8 commit 0bf6185

File tree

9 files changed

+103
-18
lines changed

9 files changed

+103
-18
lines changed

.changeset/nervous-socks-happen.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@sveltejs/kit": patch
3+
---
4+
5+
fix: add link header when preloading font

packages/kit/src/runtime/server/page/render.js

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,18 @@ export async function render_response({
7272
const stylesheets = new Set(client.stylesheets);
7373
const fonts = new Set(client.fonts);
7474

75-
/** @type {Set<string>} */
76-
const link_header_preloads = new Set();
75+
/**
76+
* The value of the Link header that is added to the response when not prerendering
77+
* @type {Set<string>}
78+
*/
79+
const link_headers = new Set();
80+
81+
/**
82+
* `<link>` tags that are added to prerendered responses
83+
* (note that stylesheets are always added, prerendered or not)
84+
* @type {Set<string>}
85+
*/
86+
const link_tags = new Set();
7787

7888
/** @type {Map<string, string>} */
7989
// TODO if we add a client entry point one day, we will need to include inline_styles with the entry, otherwise stylesheets will be linked even if they are below inlineStyleThreshold
@@ -264,8 +274,7 @@ export async function render_response({
264274
attributes.push('disabled', 'media="(max-width: 0)"');
265275
} else {
266276
if (resolve_opts.preload({ type: 'css', path })) {
267-
const preload_atts = ['rel="preload"', 'as="style"'];
268-
link_header_preloads.add(`<${encodeURI(path)}>; ${preload_atts.join(';')}; nopush`);
277+
link_headers.add(`<${encodeURI(path)}>; rel="preload"; as="style"; nopush`);
269278
}
270279
}
271280

@@ -277,15 +286,12 @@ export async function render_response({
277286

278287
if (resolve_opts.preload({ type: 'font', path })) {
279288
const ext = dep.slice(dep.lastIndexOf('.') + 1);
280-
const attributes = [
281-
'rel="preload"',
282-
'as="font"',
283-
`type="font/${ext}"`,
284-
`href="${path}"`,
285-
'crossorigin'
286-
];
287289

288-
head += `\n\t\t<link ${attributes.join(' ')}>`;
290+
link_tags.add(`<link rel="preload" as="font" type="font/${ext}" href="${path}" crossorigin>`);
291+
292+
link_headers.add(
293+
`<${encodeURI(path)}>; rel="preload"; as="font"; type="font/${ext}"; crossorigin; nopush`
294+
);
289295
}
290296
}
291297

@@ -322,15 +328,22 @@ export async function render_response({
322328

323329
for (const path of included_modulepreloads) {
324330
// see the kit.output.preloadStrategy option for details on why we have multiple options here
325-
link_header_preloads.add(`<${encodeURI(path)}>; rel="modulepreload"; nopush`);
331+
link_headers.add(`<${encodeURI(path)}>; rel="modulepreload"; nopush`);
332+
326333
if (options.preload_strategy !== 'modulepreload') {
327334
head += `\n\t\t<link rel="preload" as="script" crossorigin="anonymous" href="${path}">`;
328-
} else if (state.prerendering) {
329-
head += `\n\t\t<link rel="modulepreload" href="${path}">`;
335+
} else {
336+
link_tags.add(`<link rel="modulepreload" href="${path}">`);
330337
}
331338
}
332339
}
333340

341+
if (state.prerendering && link_tags.size > 0) {
342+
head += Array.from(link_tags)
343+
.map((tag) => `\n\t\t${tag}`)
344+
.join('');
345+
}
346+
334347
// prerender a `/path/to/page/__route.js` module
335348
if (manifest._.client.routes && state.prerendering && !state.prerendering.fallback) {
336349
const pathname = add_resolution_suffix(event.url.pathname);
@@ -545,8 +558,8 @@ export async function render_response({
545558
headers.set('content-security-policy-report-only', report_only_header);
546559
}
547560

548-
if (link_header_preloads.size) {
549-
headers.set('link', Array.from(link_header_preloads).join(', '));
561+
if (link_headers.size) {
562+
headers.set('link', Array.from(link_headers).join(', '));
550563
}
551564
}
552565

packages/kit/test/apps/basics/src/hooks.server.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,10 @@ export const handle = sequence(
192192
e.locals.message = 'hello from hooks.server.js';
193193
}
194194

195-
return resolve(event);
195+
return resolve(event, {
196+
// needed for asset-preload tests
197+
preload: () => true
198+
});
196199
}
197200
);
198201

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script>
2+
import './styles.css';
3+
</script>
4+
5+
<h1>asset-preload</h1>
6+
7+
<style>
8+
h1 {
9+
font-family: Shlop;
10+
}
11+
</style>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const prerender = true;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<h1>asset-preload/prerendered</h1>
2+
3+
<style>
4+
@import '../styles.css';
5+
6+
h1 {
7+
font-family: Shlop;
8+
}
9+
</style>
Binary file not shown.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@font-face {
2+
/* we need a font file that exceeds assetsInlineLimit */
3+
font-family: 'Shlop';
4+
src: url(./shlop.woff2);
5+
}

packages/kit/test/apps/basics/test/server.test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,3 +1233,41 @@ test.describe('remote functions', () => {
12331233
expect(fs.existsSync(path.join(root, 'dist'))).toBe(false);
12341234
});
12351235
});
1236+
1237+
test.describe('asset preload', () => {
1238+
if (!process.env.DEV) {
1239+
test('injects Link headers', async ({ request }) => {
1240+
const response = await request.get('/asset-preload');
1241+
1242+
const header = response.headers()['link'];
1243+
1244+
expect(header).toContain('rel="modulepreload"');
1245+
expect(header).toContain('as="font"');
1246+
});
1247+
1248+
test('does not inject Link headers on prerendered pages', async ({ request }) => {
1249+
const response = await request.get('/asset-preload/prerendered');
1250+
1251+
const header = response.headers()['link'];
1252+
expect(header).toBeUndefined();
1253+
});
1254+
1255+
test('injects <link> tags on prerendered pages', async ({ request }) => {
1256+
const response = await request.get('/asset-preload/prerendered');
1257+
1258+
const body = await response.text();
1259+
1260+
expect(body).toContain('rel="modulepreload"');
1261+
expect(body).toContain('as="font"');
1262+
});
1263+
1264+
test('does not inject <link> tags on non-prerendered pages', async ({ request }) => {
1265+
const response = await request.get('/asset-preload');
1266+
1267+
const body = await response.text();
1268+
1269+
expect(body).not.toContain('rel="modulepreload"');
1270+
expect(body).not.toContain('as="font"');
1271+
});
1272+
}
1273+
});

0 commit comments

Comments
 (0)