diff --git a/.changeset/fluffy-eggs-do.md b/.changeset/fluffy-eggs-do.md
new file mode 100644
index 0000000000..b36927376a
--- /dev/null
+++ b/.changeset/fluffy-eggs-do.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: throw runtime error when template returns different html
diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md
index 0beb3cb9a9..6f05d339d1 100644
--- a/documentation/docs/98-reference/.generated/client-errors.md
+++ b/documentation/docs/98-reference/.generated/client-errors.md
@@ -80,6 +80,12 @@ Maximum update depth exceeded. This can happen when a reactive block or effect r
Failed to hydrate the application
```
+### invalid_html_structure
+
+```
+This html structure `%html_input%` would be corrected like this `%html_output%` by the browser making this component impossible to hydrate properly
+```
+
### invalid_snippet
```
diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md
index ab4d1519c1..d4d3d7a8b9 100644
--- a/packages/svelte/messages/client-errors/errors.md
+++ b/packages/svelte/messages/client-errors/errors.md
@@ -52,6 +52,10 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
> Failed to hydrate the application
+## invalid_html_structure
+
+> This html structure `%html_input%` would be corrected like this `%html_output%` by the browser making this component impossible to hydrate properly
+
## invalid_snippet
> Could not `{@render}` snippet due to the expression being `null` or `undefined`. Consider using optional chaining `{@render snippet?.()}`
diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js
index b3fc5a9c72..c002ea608d 100644
--- a/packages/svelte/src/internal/client/dom/blocks/html.js
+++ b/packages/svelte/src/internal/client/dom/blocks/html.js
@@ -99,7 +99,7 @@ export function html(node, get_value, svg, mathml, skip_warning) {
// Don't use create_fragment_with_script_from_html here because that would mean script tags are executed.
// @html is basically `.innerHTML = ...` and that doesn't execute scripts either due to security reasons.
/** @type {DocumentFragment | Element} */
- var node = create_fragment_from_html(html);
+ var node = create_fragment_from_html(html, false);
if (svg || mathml) {
node = /** @type {Element} */ (get_first_child(node));
diff --git a/packages/svelte/src/internal/client/dom/reconciler.js b/packages/svelte/src/internal/client/dom/reconciler.js
index 9897e08d53..b0e3a20481 100644
--- a/packages/svelte/src/internal/client/dom/reconciler.js
+++ b/packages/svelte/src/internal/client/dom/reconciler.js
@@ -1,6 +1,29 @@
-/** @param {string} html */
-export function create_fragment_from_html(html) {
+import { DEV } from 'esm-env';
+import * as e from '../errors.js';
+
+/**
+ * @param {string} html
+ * @param {boolean} [check_structure]
+ */
+export function create_fragment_from_html(html, check_structure = true) {
var elem = document.createElement('template');
elem.innerHTML = html;
+ if (DEV && check_structure) {
+ let replace_comments = html.replaceAll('', '');
+ let remove_attributes_and_text_input = replace_comments
+ // we remove every attribute since the template automatically adds ="" after boolean attributes
+ .replace(/<([a-z0-9]+)(\s+[^>]+?)?>/g, '<$1>')
+ // we remove the text within the elements because the template change & to & (and similar)
+ .replace(/>([^<>]*)/g, '>');
+ let remove_attributes_and_text_output = elem.innerHTML
+ // we remove every attribute since the template automatically adds ="" after boolean attributes
+ .replace(/<([a-z0-9]+)(\s+[^>]+?)?>/g, '<$1>')
+ // we remove the text within the elements because the template change & to & (and similar)
+ .replace(/>([^<>]*)/g, '>');
+ if (remove_attributes_and_text_input !== remove_attributes_and_text_output) {
+ e.invalid_html_structure(remove_attributes_and_text_input, remove_attributes_and_text_output);
+ }
+ }
+
return elem.content;
}
diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js
index 682816e1d6..2cbef310cd 100644
--- a/packages/svelte/src/internal/client/errors.js
+++ b/packages/svelte/src/internal/client/errors.js
@@ -198,6 +198,23 @@ export function hydration_failed() {
}
}
+/**
+ * This html structure `%html_input%` would be corrected like this `%html_output%` by the browser making this component impossible to hydrate properly
+ * @param {string} html_input
+ * @param {string} html_output
+ * @returns {never}
+ */
+export function invalid_html_structure(html_input, html_output) {
+ if (DEV) {
+ const error = new Error(`invalid_html_structure\nThis html structure \`${html_input}\` would be corrected like this \`${html_output}\` by the browser making this component impossible to hydrate properly\nhttps://svelte.dev/e/invalid_html_structure`);
+
+ error.name = 'Svelte error';
+ throw error;
+ } else {
+ throw new Error(`https://svelte.dev/e/invalid_html_structure`);
+ }
+}
+
/**
* Could not `{@render}` snippet due to the expression being `null` or `undefined`. Consider using optional chaining `{@render snippet?.()}`
* @returns {never}
diff --git a/packages/svelte/tests/runtime-runes/samples/invalid-html-structure/_config.js b/packages/svelte/tests/runtime-runes/samples/invalid-html-structure/_config.js
new file mode 100644
index 0000000000..fd6f4c03c4
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/invalid-html-structure/_config.js
@@ -0,0 +1,7 @@
+import { test } from '../../test';
+
+export default test({
+ mode: ['client', 'hydrate'],
+ recover: true,
+ runtime_error: 'invalid_html_structure'
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/invalid-html-structure/main.svelte b/packages/svelte/tests/runtime-runes/samples/invalid-html-structure/main.svelte
new file mode 100644
index 0000000000..ce131ae22a
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/invalid-html-structure/main.svelte
@@ -0,0 +1,3 @@
+