diff --git a/.changeset/flat-planes-kneel.md b/.changeset/flat-planes-kneel.md
new file mode 100644
index 000000000000..fad7d235a6c1
--- /dev/null
+++ b/.changeset/flat-planes-kneel.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+fix: correctly replace state when `data-sveltekit-replacestate` is used with a hash link
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index 0e05ded0a3a0..0ec95f1fee96 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -1532,7 +1532,6 @@ export function create_client(app, target) {
if (hash !== undefined && nonhash === location.href.split('#')[0]) {
// set this flag to distinguish between navigations triggered by
// clicking a hash link and those triggered by popstate
- // TODO why not update history here directly?
hash_navigating = true;
update_scroll_positions(current_history_index);
@@ -1541,7 +1540,11 @@ export function create_client(app, target) {
stores.page.set({ ...page, url });
stores.page.notify();
- return;
+ if (!options.replace_state) return;
+
+ // hashchange event shouldn't occur if the router is replacing state.
+ hash_navigating = false;
+ event.preventDefault();
}
navigate({
diff --git a/packages/kit/test/apps/basics/src/routes/routing/hashes/a/+page.svelte b/packages/kit/test/apps/basics/src/routes/routing/hashes/a/+page.svelte
index 0f5acc8bc10d..19192d1e957b 100644
--- a/packages/kit/test/apps/basics/src/routes/routing/hashes/a/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/routing/hashes/a/+page.svelte
@@ -1,3 +1,4 @@
a
hash link
b
+replace state
\ No newline at end of file
diff --git a/packages/kit/test/apps/basics/test/cross-platform/client.test.js b/packages/kit/test/apps/basics/test/cross-platform/client.test.js
index 42693f8f30d4..77e8d2a4877c 100644
--- a/packages/kit/test/apps/basics/test/cross-platform/client.test.js
+++ b/packages/kit/test/apps/basics/test/cross-platform/client.test.js
@@ -623,6 +623,33 @@ test.describe('Routing', () => {
await expect(page.locator('#page-url-hash')).toHaveText('');
});
+ test('back button returns to previous route when previous route has been navigated to via hash anchor', async ({
+ page,
+ clicknav
+ }) => {
+ await page.goto('/routing/hashes/a');
+
+ await page.locator('[href="#hash-target"]').click();
+ await clicknav('[href="/routing/hashes/b"]');
+
+ await page.goBack();
+ expect(await page.textContent('h1')).toBe('a');
+ });
+
+ test('replaces state if the data-sveltekit-replacestate router option is specified for the hash link', async ({
+ page,
+ clicknav,
+ baseURL
+ }) => {
+ await page.goto('/routing/hashes/a');
+
+ await clicknav('[href="#hash-target"]');
+ await clicknav('[href="#replace-state"]');
+
+ await page.goBack();
+ expect(await page.url()).toBe(`${baseURL}/routing/hashes/a`);
+ });
+
test('does not normalize external path', async ({ page, start_server }) => {
const html_ok = 'ok';
const { port } = await start_server((_req, res) => {
diff --git a/packages/kit/test/apps/basics/test/cross-platform/test.js b/packages/kit/test/apps/basics/test/cross-platform/test.js
index 38bfdb02c7aa..3d69b9f221e5 100644
--- a/packages/kit/test/apps/basics/test/cross-platform/test.js
+++ b/packages/kit/test/apps/basics/test/cross-platform/test.js
@@ -743,19 +743,6 @@ test.describe('Routing', () => {
expect(await page.textContent('h1')).toBe('Great success!');
});
- test('back button returns to previous route when previous route has been navigated to via hash anchor', async ({
- page,
- clicknav
- }) => {
- await page.goto('/routing/hashes/a');
-
- await page.locator('[href="#hash-target"]').click();
- await clicknav('[href="/routing/hashes/b"]');
-
- await page.goBack();
- expect(await page.textContent('h1')).toBe('a');
- });
-
test('focus works if page load has hash', async ({ page, browserName }) => {
await page.goto('/routing/hashes/target#p2');