diff --git a/packages/fresh/src/runtime/client/partials.ts b/packages/fresh/src/runtime/client/partials.ts
index ab7cbb35bb4..008e23a9592 100644
--- a/packages/fresh/src/runtime/client/partials.ts
+++ b/packages/fresh/src/runtime/client/partials.ts
@@ -83,7 +83,7 @@ if (!history.state) {
history.replaceState(state, document.title);
}
-function maybeUpdateHistory(nextUrl: URL) {
+function maybePushHistory(nextUrl: URL) {
// Only add history entry when URL is new. Still apply
// the partials because sometimes users click a link to
// "refresh" the current page.
@@ -106,6 +106,12 @@ function maybeUpdateHistory(nextUrl: URL) {
}
}
+function maybeReplaceHistory(nextUrl: URL) {
+ if (nextUrl.href !== globalThis.location.href) {
+ history.replaceState(history.state, "", nextUrl.href);
+ }
+}
+
document.addEventListener("click", async (e) => {
let el = e.target;
if (el && (el instanceof HTMLElement || el instanceof SVGElement)) {
@@ -154,7 +160,7 @@ document.addEventListener("click", async (e) => {
const nextUrl = new URL(el.href);
try {
- maybeUpdateHistory(nextUrl);
+ maybePushHistory(nextUrl);
const partialUrl = new URL(
partial ? partial : nextUrl.href,
@@ -361,6 +367,7 @@ async function fetchPartials(
actualUrl = nextUrl;
}
}
+ actualUrl.searchParams.delete(PARTIAL_SEARCH_PARAM);
try {
await applyPartials(res);
@@ -375,7 +382,7 @@ async function fetchPartials(
}
if (shouldNavigate) {
- maybeUpdateHistory(actualUrl);
+ maybeReplaceHistory(actualUrl);
}
}
diff --git a/packages/fresh/tests/partials_test.tsx b/packages/fresh/tests/partials_test.tsx
index 1ef3a78f5a8..9b4a17d510b 100644
--- a/packages/fresh/tests/partials_test.tsx
+++ b/packages/fresh/tests/partials_test.tsx
@@ -3498,3 +3498,93 @@ Deno.test({
});
},
});
+
+Deno.test({
+ name: "partials - redirect does not create back-button trap",
+ fn: async () => {
+ const app = testApp()
+ .get("/target", (ctx) => {
+ return ctx.render(
+
+
+ target page
+
+ ,
+ );
+ })
+ .get("/redirect", (ctx) => ctx.redirect("/target"))
+ .get("/", (ctx) => {
+ return ctx.render(
+
+
+ ,
+ );
+ });
+
+ await withBrowserApp(app, async (page, address) => {
+ await page.goto(address, { waitUntil: "load" });
+ await page.locator(".init").wait();
+
+ // Click link that redirects
+ await page.locator(".nav").click();
+ await page.locator(".done").wait();
+
+ // Should show the redirect target URL, not the intermediate
+ await page.waitForFunction(() => {
+ return window.location.pathname === "/target";
+ });
+
+ // Going back should return to home, not to /redirect
+ await page.evaluate(() => window.history.go(-1));
+ await page.locator(".init").wait();
+ await page.waitForFunction(() => {
+ return window.location.pathname === "/";
+ });
+ });
+ },
+});
+
+Deno.test({
+ name: "partials - redirect does not leak ?fresh-partial in URL",
+ fn: async () => {
+ const app = testApp()
+ .get("/target", (ctx) => {
+ return ctx.render(
+
+
+ target page
+
+ ,
+ );
+ })
+ .get("/redirect", (ctx) => ctx.redirect("/target"))
+ .get("/", (ctx) => {
+ return ctx.render(
+
+
+ ,
+ );
+ });
+
+ await withBrowserApp(app, async (page, address) => {
+ await page.goto(address, { waitUntil: "load" });
+ await page.locator(".init").wait();
+
+ await page.locator(".nav").click();
+ await page.locator(".done").wait();
+
+ const url = await page.evaluate(() => window.location.href);
+ expect(url).not.toContain("fresh-partial");
+ });
+ },
+});