diff --git a/.changeset/angry-pigs-float.md b/.changeset/angry-pigs-float.md
new file mode 100644
index 000000000000..59f186f4966e
--- /dev/null
+++ b/.changeset/angry-pigs-float.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: improve hydration of altered html
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 66d337183637..b7ed6d4deb8a 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
@@ -3,10 +3,11 @@ import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hyd
import { create_text, get_first_child, get_next_sibling } from '../operations.js';
import { block } from '../../reactivity/effects.js';
import { COMMENT_NODE, HEAD_EFFECT } from '#client/constants';
-import { HYDRATION_START } from '../../../../constants.js';
+import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../../../constants.js';
+import { hydration_mismatch } from '../../warnings.js';
/**
- * @type {Node | undefined}
+ * @type {Node | null | undefined}
*/
let head_anchor;
@@ -32,7 +33,7 @@ export function head(render_fn) {
// 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 {TemplateNode} */ (get_first_child(document.head));
+ head_anchor = get_first_child(document.head);
}
while (
@@ -40,7 +41,7 @@ export function head(render_fn) {
(head_anchor.nodeType !== COMMENT_NODE ||
/** @type {Comment} */ (head_anchor).data !== HYDRATION_START)
) {
- head_anchor = /** @type {TemplateNode} */ (get_next_sibling(head_anchor));
+ head_anchor = get_next_sibling(head_anchor);
}
// If we can't find an opening hydration marker, skip hydration (this can happen
@@ -58,6 +59,43 @@ export function head(render_fn) {
try {
block(() => render_fn(anchor), HEAD_EFFECT);
+ check_end();
+ } catch (error) {
+ // Remount only this svelte:head
+ if (was_hydrating && head_anchor != null) {
+ hydration_mismatch();
+ // Here head_anchor is the node next after HYDRATION_START
+ /** @type {Node | null} */
+ var node = head_anchor.previousSibling;
+ // Remove nodes that failed to hydrate
+ var depth = 0;
+ while (node !== null) {
+ var prev = /** @type {TemplateNode} */ (node);
+ node = get_next_sibling(node);
+ prev.remove();
+ if (prev.nodeType === COMMENT_NODE) {
+ var data = /** @type {Comment} */ (prev).data;
+ if (data === HYDRATION_END) {
+ depth -= 1;
+ if (depth === 0) break;
+ } else if (data === HYDRATION_START) {
+ depth += 1;
+ }
+ }
+ }
+ // Setup hydration for the next svelte:head
+ if (node === null) {
+ head_anchor = null;
+ } else {
+ head_anchor = set_hydrate_node(/** @type {TemplateNode} */ (node));
+ }
+
+ set_hydrating(false);
+ anchor = document.head.appendChild(create_text());
+ block(() => render_fn(anchor), HEAD_EFFECT);
+ } else {
+ throw error;
+ }
} finally {
if (was_hydrating) {
set_hydrating(true);
@@ -66,3 +104,11 @@ export function head(render_fn) {
}
}
}
+
+// treeshaking of hydrate node fails when this is directly in the try-catch
+function check_end() {
+ if (hydrating && /** @type {Comment|null} */ (hydrate_node)?.data !== HYDRATION_END) {
+ hydration_mismatch();
+ throw HYDRATION_ERROR;
+ }
+}
diff --git a/packages/svelte/tests/hydration/samples/head-corrupted-2/_config.js b/packages/svelte/tests/hydration/samples/head-corrupted-2/_config.js
new file mode 100644
index 000000000000..91470f805211
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/head-corrupted-2/_config.js
@@ -0,0 +1,18 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ expect_hydration_error: true,
+ test(assert, target, snapshot, component, window) {
+ assert.equal(window.document.querySelectorAll('meta').length, 5);
+
+ const [button] = target.getElementsByTagName('button');
+ button.click();
+ flushSync();
+
+ /** @type {NodeList} */
+ const metas = window.document.querySelectorAll('meta[name=count]');
+ assert.equal(metas.length, 4);
+ metas.forEach((meta) => assert.equal(/** @type {HTMLMetaElement} */ (meta).content, '2'));
+ }
+});
diff --git a/packages/svelte/tests/hydration/samples/head-corrupted-2/_expected_head.html b/packages/svelte/tests/hydration/samples/head-corrupted-2/_expected_head.html
new file mode 100644
index 000000000000..3ce4f3237b46
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/head-corrupted-2/_expected_head.html
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/packages/svelte/tests/hydration/samples/head-corrupted-2/_override_head.html b/packages/svelte/tests/hydration/samples/head-corrupted-2/_override_head.html
new file mode 100644
index 000000000000..f7404d045e06
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/head-corrupted-2/_override_head.html
@@ -0,0 +1,5 @@
+
+
diff --git a/packages/svelte/tests/hydration/samples/head-corrupted-2/head.svelte b/packages/svelte/tests/hydration/samples/head-corrupted-2/head.svelte
new file mode 100644
index 000000000000..07f1acef653f
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/head-corrupted-2/head.svelte
@@ -0,0 +1,6 @@
+
+