diff --git a/.changeset/few-badgers-guess.md b/.changeset/few-badgers-guess.md
new file mode 100644
index 0000000000..b721433c96
--- /dev/null
+++ b/.changeset/few-badgers-guess.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+feat: warn in dev on `{@html ...}` block hydration mismatch
diff --git a/packages/svelte/messages/client-warnings/warnings.md b/packages/svelte/messages/client-warnings/warnings.md
index 73dba661b3..2543a2065a 100644
--- a/packages/svelte/messages/client-warnings/warnings.md
+++ b/packages/svelte/messages/client-warnings/warnings.md
@@ -2,6 +2,12 @@
> The `%attribute%` attribute on `%html%` changed its value between server and client renders. The client value, `%value%`, will be ignored in favour of the server value
+## hydration_html_changed
+
+> The value of an `{@html ...}` block changed between server and client renders. The client value will be ignored in favour of the server value
+
+> The value of an `{@html ...}` block %location% changed between server and client renders. The client value will be ignored in favour of the server value
+
## hydration_mismatch
> Hydration failed because the initial UI does not match what was rendered on the server
diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js
index 5a44c30d17..d48fa34561 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/index.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/index.js
@@ -27,7 +27,7 @@ import {
import { should_proxy_or_freeze } from '../3-transform/client/utils.js';
import { analyze_css } from './css/css-analyze.js';
import { prune } from './css/css-prune.js';
-import { hash } from './utils.js';
+import { hash } from '../../../utils.js';
import { warn_unused } from './css/css-warn.js';
import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js';
import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js';
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
index 0e5c584591..687984b395 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
@@ -1169,7 +1169,7 @@ const template_visitors = {
},
HtmlTag(node, context) {
const expression = /** @type {import('estree').Expression} */ (context.visit(node.expression));
- context.state.template.push(empty_comment, expression, empty_comment);
+ context.state.template.push(b.call('$.html', expression));
},
ConstTag(node, { state, visit }) {
const declaration = node.declaration.declarations[0];
diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js
index 4d0fc38e67..f0a4d2bc79 100644
--- a/packages/svelte/src/internal/client/dom/blocks/html.js
+++ b/packages/svelte/src/internal/client/dom/blocks/html.js
@@ -5,6 +5,32 @@ import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from '../hydr
import { create_fragment_from_html } from '../reconciler.js';
import { assign_nodes } from '../template.js';
import * as w from '../../warnings.js';
+import { hash } from '../../../../utils.js';
+import { DEV } from 'esm-env';
+import { dev_current_component_function } from '../../runtime.js';
+
+/**
+ * @param {Element} element
+ * @param {string | null} server_hash
+ * @param {string} value
+ */
+function check_hash(element, server_hash, value) {
+ if (!server_hash || server_hash === hash(String(value ?? ''))) return;
+
+ let location;
+
+ // @ts-expect-error
+ const loc = element.__svelte_meta?.loc;
+ if (loc) {
+ location = `near ${loc.file}:${loc.line}:${loc.column}`;
+ } else if (dev_current_component_function.filename) {
+ location = `in ${dev_current_component_function.filename}`;
+ }
+
+ w.hydration_html_changed(
+ location?.replace(/\//g, '/\u200b') // prevent devtools trying to make it a clickable link by inserting a zero-width space
+ );
+}
/**
* @param {Element | Text | Comment} node
@@ -33,6 +59,7 @@ export function html(node, get_value, svg, mathml) {
effect = branch(() => {
if (hydrating) {
+ var hash = /** @type {Comment} */ (hydrate_node).data;
var next = hydrate_next();
var last = next;
@@ -49,6 +76,10 @@ export function html(node, get_value, svg, mathml) {
throw HYDRATION_ERROR;
}
+ if (DEV) {
+ check_hash(/** @type {Element} */ (next.parentNode), hash, value);
+ }
+
assign_nodes(hydrate_node, last);
anchor = set_hydrate_node(next);
return;
diff --git a/packages/svelte/src/internal/client/warnings.js b/packages/svelte/src/internal/client/warnings.js
index 576453e525..e339cf1a8c 100644
--- a/packages/svelte/src/internal/client/warnings.js
+++ b/packages/svelte/src/internal/client/warnings.js
@@ -20,6 +20,19 @@ export function hydration_attribute_changed(attribute, html, value) {
}
}
+/**
+ * The value of an `{@html ...}` block %location% changed between server and client renders. The client value will be ignored in favour of the server value
+ * @param {string | undefined | null} [location]
+ */
+export function hydration_html_changed(location) {
+ if (DEV) {
+ console.warn(`%c[svelte] hydration_html_changed\n%c${location ? `The value of an \`{@html ...}\` block ${location} changed between server and client renders. The client value will be ignored in favour of the server value` : "The value of an `{@html ...}` block changed between server and client renders. The client value will be ignored in favour of the server value"}`, bold, normal);
+ } else {
+ // TODO print a link to the documentation
+ console.warn("hydration_html_changed");
+ }
+}
+
/**
* Hydration failed because the initial UI does not match what was rendered on the server. The error occurred near %location%
* @param {string | undefined | null} [location]
diff --git a/packages/svelte/src/internal/server/blocks/html.js b/packages/svelte/src/internal/server/blocks/html.js
new file mode 100644
index 0000000000..09efbc78fe
--- /dev/null
+++ b/packages/svelte/src/internal/server/blocks/html.js
@@ -0,0 +1,10 @@
+import { DEV } from 'esm-env';
+import { hash } from '../../../utils.js';
+
+/**
+ * @param {string} value
+ */
+export function html(value) {
+ var open = DEV ? `` : '';
+ return `${open}${value}`;
+}
diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js
index c52b6e4151..841794e544 100644
--- a/packages/svelte/src/internal/server/index.js
+++ b/packages/svelte/src/internal/server/index.js
@@ -545,6 +545,8 @@ export function once(get_value) {
};
}
+export { html } from './blocks/html.js';
+
export { push, pop } from './context.js';
export { push_element, pop_element } from './dev.js';
diff --git a/packages/svelte/src/compiler/phases/2-analyze/utils.js b/packages/svelte/src/utils.js
similarity index 100%
rename from packages/svelte/src/compiler/phases/2-analyze/utils.js
rename to packages/svelte/src/utils.js
diff --git a/packages/svelte/tests/hydration/samples/html-tag-hydration-2/_config.js b/packages/svelte/tests/hydration/samples/html-tag-hydration-2/_config.js
new file mode 100644
index 0000000000..7713578002
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/html-tag-hydration-2/_config.js
@@ -0,0 +1,19 @@
+import { test } from '../../test';
+
+export default test({
+ server_props: {
+ browser: false
+ },
+
+ props: {
+ browser: true
+ },
+
+ compileOptions: {
+ dev: true
+ },
+
+ errors: [
+ 'The value of an `{@html ...}` block in packages/svelte/tests/hydration/samples/html-tag-hydration-2/main.svelte changed between server and client renders. The client value will be ignored in favour of the server value'
+ ]
+});
diff --git a/packages/svelte/tests/hydration/samples/html-tag-hydration-2/main.svelte b/packages/svelte/tests/hydration/samples/html-tag-hydration-2/main.svelte
new file mode 100644
index 0000000000..6794452c67
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/html-tag-hydration-2/main.svelte
@@ -0,0 +1,5 @@
+
+
+{@html browser ? 'browser' : 'server'}
diff --git a/packages/svelte/tests/runtime-legacy/samples/raw-anchor-first-child/_config.js b/packages/svelte/tests/runtime-legacy/samples/raw-anchor-first-child/_config.js
index 0f47f3c114..76b2446812 100644
--- a/packages/svelte/tests/runtime-legacy/samples/raw-anchor-first-child/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/raw-anchor-first-child/_config.js
@@ -11,7 +11,7 @@ export default test({
if (variant === 'dom') {
assert.ok(!span.previousSibling);
} else {
- assert.ok(span.previousSibling?.textContent === ''); // ssr commment node
+ assert.equal(span.previousSibling?.textContent, '1tbe2lq'); // hash of the value
}
component.raw = 'bar';