From 9333602668e41191b2ecf58cdbda7e6f0392c7f7 Mon Sep 17 00:00:00 2001 From: eti Date: Mon, 21 Sep 2026 17:03:59 +0200 Subject: [PATCH] web/[handle]: keep the path suffix when redirecting to the canonical handle Signed-off-by: eti --- web/src/routes/[handle]/+layout.svelte | 6 ++++- web/src/routes/[handle]/+layout.ts | 3 ++- web/src/routes/[handle]/handle.test.ts | 31 ++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/web/src/routes/[handle]/+layout.svelte b/web/src/routes/[handle]/+layout.svelte index 548685392..6e710d06a 100644 --- a/web/src/routes/[handle]/+layout.svelte +++ b/web/src/routes/[handle]/+layout.svelte @@ -110,11 +110,15 @@ if (!(pending instanceof Promise)) return; const identifier = decodeURIComponent(page.params.handle ?? ""); const search = page.url.search; + const suffix = page.url.pathname.split("/").slice(2).join("/"); + const hash = page.url.hash; void pending .then((resolved) => { const canonical = resolved.handle.endsWith(".invalid") ? null : resolved.handle; if (canonical && identifier.toLowerCase() !== canonical.toLowerCase()) { - goto(`/${canonical}${search}` as ResolvedPathname); + goto( + `/${canonical}${suffix ? `/${suffix}` : ""}${search}${hash}` as ResolvedPathname + ); } }) // failures surface through the error boundary instead diff --git a/web/src/routes/[handle]/+layout.ts b/web/src/routes/[handle]/+layout.ts index 8bc9b3bee..a5842a1ea 100644 --- a/web/src/routes/[handle]/+layout.ts +++ b/web/src/routes/[handle]/+layout.ts @@ -110,7 +110,8 @@ export const load: LayoutLoad = async (event) => { if (!browser || isDid(identifier)) { const { identity, canonical } = await resolved; if (canonical && identifier.toLowerCase() !== canonical.toLowerCase()) { - redirect(307, `/${canonical}${event.url.search}`); + const suffix = event.url.pathname.split("/").slice(2).join("/"); + redirect(307, `/${canonical}${suffix ? `/${suffix}` : ""}${event.url.search}`); } return settle({ identity, diff --git a/web/src/routes/[handle]/handle.test.ts b/web/src/routes/[handle]/handle.test.ts index 8983c8821..67bd3ab92 100644 --- a/web/src/routes/[handle]/handle.test.ts +++ b/web/src/routes/[handle]/handle.test.ts @@ -50,6 +50,37 @@ const makeMockFetch = () => return jsonResponse({}, 404); }); +describe("/[handle] canonical navigation", () => { + it.each(["", "/settings", "/settings/members", "/settings/members/new"])( + "preserves the %s suffix and search parameters", + async (suffix) => { + const event = { + url: new URL(`http://web.test/${OWNER_DID}${suffix}?test=value`), + params: { handle: OWNER_DID }, + fetch: vi.fn().mockImplementation(async (_input, init) => { + if (String(init?.body).includes("resolveMiniDoc")) { + return jsonResponse({ + output: { did: OWNER_DID, handle: "tangled.org" }, + data: {} + }); + } + return jsonResponse({}, 404); + }), + parent: async () => ({ + publicConfig: { bobbinUrl: "https://bobbin.test" }, + auth: null + }), + route: { id: `/[handle]${suffix}` } + } as never; + + await expect(handleLayoutLoad(event)).rejects.toMatchObject({ + status: 307, + location: `/tangled.org${suffix}?test=value` + }); + } + ); +}); + describe("/[handle] repoDID navigation", () => { it("redirects /[repoDID] to /[ownerHandle]/[repoName]", async () => { const fetchMock = makeMockFetch(); -- 2.51.2