diff --git a/.changeset/pushstate-page-url-key.md b/.changeset/pushstate-page-url-key.md
new file mode 100644
index 000000000000..e7f031cb9af7
--- /dev/null
+++ b/.changeset/pushstate-page-url-key.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+fix: load the URL shown in the address bar when returning to a `pushState`/`replaceState` history entry
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index c02e1274509d..9082afe6ab50 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -2559,6 +2559,15 @@ export async function preloadCode(pathname) {
return _preload_code(url);
}
+/**
+ * Resolves a shallow routing `url` argument, where `''` means the current URL.
+ * `new URL('', ...)` drops the fragment, which would lose the hash route or anchor.
+ * @param {string | URL} url
+ */
+function resolve_shallow_url(url) {
+ return url === '' ? new URL(location.href) : resolve_url(url);
+}
+
/**
* Programmatically create a new history entry with the given `page.state`. To use the current URL, you can pass `''` as the first argument. Used for [shallow routing](https://svelte.dev/docs/kit/shallow-routing).
*
@@ -2587,14 +2596,17 @@ export function pushState(url, state) {
update_scroll_positions(current_history_index);
+ const resolved = resolve_shallow_url(url);
+
const opts = {
[HISTORY_INDEX]: (current_history_index += 1),
[NAVIGATION_INDEX]: current_navigation_index,
- [PAGE_URL_KEY]: page.url.href,
+ // store the address bar URL, so returning to this entry loads what the user saw
+ [PAGE_URL_KEY]: resolved.href,
[STATES_KEY]: state
};
- history.pushState(opts, '', resolve_url(url));
+ history.pushState(opts, '', resolved);
has_navigated = true;
page.state = state;
@@ -2628,14 +2640,16 @@ export function replaceState(url, state) {
}
}
+ const resolved = resolve_shallow_url(url);
+
const opts = {
[HISTORY_INDEX]: current_history_index,
[NAVIGATION_INDEX]: current_navigation_index,
- [PAGE_URL_KEY]: page.url.href,
+ [PAGE_URL_KEY]: resolved.href,
[STATES_KEY]: state
};
- history.replaceState(opts, '', resolve_url(url));
+ history.replaceState(opts, '', resolved);
page.state = state;
}
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js
index 143244dcba84..1bb16aa3fc00 100644
--- a/packages/kit/test/apps/basics/test/client.test.js
+++ b/packages/kit/test/apps/basics/test/client.test.js
@@ -1709,6 +1709,24 @@ test.describe('Shallow routing', () => {
await expect(page.locator('p')).toHaveText('active: true');
});
+ test('Loads the pushed URL when returning from a subsequent navigation', async ({
+ baseURL,
+ page,
+ clicknav
+ }) => {
+ await page.goto('/shallow-routing/push-state');
+ await page.locator('[data-id="two"]').click();
+ expect(page.url()).toBe(`${baseURL}/shallow-routing/push-state/a`);
+ await expect(page.locator('h1')).toHaveText('parent');
+
+ await clicknav('a[href="/shallow-routing/push-state/b"]');
+ await page.goBack();
+
+ expect(page.url()).toBe(`${baseURL}/shallow-routing/push-state/a`);
+ await expect(page.locator('h1')).toHaveText('a');
+ await expect(page.locator('p')).toHaveText('active: true');
+ });
+
test('Replaces state on the current URL', async ({ baseURL, page, clicknav }) => {
await page.goto('/shallow-routing/replace-state/b');
await clicknav('[href="/shallow-routing/replace-state"]');
@@ -1739,7 +1757,7 @@ test.describe('Shallow routing', () => {
await page.goForward();
expect(page.url()).toBe(`${baseURL}/shallow-routing/replace-state/a`);
- await expect(page.locator('h1')).toHaveText('parent');
+ await expect(page.locator('h1')).toHaveText('a');
await expect(page.locator('p')).toHaveText('active: true');
});
diff --git a/packages/kit/test/apps/hash-based-routing/src/routes/+layout.svelte b/packages/kit/test/apps/hash-based-routing/src/routes/+layout.svelte
index 76cae24dfaf8..00941c588f7a 100644
--- a/packages/kit/test/apps/hash-based-routing/src/routes/+layout.svelte
+++ b/packages/kit/test/apps/hash-based-routing/src/routes/+layout.svelte
@@ -17,6 +17,7 @@
/#/reroute-b
+
{@render children()}
diff --git a/packages/kit/test/apps/hash-based-routing/test/test.js b/packages/kit/test/apps/hash-based-routing/test/test.js
index 6801d80444d0..7d17a1b472c1 100644
--- a/packages/kit/test/apps/hash-based-routing/test/test.js
+++ b/packages/kit/test/apps/hash-based-routing/test/test.js
@@ -50,6 +50,23 @@ test.describe('hash based navigation', () => {
expect(url.hash).toBe('#/a#b');
});
+ test('pushState with empty url keeps the hash route', async ({ page }) => {
+ await page.goto('/#/a');
+ await expect(page.locator('p')).toHaveText('a');
+
+ await page.locator('button[data-push-current]').click();
+ let url = new URL(page.url());
+ expect(url.hash).toBe('#/a');
+
+ await page.locator('a[href="/#/b"]').click();
+ await expect(page.locator('p')).toHaveText('b');
+
+ await page.goBack();
+ await expect(page.locator('p')).toHaveText('a');
+ url = new URL(page.url());
+ expect(url.hash).toBe('#/a');
+ });
+
test('navigates to correct page on load', async ({ page }) => {
await page.goto('/#/a');
await expect(page.locator('p')).toHaveText('a');