diff --git a/.changeset/tricky-garlics-mate.md b/.changeset/tricky-garlics-mate.md new file mode 100644 index 000000000000..976f7a80f424 --- /dev/null +++ b/.changeset/tricky-garlics-mate.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: support server-rendering attributes of `` blocks diff --git a/documentation/docs/10-getting-started/30-project-structure.md b/documentation/docs/10-getting-started/30-project-structure.md index 34b938c0a862..9d1630d8e898 100644 --- a/documentation/docs/10-getting-started/30-project-structure.md +++ b/documentation/docs/10-getting-started/30-project-structure.md @@ -48,6 +48,7 @@ The `src` directory contains the meat of your project. Everything except `src/ro - `%sveltekit.assets%` — either [`paths.assets`](configuration#paths), if specified, or a relative path to [`paths.base`](configuration#paths) - `%sveltekit.nonce%` — a [CSP](configuration#csp) nonce for manually included links and scripts, if used - `%sveltekit.env.[NAME]%` - this will be replaced at render time with the `[NAME]` environment variable, which must begin with the [`publicPrefix`](configuration#env) (usually `PUBLIC_`). It will fallback to `''` if not matched. + - `%svelte.htmlAttributes%` — the attributes collected from `` blocks - `error.html` is the page that is rendered when everything else fails. It can contain the following placeholders: - `%sveltekit.status%` — the HTTP status - `%sveltekit.error.message%` — the error message diff --git a/packages/kit/src/core/sync/write_server.js b/packages/kit/src/core/sync/write_server.js index 90074cfd8501..e8157d160a3d 100644 --- a/packages/kit/src/core/sync/write_server.js +++ b/packages/kit/src/core/sync/write_server.js @@ -37,6 +37,7 @@ import { set_private_env, set_public_env, set_safe_public_env } from '${runtime_ export const options = { app_dir: ${s(config.kit.appDir)}, app_template_contains_nonce: ${template.includes('%sveltekit.nonce%')}, + app_template_contains_svelte_htmlAttributes: ${template.includes('%svelte.htmlAttributes%')}, csp: ${s(config.kit.csp)}, csrf_check_origin: ${s(config.kit.csrf.checkOrigin)}, embedded: ${config.kit.embedded}, @@ -47,7 +48,8 @@ export const options = { root, service_worker: ${has_service_worker}, templates: { - app: ({ head, body, assets, nonce, env }) => ${s(template) + app: ({ html_attributes, head, body, assets, nonce, env }) => ${s(template) + .replace('%svelte.htmlAttributes%', '" + html_attributes + "') .replace('%sveltekit.head%', '" + head + "') .replace('%sveltekit.body%', '" + body + "') .replace(/%sveltekit\.assets%/g, '" + assets + "') diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index a3e9cddd8866..3975b45320c3 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -202,7 +202,17 @@ export async function render_response({ } } } else { - rendered = { head: '', html: '', css: { code: '', map: null } }; + rendered = { head: '', html: '', css: { code: '', map: null }, htmlAttributes: '' }; + } + + if ( + !options.app_template_contains_svelte_htmlAttributes && + // @ts-expect-error only exists in later versions of Svelte 5 + rendered.htmlAttributes + ) { + console.warn( + 'One or more components used `` to output attributes to the HTML tag but app.html does not contain %svelte.htmlAttributes% to render them. The attributes will be ignored.' + ); } let head = ''; @@ -476,6 +486,9 @@ export async function render_response({ head += rendered.head; const html = options.templates.app({ + // @ts-expect-error only exists in later versions of Svelte 5 + html_attributes: rendered.htmlAttributes ?? '', + head, body, assets, diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index d838add28a54..b21c8a720144 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -365,6 +365,7 @@ export type SSRNodeLoader = () => Promise; export interface SSROptions { app_dir: string; app_template_contains_nonce: boolean; + app_template_contains_svelte_htmlAttributes: boolean; csp: ValidatedConfig['kit']['csp']; csrf_check_origin: boolean; embedded: boolean; @@ -376,6 +377,7 @@ export interface SSROptions { service_worker: boolean; templates: { app(values: { + htmlAttributes: string; head: string; body: string; assets: string; diff --git a/packages/kit/test/apps/basics/src/app.html b/packages/kit/test/apps/basics/src/app.html index c1a247d71ecd..09509b29d23a 100644 --- a/packages/kit/test/apps/basics/src/app.html +++ b/packages/kit/test/apps/basics/src/app.html @@ -1,5 +1,5 @@ - + diff --git a/packages/kit/test/apps/basics/src/routes/+layout.svelte b/packages/kit/test/apps/basics/src/routes/+layout.svelte index 6272671c81ae..c63814e21c7f 100644 --- a/packages/kit/test/apps/basics/src/routes/+layout.svelte +++ b/packages/kit/test/apps/basics/src/routes/+layout.svelte @@ -7,6 +7,8 @@ setup(); + +
{data.foo.bar}
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index c5867f34e00c..5254cc14aa1b 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -1532,3 +1532,10 @@ test.describe('Serialization', () => { expect(await page.textContent('h1')).toBe('It works!'); }); }); + +test.describe('svelte:html', () => { + test('server-renders correctly', async ({ page }) => { + await page.goto('/'); + expect(page.locator('html')).toHaveAttribute('lang', 'en'); + }); +}); diff --git a/playgrounds/basic/src/app.html b/playgrounds/basic/src/app.html index 77a5ff52c923..f7e2cf5e9703 100644 --- a/playgrounds/basic/src/app.html +++ b/playgrounds/basic/src/app.html @@ -1,5 +1,5 @@ - +