From 4f00299d47807fa23f838d4bece7f0251bd9acbb Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:47:27 -0400 Subject: [PATCH 1/8] feat: pass `name` to the `preload` filter for fonts and assets Assets are emitted as `[name].[hash][extname]`, so the source file name can be recovered from the output path without a manifest lookup. The font and modulepreload paths each branched on `link_header_preload` themselves; a Link header value is the `` attribute list joined with `; ` plus `nopush`, so one helper now covers both. --- .changeset/hungry-hounds-argue.md | 5 ++ documentation/docs/30-advanced/20-hooks.md | 2 +- packages/kit/src/exports/public.d.ts | 10 +++- .../kit/src/runtime/server/page/render.js | 53 ++++++++----------- packages/kit/types/index.d.ts | 10 +++- 5 files changed, 47 insertions(+), 33 deletions(-) create mode 100644 .changeset/hungry-hounds-argue.md diff --git a/.changeset/hungry-hounds-argue.md b/.changeset/hungry-hounds-argue.md new file mode 100644 index 000000000000..8e630f12fd7e --- /dev/null +++ b/.changeset/hungry-hounds-argue.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: pass `name` to the `preload` filter for fonts and assets diff --git a/documentation/docs/30-advanced/20-hooks.md b/documentation/docs/30-advanced/20-hooks.md index 6d1275d8665d..621555511f1c 100644 --- a/documentation/docs/30-advanced/20-hooks.md +++ b/documentation/docs/30-advanced/20-hooks.md @@ -45,7 +45,7 @@ You can define multiple `handle` functions and execute them with the [`sequence` - `transformPageChunk(opts: { html: string, done: boolean }): MaybePromise` — applies custom transforms to HTML. If `done` is true, it's the final chunk. Chunks are not guaranteed to be well-formed HTML (they could include an element's opening tag but not its closing tag, for example) but they will always be split at sensible boundaries such as `%sveltekit.head%` or layout/page components. - `filterSerializedResponseHeaders(name: string, value: string): boolean` — determines which headers should be included in serialized responses when a `load` function loads a resource with `fetch`. By default, none will be included. -- `preload(input: { type: 'js' | 'css' | 'font' | 'asset', path: string }): boolean` — determines which files should be preloaded. Files are preloaded via `` tags added to the `` tag; if [`output.linkHeaderPreload`](configuration#output) is enabled, dynamically rendered pages use the [`Link` response header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) instead. The method is called with each file that was found at build time while constructing the code chunks — so if you for example have `import './styles.css` in your `+page.svelte`, `preload` will be called with the resolved path to that CSS file when visiting that page. Note that in dev mode `preload` is _not_ called, since it depends on analysis that happens at build time. Preloading can improve performance by downloading assets sooner, but it can also hurt if too much is downloaded unnecessarily. By default, `js` and `css` files will be preloaded. `asset` files are not preloaded at all currently, but we may add this later after evaluating feedback. +- `preload(input: { type: 'js' | 'css', path: string } | { type: 'font' | 'asset', path: string, name: string }): boolean` — determines which files should be preloaded. Files are preloaded via `` tags added to the `` tag; if [`output.linkHeaderPreload`](configuration#output) is enabled, dynamically rendered pages use the [`Link` response header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) instead. The method is called with each file that was found at build time while constructing the code chunks — so if you for example have `import './styles.css` in your `+page.svelte`, `preload` will be called with the resolved path to that CSS file when visiting that page. Note that in dev mode `preload` is _not_ called, since it depends on analysis that happens at build time. Preloading can improve performance by downloading assets sooner, but it can also hurt if too much is downloaded unnecessarily. By default, `js` and `css` files will be preloaded. `asset` files are not preloaded at all currently, but we may add this later after evaluating feedback. `font` and `asset` files also receive `name`, the file name before hashing, so that a filter can match on it instead of the hashed path. Characters that can't appear in a URL are rewritten in the output, so a font saved as `inter+bold.woff2` has a `name` of `inter_bold.woff2`. ```js /// file: src/hooks.server.js diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 8b493da6337a..cc24972fa6da 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -1732,9 +1732,17 @@ export interface ResolveOptions { * `` tag; if `output.linkHeaderPreload` is enabled, dynamically rendered pages use the * [`Link` response header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) instead. * By default, `js` and `css` files will be preloaded. + * + * `font` and `asset` files also receive `name`, the source file name before hashing, so that a + * filter can match on it instead of the hashed path. `js` and `css` files are bundled and have no + * single source file name. * @param input the type of the file and its path */ - preload?: (input: { type: 'font' | 'css' | 'js' | 'asset'; path: string }) => boolean; + preload?: ( + input: + | { type: 'css' | 'js'; path: string } + | { type: 'font' | 'asset'; path: string; name: string } + ) => boolean; } export interface RouteDefinition { diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index c63329967881..3df91819353f 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -290,6 +290,19 @@ export async function render_response({ return `${assets}/${path}`; }; + /** + * see the `output.preloadStrategy` option for details on why we have multiple options here + * @param {string} path + * @param {string[]} attributes + */ + const add_preload = (path, attributes) => { + if (options.link_header_preload && !state.prerendering) { + link_headers.add(`<${encodeURI(path)}>; ${attributes.join('; ')}; nopush`); + } else { + head.add_link_tag(path, attributes); + } + }; + const style = client?.inline ? client.inline?.style : Array.from(inline_styles.values()).join('\n'); @@ -325,21 +338,12 @@ export async function render_response({ for (const dep of fonts) { const path = prefixed(dep); - if (resolve_opts.preload({ type: 'font', path })) { - const ext = dep.slice(dep.lastIndexOf('.') + 1); + // assets are emitted as `[name].[hash][extname]` + const name = dep.slice(dep.lastIndexOf('/') + 1).replace(/\.[^.]+(?=\.[^.]+$)/, ''); + const ext = name.slice(name.lastIndexOf('.') + 1); - if (options.link_header_preload && !state.prerendering) { - link_headers.add( - `<${encodeURI(path)}>; rel="preload"; as="font"; type="font/${ext}"; crossorigin; nopush` - ); - } else { - head.add_link_tag(path, [ - 'rel="preload"', - 'as="font"', - `type="font/${ext}"`, - 'crossorigin' - ]); - } + if (resolve_opts.preload({ type: 'font', path, name })) { + add_preload(path, ['rel="preload"', 'as="font"', `type="font/${ext}"`, 'crossorigin']); } } @@ -368,23 +372,12 @@ export async function render_response({ } if (!client.inline) { - const included_modulepreloads = Array.from(modulepreloads, (dep) => prefixed(dep)).filter( - (path) => resolve_opts.preload({ type: 'js', path }) - ); - - /** @type {(path: string) => void} */ - let add_preload; + for (const dep of modulepreloads) { + const path = prefixed(dep); - // see the output.preloadStrategy option for details on why we have multiple options here - if (options.link_header_preload && !state.prerendering) { - add_preload = (path) => - link_headers.add(`<${encodeURI(path)}>; rel="modulepreload"; nopush`); - } else { - add_preload = (path) => head.add_link_tag(path, ['rel="modulepreload"']); - } - - for (const path of included_modulepreloads) { - add_preload(path); + if (resolve_opts.preload({ type: 'js', path })) { + add_preload(path, ['rel="modulepreload"']); + } } } diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index b35569cdfc29..600063d2c12b 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -1702,9 +1702,17 @@ declare module '@sveltejs/kit' { * `` tag; if `output.linkHeaderPreload` is enabled, dynamically rendered pages use the * [`Link` response header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) instead. * By default, `js` and `css` files will be preloaded. + * + * `font` and `asset` files also receive `name`, the source file name before hashing, so that a + * filter can match on it instead of the hashed path. `js` and `css` files are bundled and have no + * single source file name. * @param input the type of the file and its path */ - preload?: (input: { type: 'font' | 'css' | 'js' | 'asset'; path: string }) => boolean; + preload?: ( + input: + | { type: 'css' | 'js'; path: string } + | { type: 'font' | 'asset'; path: string; name: string } + ) => boolean; } export interface RouteDefinition { From be0eacdf7fcee0775ad970a4f297e4aeee14b9f1 Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:47:27 -0400 Subject: [PATCH 2/8] test: cover `name` in the preload filter --- packages/kit/test/apps/basics/src/hooks.server.js | 4 ++-- packages/kit/test/apps/basics/test/server.test.js | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/kit/test/apps/basics/src/hooks.server.js b/packages/kit/test/apps/basics/src/hooks.server.js index dc20c948692c..b6eb4d00a107 100644 --- a/packages/kit/test/apps/basics/src/hooks.server.js +++ b/packages/kit/test/apps/basics/src/hooks.server.js @@ -193,8 +193,8 @@ export const handle = sequence( } return resolve(event, { - // needed for asset-preload tests - preload: () => true + // needed for asset-preload tests, which assert `name` is the unhashed file name + preload: (input) => input.type !== 'font' || input.name === 'shlop.woff2' }); } ); diff --git a/packages/kit/test/apps/basics/test/server.test.js b/packages/kit/test/apps/basics/test/server.test.js index f6890b012d33..b0054d2b7ecc 100644 --- a/packages/kit/test/apps/basics/test/server.test.js +++ b/packages/kit/test/apps/basics/test/server.test.js @@ -1492,6 +1492,7 @@ test.describe('asset preload', () => { expect(body).toContain('rel="modulepreload"'); expect(body).toContain('as="font"'); + expect(body).toMatch(/href="[^"]+\/shlop\.[^".]+\.woff2"/); }); }); From 46550e086641ed90f4e7c08f03029a8ca160fea5 Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:15:19 -0400 Subject: [PATCH 3/8] chore: fix stale preloadStrategy comment --- packages/kit/src/runtime/server/page/render.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 3df91819353f..72767a0ca352 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -291,7 +291,7 @@ export async function render_response({ }; /** - * see the `output.preloadStrategy` option for details on why we have multiple options here + * see the `output.linkHeaderPreload` option for details on why we have multiple options here * @param {string} path * @param {string[]} attributes */ From e25a23660bd9f567c9e84de225f2409a95d4e85b Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:15:19 -0400 Subject: [PATCH 4/8] docs: correct the description of name --- documentation/docs/30-advanced/20-hooks.md | 2 +- packages/kit/src/exports/public.d.ts | 7 ++++--- packages/kit/types/index.d.ts | 7 ++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/documentation/docs/30-advanced/20-hooks.md b/documentation/docs/30-advanced/20-hooks.md index 621555511f1c..ac300e5303a8 100644 --- a/documentation/docs/30-advanced/20-hooks.md +++ b/documentation/docs/30-advanced/20-hooks.md @@ -45,7 +45,7 @@ You can define multiple `handle` functions and execute them with the [`sequence` - `transformPageChunk(opts: { html: string, done: boolean }): MaybePromise` — applies custom transforms to HTML. If `done` is true, it's the final chunk. Chunks are not guaranteed to be well-formed HTML (they could include an element's opening tag but not its closing tag, for example) but they will always be split at sensible boundaries such as `%sveltekit.head%` or layout/page components. - `filterSerializedResponseHeaders(name: string, value: string): boolean` — determines which headers should be included in serialized responses when a `load` function loads a resource with `fetch`. By default, none will be included. -- `preload(input: { type: 'js' | 'css', path: string } | { type: 'font' | 'asset', path: string, name: string }): boolean` — determines which files should be preloaded. Files are preloaded via `` tags added to the `` tag; if [`output.linkHeaderPreload`](configuration#output) is enabled, dynamically rendered pages use the [`Link` response header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) instead. The method is called with each file that was found at build time while constructing the code chunks — so if you for example have `import './styles.css` in your `+page.svelte`, `preload` will be called with the resolved path to that CSS file when visiting that page. Note that in dev mode `preload` is _not_ called, since it depends on analysis that happens at build time. Preloading can improve performance by downloading assets sooner, but it can also hurt if too much is downloaded unnecessarily. By default, `js` and `css` files will be preloaded. `asset` files are not preloaded at all currently, but we may add this later after evaluating feedback. `font` and `asset` files also receive `name`, the file name before hashing, so that a filter can match on it instead of the hashed path. Characters that can't appear in a URL are rewritten in the output, so a font saved as `inter+bold.woff2` has a `name` of `inter_bold.woff2`. +- `preload(input: { type: 'js' | 'css', path: string } | { type: 'font' | 'asset', path: string, name: string }): boolean` — determines which files should be preloaded. Files are preloaded via `` tags added to the `` tag; if [`output.linkHeaderPreload`](configuration#output) is enabled, dynamically rendered pages use the [`Link` response header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) instead. The method is called with each file that was found at build time while constructing the code chunks — so if you for example have `import './styles.css` in your `+page.svelte`, `preload` will be called with the resolved path to that CSS file when visiting that page. Note that in dev mode `preload` is _not_ called, since it depends on analysis that happens at build time. Preloading can improve performance by downloading assets sooner, but it can also hurt if too much is downloaded unnecessarily. By default, `js` and `css` files will be preloaded. `asset` files are not preloaded at all currently, but we may add this later after evaluating feedback. For `font` and `asset` files, `input` also has a `name` property, the file name before hashing, so that a filter can match on it instead of the hashed path. Some characters are replaced in emitted file names, so a font saved as `inter+bold.woff2` has a `name` of `inter_bold.woff2`. ```js /// file: src/hooks.server.js diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index cc24972fa6da..e0c91f4cb173 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -1733,9 +1733,10 @@ export interface ResolveOptions { * [`Link` response header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) instead. * By default, `js` and `css` files will be preloaded. * - * `font` and `asset` files also receive `name`, the source file name before hashing, so that a - * filter can match on it instead of the hashed path. `js` and `css` files are bundled and have no - * single source file name. + * For `font` and `asset` files, `input` also has a `name` property, the file name before hashing, + * so that a filter can match on it instead of the hashed path. Some characters are replaced in + * emitted file names, so a font saved as `inter+bold.woff2` has a `name` of `inter_bold.woff2`. + * `js` and `css` files are bundled and have no single source file name. * @param input the type of the file and its path */ preload?: ( diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 600063d2c12b..92e6ef953b8f 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -1703,9 +1703,10 @@ declare module '@sveltejs/kit' { * [`Link` response header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) instead. * By default, `js` and `css` files will be preloaded. * - * `font` and `asset` files also receive `name`, the source file name before hashing, so that a - * filter can match on it instead of the hashed path. `js` and `css` files are bundled and have no - * single source file name. + * For `font` and `asset` files, `input` also has a `name` property, the file name before hashing, + * so that a filter can match on it instead of the hashed path. Some characters are replaced in + * emitted file names, so a font saved as `inter+bold.woff2` has a `name` of `inter_bold.woff2`. + * `js` and `css` files are bundled and have no single source file name. * @param input the type of the file and its path */ preload?: ( From 22364b826cd2773ef28d8a8ee575256a73d95eee Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:15:19 -0400 Subject: [PATCH 5/8] test: cover dotted font names --- .../kit/test/apps/basics/src/hooks.server.js | 3 ++- .../src/routes/asset-preload/shlop.var.woff2 | Bin 0 -> 28484 bytes .../basics/src/routes/asset-preload/styles.css | 6 ++++++ .../kit/test/apps/basics/test/server.test.js | 1 + 4 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 packages/kit/test/apps/basics/src/routes/asset-preload/shlop.var.woff2 diff --git a/packages/kit/test/apps/basics/src/hooks.server.js b/packages/kit/test/apps/basics/src/hooks.server.js index b6eb4d00a107..594b0b4f725c 100644 --- a/packages/kit/test/apps/basics/src/hooks.server.js +++ b/packages/kit/test/apps/basics/src/hooks.server.js @@ -194,7 +194,8 @@ export const handle = sequence( return resolve(event, { // needed for asset-preload tests, which assert `name` is the unhashed file name - preload: (input) => input.type !== 'font' || input.name === 'shlop.woff2' + preload: (input) => + input.type !== 'font' || ['shlop.woff2', 'shlop.var.woff2'].includes(input.name) }); } ); diff --git a/packages/kit/test/apps/basics/src/routes/asset-preload/shlop.var.woff2 b/packages/kit/test/apps/basics/src/routes/asset-preload/shlop.var.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..95964213afa74b6ecc3a30b956bff34e84d14f3f GIT binary patch literal 28484 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%0Dnu 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 D{ { expect(body).toContain('rel="modulepreload"'); expect(body).toContain('as="font"'); expect(body).toMatch(/href="[^"]+\/shlop\.[^".]+\.woff2"/); + expect(body).toMatch(/href="[^"]+\/shlop\.var\.[^".]+\.woff2"/); }); }); From 044e6ac4a196a7fca20452f066eab1502c625795 Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:33:44 -0400 Subject: [PATCH 6/8] fix: derive font preload names from the Vite manifest --- .changeset/hungry-hounds-argue.md | 2 +- documentation/docs/30-advanced/20-hooks.md | 2 +- packages/kit/src/exports/public.d.ts | 11 ++++---- .../src/exports/vite/build/build_server.js | 4 +-- packages/kit/src/exports/vite/build/utils.js | 25 +++++++++++++++--- .../kit/src/runtime/server/page/render.js | 15 +++++------ packages/kit/src/types/internal.d.ts | 13 ++++++--- .../kit/test/apps/basics/src/hooks.server.js | 3 ++- .../src/routes/asset-preload/shlop+bold.woff2 | Bin 0 -> 28486 bytes .../src/routes/asset-preload/styles.css | 5 ++++ .../kit/test/apps/basics/test/server.test.js | 2 ++ packages/kit/types/index.d.ts | 11 ++++---- 12 files changed, 61 insertions(+), 32 deletions(-) create mode 100644 packages/kit/test/apps/basics/src/routes/asset-preload/shlop+bold.woff2 diff --git a/.changeset/hungry-hounds-argue.md b/.changeset/hungry-hounds-argue.md index 8e630f12fd7e..81fe65d98f35 100644 --- a/.changeset/hungry-hounds-argue.md +++ b/.changeset/hungry-hounds-argue.md @@ -2,4 +2,4 @@ '@sveltejs/kit': minor --- -feat: pass `name` to the `preload` filter for fonts and assets +feat: pass the source file `name` to the `preload` filter for fonts diff --git a/documentation/docs/30-advanced/20-hooks.md b/documentation/docs/30-advanced/20-hooks.md index ac300e5303a8..4213ad7b7ca2 100644 --- a/documentation/docs/30-advanced/20-hooks.md +++ b/documentation/docs/30-advanced/20-hooks.md @@ -45,7 +45,7 @@ You can define multiple `handle` functions and execute them with the [`sequence` - `transformPageChunk(opts: { html: string, done: boolean }): MaybePromise` — applies custom transforms to HTML. If `done` is true, it's the final chunk. Chunks are not guaranteed to be well-formed HTML (they could include an element's opening tag but not its closing tag, for example) but they will always be split at sensible boundaries such as `%sveltekit.head%` or layout/page components. - `filterSerializedResponseHeaders(name: string, value: string): boolean` — determines which headers should be included in serialized responses when a `load` function loads a resource with `fetch`. By default, none will be included. -- `preload(input: { type: 'js' | 'css', path: string } | { type: 'font' | 'asset', path: string, name: string }): boolean` — determines which files should be preloaded. Files are preloaded via `` tags added to the `` tag; if [`output.linkHeaderPreload`](configuration#output) is enabled, dynamically rendered pages use the [`Link` response header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) instead. The method is called with each file that was found at build time while constructing the code chunks — so if you for example have `import './styles.css` in your `+page.svelte`, `preload` will be called with the resolved path to that CSS file when visiting that page. Note that in dev mode `preload` is _not_ called, since it depends on analysis that happens at build time. Preloading can improve performance by downloading assets sooner, but it can also hurt if too much is downloaded unnecessarily. By default, `js` and `css` files will be preloaded. `asset` files are not preloaded at all currently, but we may add this later after evaluating feedback. For `font` and `asset` files, `input` also has a `name` property, the file name before hashing, so that a filter can match on it instead of the hashed path. Some characters are replaced in emitted file names, so a font saved as `inter+bold.woff2` has a `name` of `inter_bold.woff2`. +- `preload(input: { type: 'js' | 'css' | 'asset', path: string } | { type: 'font', path: string, name: string }): boolean` — determines which files should be preloaded. Files are preloaded via `` tags added to the `` tag; if [`output.linkHeaderPreload`](configuration#output) is enabled, dynamically rendered pages use the [`Link` response header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) instead. The method is called with each file that was found at build time while constructing the code chunks — so if you for example have `import './styles.css` in your `+page.svelte`, `preload` will be called with the resolved path to that CSS file when visiting that page. Note that in dev mode `preload` is _not_ called, since it depends on analysis that happens at build time. Preloading can improve performance by downloading assets sooner, but it can also hurt if too much is downloaded unnecessarily. By default, `js` and `css` files will be preloaded. `asset` files are not preloaded at all currently, but we may add this later after evaluating feedback. For `font` files, `input` also has a `name` property, the source file's name, so that a filter can match on it instead of the hashed path. ```js /// file: src/hooks.server.js diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index e0c91f4cb173..c413d6e13c82 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -1733,16 +1733,15 @@ export interface ResolveOptions { * [`Link` response header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) instead. * By default, `js` and `css` files will be preloaded. * - * For `font` and `asset` files, `input` also has a `name` property, the file name before hashing, - * so that a filter can match on it instead of the hashed path. Some characters are replaced in - * emitted file names, so a font saved as `inter+bold.woff2` has a `name` of `inter_bold.woff2`. - * `js` and `css` files are bundled and have no single source file name. + * For `font` files, `input` also has a `name` property, the source file's name, so that a + * filter can match on it instead of the hashed path. `js` and `css` files are bundled and + * have no single source file name. * @param input the type of the file and its path */ preload?: ( input: - | { type: 'css' | 'js'; path: string } - | { type: 'font' | 'asset'; path: string; name: string } + | { type: 'css' | 'js' | 'asset'; path: string } + | { type: 'font'; path: string; name: string } ) => boolean; } diff --git a/packages/kit/src/exports/vite/build/build_server.js b/packages/kit/src/exports/vite/build/build_server.js index de55700d5cb5..225904c6eba3 100644 --- a/packages/kit/src/exports/vite/build/build_server.js +++ b/packages/kit/src/exports/vite/build/build_server.js @@ -129,7 +129,7 @@ export function build_server_nodes( /** @type {string[]} */ let stylesheets = []; - /** @type {string[]} */ + /** @type {import('types').FontDependency[]} */ let fonts = []; /** @type {Set} */ @@ -213,7 +213,7 @@ export function build_server_nodes( imported = entry.imports; stylesheets = Array.from(eager_css); - fonts = filter_fonts(Array.from(eager_assets)); + fonts = filter_fonts(Array.from(eager_assets), client_manifest); } else { for (const entry of [component, universal]) { if (!entry) continue; diff --git a/packages/kit/src/exports/vite/build/utils.js b/packages/kit/src/exports/vite/build/utils.js index 503c959f4b0a..f2055a919660 100644 --- a/packages/kit/src/exports/vite/build/utils.js +++ b/packages/kit/src/exports/vite/build/utils.js @@ -94,7 +94,7 @@ export function find_deps(manifest, entry, add_dynamic_css, root) { imports: Array.from(imports), stylesheets: Array.from(stylesheets), // TODO do we need this separately? - fonts: filter_fonts(assets), + fonts: filter_fonts(assets, manifest), stylesheet_map }; } @@ -118,10 +118,27 @@ export function resolve_symlinks(manifest, file, root) { /** * @param {string[]} assets - * @returns {string[]} + * @param {import('vite').Manifest} manifest + * @returns {import('types').FontDependency[]} */ -export function filter_fonts(assets) { - return assets.filter((asset) => /\.(woff2?|ttf|otf)$/.test(asset)); +export function filter_fonts(assets, manifest) { + /** @type {Map} */ + const sources = new Map(); + + // identical files are emitted once but can have several sources — keep the first + for (const key of Object.keys(manifest).sort()) { + const { file, src } = manifest[key]; + if (src && !sources.has(file)) { + sources.set(file, src); + } + } + + return assets + .filter((asset) => /\.(woff2?|ttf|otf)$/.test(asset)) + .map((file) => { + const src = sources.get(file) ?? file; + return { file, name: src.slice(src.lastIndexOf('/') + 1) }; + }); } /** diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index f6e9f3771528..f3e8fab36660 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -72,7 +72,9 @@ export async function render_response({ const modulepreloads = new Set(client?.imports); const stylesheets = new Set(client?.stylesheets); - const fonts = new Set(client?.fonts); + + /** @type {Map} */ + const fonts = new Map(client?.fonts.map((font) => [font.file, font])); /** * The value of the Link header that is added to the response when not prerendering @@ -262,7 +264,7 @@ export async function render_response({ for (const { node } of branch) { for (const url of node.imports) modulepreloads.add(url); for (const url of node.stylesheets) stylesheets.add(url); - for (const url of node.fonts) fonts.add(url); + for (const font of node.fonts) fonts.set(font.file, font); if (node.inline_styles && !client?.inline) { Object.entries(await node.inline_styles()).forEach(([filename, css]) => { @@ -335,14 +337,11 @@ export async function render_response({ head.add_stylesheet(path, attributes); } - for (const dep of fonts) { - const path = prefixed(dep); - - // assets are emitted as `[name].[hash][extname]` - const name = dep.slice(dep.lastIndexOf('/') + 1).replace(/\.[^.]+(?=\.[^.]+$)/, ''); + for (const { file, name } of fonts.values()) { + const path = prefixed(file); if (resolve_opts.preload({ type: 'font', path, name })) { - const ext = dep.slice(dep.lastIndexOf('.') + 1); + const ext = file.slice(file.lastIndexOf('.') + 1); add_preload(path, ['rel="preload"', 'as="font"', `type="font/${ext}"`, 'crossorigin']); } diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 547a3f103758..197f7bdf542d 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -62,10 +62,17 @@ export interface AssetDependencies { file: string; imports: string[]; stylesheets: string[]; - fonts: string[]; + fonts: FontDependency[]; stylesheet_map: Map; assets: Set }>; } +export interface FontDependency { + /** emitted file path, relative to the client output directory */ + file: string; + /** the source file's basename, before hashing and character sanitization */ + name: string; +} + export interface BuildData { app_dir: string; app_path: string; @@ -98,7 +105,7 @@ export interface BuildData { */ routes?: SSRClientRoute[]; stylesheets: string[]; - fonts: string[]; + fonts: FontDependency[]; /** * Whether the client uses public dynamic env vars — `$env/dynamic/public` or `$app/env/public`. */ @@ -449,7 +456,7 @@ export interface SSRNode { /** external CSS files that are loaded on the client */ stylesheets: string[]; /** external font files that are loaded on the client */ - fonts: string[]; + fonts: FontDependency[]; universal_id?: string; server_id?: string; diff --git a/packages/kit/test/apps/basics/src/hooks.server.js b/packages/kit/test/apps/basics/src/hooks.server.js index 594b0b4f725c..a78eb04955cd 100644 --- a/packages/kit/test/apps/basics/src/hooks.server.js +++ b/packages/kit/test/apps/basics/src/hooks.server.js @@ -195,7 +195,8 @@ export const handle = sequence( return resolve(event, { // needed for asset-preload tests, which assert `name` is the unhashed file name preload: (input) => - input.type !== 'font' || ['shlop.woff2', 'shlop.var.woff2'].includes(input.name) + input.type !== 'font' || + ['shlop.woff2', 'shlop.var.woff2', 'shlop+bold.woff2'].includes(input.name) }); } ); diff --git a/packages/kit/test/apps/basics/src/routes/asset-preload/shlop+bold.woff2 b/packages/kit/test/apps/basics/src/routes/asset-preload/shlop+bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..c213444215c17f96e5bae649339bef3277e9cd51 GIT binary patch literal 28486 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%0Dnu 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 FVPtV { expect(body).toContain('as="font"'); expect(body).toMatch(/href="[^"]+\/shlop\.[^".]+\.woff2"/); expect(body).toMatch(/href="[^"]+\/shlop\.var\.[^".]+\.woff2"/); + // the emitted file name is sanitized, but the filter matched on the original `shlop+bold.woff2` + expect(body).toMatch(/href="[^"]+\/shlop_bold\.[^".]+\.woff2"/); }); }); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 92e6ef953b8f..16cd6356f5e8 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -1703,16 +1703,15 @@ declare module '@sveltejs/kit' { * [`Link` response header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) instead. * By default, `js` and `css` files will be preloaded. * - * For `font` and `asset` files, `input` also has a `name` property, the file name before hashing, - * so that a filter can match on it instead of the hashed path. Some characters are replaced in - * emitted file names, so a font saved as `inter+bold.woff2` has a `name` of `inter_bold.woff2`. - * `js` and `css` files are bundled and have no single source file name. + * For `font` files, `input` also has a `name` property, the source file's name, so that a + * filter can match on it instead of the hashed path. `js` and `css` files are bundled and + * have no single source file name. * @param input the type of the file and its path */ preload?: ( input: - | { type: 'css' | 'js'; path: string } - | { type: 'font' | 'asset'; path: string; name: string } + | { type: 'css' | 'js' | 'asset'; path: string } + | { type: 'font'; path: string; name: string } ) => boolean; } From f8ccd81f076706beb80cf81b132a9762ed545625 Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:38:57 -0400 Subject: [PATCH 7/8] chore: build the font source map once per manifest --- packages/kit/src/exports/vite/build/utils.js | 23 +++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/kit/src/exports/vite/build/utils.js b/packages/kit/src/exports/vite/build/utils.js index f2055a919660..17c1cba54c4f 100644 --- a/packages/kit/src/exports/vite/build/utils.js +++ b/packages/kit/src/exports/vite/build/utils.js @@ -116,20 +116,27 @@ export function resolve_symlinks(manifest, file, root) { return { chunk, file }; } +/** @type {WeakMap>} */ +const source_maps = new WeakMap(); + /** * @param {string[]} assets * @param {import('vite').Manifest} manifest * @returns {import('types').FontDependency[]} */ export function filter_fonts(assets, manifest) { - /** @type {Map} */ - const sources = new Map(); - - // identical files are emitted once but can have several sources — keep the first - for (const key of Object.keys(manifest).sort()) { - const { file, src } = manifest[key]; - if (src && !sources.has(file)) { - sources.set(file, src); + let sources = source_maps.get(manifest); + + if (!sources) { + sources = new Map(); + source_maps.set(manifest, sources); + + // identical files are emitted once but can have several sources — keep the first + for (const key of Object.keys(manifest).sort()) { + const { file, src } = manifest[key]; + if (src && !sources.has(file)) { + sources.set(file, src); + } } } From 0f6fcc8892a19d7384438ce2eb30b94d7cd8e06e Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:49:56 -0400 Subject: [PATCH 8/8] chore: use path.basename --- packages/kit/src/exports/vite/build/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/exports/vite/build/utils.js b/packages/kit/src/exports/vite/build/utils.js index 17c1cba54c4f..b7aa684b353e 100644 --- a/packages/kit/src/exports/vite/build/utils.js +++ b/packages/kit/src/exports/vite/build/utils.js @@ -144,7 +144,7 @@ export function filter_fonts(assets, manifest) { .filter((asset) => /\.(woff2?|ttf|otf)$/.test(asset)) .map((file) => { const src = sources.get(file) ?? file; - return { file, name: src.slice(src.lastIndexOf('/') + 1) }; + return { file, name: path.basename(src) }; }); }