fix: clean up svelte:head anchors on unmount (#18697)

include client-created <svelte:head> anchors in the head effect’s DOM range
remove anchors through the existing HEAD_EFFECT teardown
add client and hydration regression coverage for repeated mount/unmount cycles
Fixes #18695

---------

Co-authored-by: svelte-triage-bot <team@svelte.com>
Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
pull/18508/merge
svelte-triage-bot[bot] 13 hours ago committed by GitHub
parent 15720b16a5
commit 4b61851ec8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: remove `<svelte:head>` anchors on unmount

@ -52,6 +52,14 @@ export function head(hash, render_fn) {
block(() => {
var e = branch(() => render_fn(anchor));
e.f |= HEAD_EFFECT;
if (!hydrating) {
if (e.nodes === null) {
e.nodes = { start: anchor, end: anchor, a: null, t: null };
} else {
e.nodes.end = anchor;
}
}
});
} finally {
if (was_hydrating) {

@ -0,0 +1,3 @@
<svelte:head>
<meta name="test" content="value" />
</svelte:head>

@ -0,0 +1,19 @@
import { flushSync } from 'svelte';
import { ok, test } from '../../test';
export default test({
mode: ['client', 'hydrate'],
test({ assert, target, window }) {
const initial = window.document.head.childNodes.length;
const button = target.querySelector('button');
ok(button);
for (let i = 0; i < 3; i++) {
flushSync(() => button.click());
flushSync(() => button.click());
}
assert.equal(window.document.head.childNodes.length, initial);
}
});

@ -0,0 +1,11 @@
<script>
import Head from './Head.svelte';
let show = $state(false);
</script>
<button onclick={() => (show = !show)}>toggle</button>
{#if show}
<Head />
{/if}
Loading…
Cancel
Save