diff --git a/.changeset/proud-queens-sniff.md b/.changeset/proud-queens-sniff.md new file mode 100644 index 000000000000..e2970fd4bc41 --- /dev/null +++ b/.changeset/proud-queens-sniff.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: improve hydration of svelte head blocks diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js index 70606cd32fd4..b00a3a242b1e 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js @@ -1,7 +1,16 @@ import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrate_nodes } from '../hydration.js'; import { empty } from '../operations.js'; import { block } from '../../reactivity/effects.js'; -import { HYDRATION_START } from '../../../../constants.js'; +import { HYDRATION_END, HYDRATION_START } from '../../../../constants.js'; + +/** + * @type {Node | undefined} + */ +let head_anchor; + +export function reset_head_anchor() { + head_anchor = undefined; +} /** * @param {(anchor: Node) => import('#client').Dom | void} render_fn @@ -19,12 +28,20 @@ export function head(render_fn) { if (hydrating) { previous_hydrate_nodes = hydrate_nodes; - let anchor = /** @type {import('#client').TemplateNode} */ (document.head.firstChild); - while (anchor.nodeType !== 8 || /** @type {Comment} */ (anchor).data !== HYDRATION_START) { - anchor = /** @type {import('#client').TemplateNode} */ (anchor.nextSibling); + // There might be multiple head blocks in our app, so we need to account for each one needing independent hydration. + if (head_anchor === undefined) { + head_anchor = /** @type {import('#client').TemplateNode} */ (document.head.firstChild); + } + + while ( + head_anchor.nodeType !== 8 || + /** @type {Comment} */ (head_anchor).data !== HYDRATION_START + ) { + head_anchor = /** @type {import('#client').TemplateNode} */ (head_anchor.nextSibling); } - anchor = /** @type {import('#client').TemplateNode} */ (hydrate_anchor(anchor)); + head_anchor = /** @type {import('#client').TemplateNode} */ (hydrate_anchor(head_anchor)); + head_anchor = /** @type {import('#client').TemplateNode} */ (head_anchor.nextSibling); } else { anchor = document.head.appendChild(empty()); } diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 421e30caad4e..833070925ed0 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -18,6 +18,7 @@ import { } from './dom/hydration.js'; import { array_from } from './utils.js'; import { handle_event_propagation } from './dom/elements/events.js'; +import { reset_head_anchor } from './dom/blocks/svelte-head.js'; /** @type {Set} */ export const all_registered_events = new Set(); @@ -175,6 +176,7 @@ export function hydrate(component, options) { } finally { set_hydrating(!!previous_hydrate_nodes); set_hydrate_nodes(previous_hydrate_nodes); + reset_head_anchor(); } } diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index b6d10cfca8cd..6a5e8aa1770c 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -207,10 +207,7 @@ export function render(component, options) { on_destroy = prev_on_destroy; return { - head: - payload.head.out || payload.head.title - ? payload.head.title + BLOCK_OPEN + payload.head.out + BLOCK_CLOSE - : '', + head: payload.head.out || payload.head.title ? payload.head.out + payload.head.title : '', html: payload.out }; } @@ -247,7 +244,9 @@ export function escape(value, is_attr = false) { */ export function head(payload, fn) { const head_payload = payload.head; + payload.head.out += BLOCK_OPEN; fn(head_payload); + payload.head.out += BLOCK_CLOSE; } /** diff --git a/packages/svelte/tests/runtime-runes/samples/multiple-head/MetaTag.svelte b/packages/svelte/tests/runtime-runes/samples/multiple-head/MetaTag.svelte new file mode 100644 index 000000000000..b19d8a77db25 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/multiple-head/MetaTag.svelte @@ -0,0 +1,9 @@ + + + {title} + + + diff --git a/packages/svelte/tests/runtime-runes/samples/multiple-head/_config.js b/packages/svelte/tests/runtime-runes/samples/multiple-head/_config.js new file mode 100644 index 000000000000..5c9395a73c49 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/multiple-head/_config.js @@ -0,0 +1,12 @@ +import { test } from '../../test'; + +export default test({ + html: `
Hello
`, + + async test({ assert, target }) { + assert.htmlEqual( + target.ownerDocument.head.innerHTML, + `Hello world` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/multiple-head/main.svelte b/packages/svelte/tests/runtime-runes/samples/multiple-head/main.svelte new file mode 100644 index 000000000000..c5dcbb912991 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/multiple-head/main.svelte @@ -0,0 +1,9 @@ + + + + + + +
Hello
diff --git a/packages/svelte/tests/server-side-rendering/samples/head-meta-hydrate-duplicate/_expected-head.html b/packages/svelte/tests/server-side-rendering/samples/head-meta-hydrate-duplicate/_expected-head.html index 0e12a4a9de64..c20e3600ff0f 100644 --- a/packages/svelte/tests/server-side-rendering/samples/head-meta-hydrate-duplicate/_expected-head.html +++ b/packages/svelte/tests/server-side-rendering/samples/head-meta-hydrate-duplicate/_expected-head.html @@ -1,6 +1,6 @@ -Some Title +Some Title