Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-links-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: add `crossorigin` to SSR stylesheet links and `Link` preload headers so browsers don't block CSS from a cross-origin `paths.assets` after client-side navigation. The asset server must now send `Access-Control-Allow-Origin` for CSS as well as JavaScript. Pages with `csr = false` could load CSS without it before.
2 changes: 2 additions & 0 deletions packages/kit/src/exports/vite/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,8 @@ export interface Config extends VitePluginSvelteOptions {
paths?: {
/**
* An absolute path that your app's files are served from. This is useful if your files are served from a storage bucket of some kind.
*
* The browser loads your app's JavaScript, CSS and fonts from this origin with CORS, so the server must respond with an `Access-Control-Allow-Origin` header that allows your app's origin.
* @default ""
*/
assets?: '' | `http://${string}` | `https://${string}`;
Expand Down
5 changes: 3 additions & 2 deletions packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -312,15 +312,16 @@ export async function render_response({
for (const dep of stylesheets) {
const path = prefixed(dep);

const attributes = ['rel="stylesheet"'];
// match the CORS mode of the stylesheets that Vite's preload helper adds on client-side navigation
const attributes = ['rel="stylesheet"', 'crossorigin'];

if (inline_styles.has(dep)) {
// don't load stylesheets that are already inlined
// include them in disabled state so that Vite can detect them and doesn't try to add them
attributes.push('disabled', 'media="(max-width: 0)"');
} else {
if (__SVELTEKIT_LINK_HEADER_PRELOAD__ && resolve_opts.preload({ type: 'css', path })) {
link_headers.add(`<${encodeURI(path)}>; rel="preload"; as="style"; nopush`);
link_headers.add(`<${encodeURI(path)}>; rel="preload"; as="style"; crossorigin; nopush`);
}
}

Expand Down
12 changes: 12 additions & 0 deletions packages/kit/test/apps/options-2/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,18 @@ test.describe('Link header preload', () => {
expect(header).toContain('as="font"');
});

test('injects Link headers for stylesheets with crossorigin', async ({ request }) => {
const response = await request.get('/basepath/asset-preload');

const header = response.headers()['link'] ?? '';
const stylesheets = header.split(', ').filter((entry) => entry.includes('as="style"'));

expect(stylesheets.length).toBeGreaterThan(0);
for (const entry of stylesheets) {
expect(entry).toContain('; crossorigin;');
}
});

test('does not inject Link headers on prerendered pages', async ({ request }) => {
const response = await request.get('/basepath/asset-preload/prerendered');

Expand Down
16 changes: 16 additions & 0 deletions packages/kit/test/apps/options/test/paths.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,22 @@ test.describe('assets path', () => {
expect(response.status()).toBe(200);
});

test('server-rendered stylesheet links have the crossorigin attribute', async ({
request,
javaScriptEnabled
}) => {
test.skip(!!process.env.DEV || javaScriptEnabled, 'only applicable to the build output');

const response = await request.get('/path-base/base/');
const html = await response.text();

const links = html.match(/<link [^>]*rel="stylesheet"[^>]*>/g) ?? [];
expect(links.length).toBeGreaterThan(0);
for (const link of links) {
expect(link).toMatch(/\scrossorigin[\s>]/);
}
});

test('client avoids generating relative URLs if paths.assets or paths.relative are truthy', async () => {
test.skip(!!process.env.DEV, 'only applicable to the build output');
const nodes = readdirSync('.custom-out-dir/output/client/_wheee/nested/immutable/nodes');
Expand Down
2 changes: 2 additions & 0 deletions packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1894,6 +1894,8 @@ declare module '@sveltejs/kit/vite' {
paths?: {
/**
* An absolute path that your app's files are served from. This is useful if your files are served from a storage bucket of some kind.
*
* The browser loads your app's JavaScript, CSS and fonts from this origin with CORS, so the server must respond with an `Access-Control-Allow-Origin` header that allows your app's origin.
* @default ""
*/
assets?: '' | `http://${string}` | `https://${string}`;
Expand Down
Loading