diff --git a/.changeset/neat-boxes-chew.md b/.changeset/neat-boxes-chew.md
new file mode 100644
index 0000000000..62e92ed0d6
--- /dev/null
+++ b/.changeset/neat-boxes-chew.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: bail-out of hydrating head if no anchor is found
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 0fc2efa1eb..6f39a63eff 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
@@ -1,5 +1,5 @@
/** @import { TemplateNode } from '#client' */
-import { hydrate_node, hydrating, set_hydrate_node } from '../hydration.js';
+import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hydration.js';
import { empty } from '../operations.js';
import { block } from '../../reactivity/effects.js';
import { HEAD_EFFECT } from '../../constants.js';
@@ -36,14 +36,22 @@ export function head(render_fn) {
}
while (
- head_anchor.nodeType !== 8 ||
- /** @type {Comment} */ (head_anchor).data !== HYDRATION_START
+ head_anchor !== null &&
+ (head_anchor.nodeType !== 8 || /** @type {Comment} */ (head_anchor).data !== HYDRATION_START)
) {
head_anchor = /** @type {TemplateNode} */ (head_anchor.nextSibling);
}
- head_anchor = set_hydrate_node(/** @type {TemplateNode} */ (head_anchor.nextSibling));
- } else {
+ // If we can't find an opening hydration marker, skip hydration (this can happen
+ // if a framework rendered body but not head content)
+ if (head_anchor === null) {
+ set_hydrating(false);
+ } else {
+ head_anchor = set_hydrate_node(/** @type {TemplateNode} */ (head_anchor.nextSibling));
+ }
+ }
+
+ if (!hydrating) {
anchor = document.head.appendChild(empty());
}
@@ -51,6 +59,7 @@ export function head(render_fn) {
block(() => render_fn(anchor), HEAD_EFFECT);
} finally {
if (was_hydrating) {
+ set_hydrating(true);
head_anchor = hydrate_node; // so that next head block starts from the correct node
set_hydrate_node(/** @type {TemplateNode} */ (previous_hydrate_node));
}
diff --git a/packages/svelte/tests/hydration/samples/head-missing/_config.js b/packages/svelte/tests/hydration/samples/head-missing/_config.js
new file mode 100644
index 0000000000..fb03a77adb
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/head-missing/_config.js
@@ -0,0 +1,7 @@
+import { test } from '../../test';
+
+export default test({
+ test(assert, target, snapshot, component, window) {
+ assert.equal(window.document.querySelectorAll('meta').length, 2);
+ }
+});
diff --git a/packages/svelte/tests/hydration/samples/head-missing/_expected_head.html b/packages/svelte/tests/hydration/samples/head-missing/_expected_head.html
new file mode 100644
index 0000000000..ae8f27c2d2
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/head-missing/_expected_head.html
@@ -0,0 +1 @@
+
diff --git a/packages/svelte/tests/hydration/samples/head-missing/_override_head.html b/packages/svelte/tests/hydration/samples/head-missing/_override_head.html
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/svelte/tests/hydration/samples/head-missing/main.svelte b/packages/svelte/tests/hydration/samples/head-missing/main.svelte
new file mode 100644
index 0000000000..fee6ed54a3
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/head-missing/main.svelte
@@ -0,0 +1,6 @@
+