diff --git a/web/src/components/MemoContent/Tag.tsx b/web/src/components/MemoContent/Tag.tsx index 72cb3e2d..18a323c1 100644 --- a/web/src/components/MemoContent/Tag.tsx +++ b/web/src/components/MemoContent/Tag.tsx @@ -42,7 +42,7 @@ export const Tag: React.FC = ({ "data-tag": dataTag, children, classNa // If the tag is clicked in a memo detail page, we should navigate to the memo list page. if (location.pathname.startsWith("/m")) { - const pathname = parentPage || Routes.ENTRY; + const pathname = parentPage || Routes.HOME; const searchParams = new URLSearchParams(); searchParams.set("filter", stringifyFilters([{ factor: "tagSearch", value: tag }])); diff --git a/web/src/components/Navigation.tsx b/web/src/components/Navigation.tsx index 211a3982..5ff483a2 100644 --- a/web/src/components/Navigation.tsx +++ b/web/src/components/Navigation.tsx @@ -94,6 +94,7 @@ const Navigation = (props: Props) => { } key={navLink.id} to={navLink.path} + end={navLink.path === Routes.HOME} id={navLink.id} aria-label={navLink.id === "header-inbox" ? inboxAriaLabel : undefined} viewTransition diff --git a/web/src/router/guards.tsx b/web/src/router/guards.tsx index 71466170..7e0be47f 100644 --- a/web/src/router/guards.tsx +++ b/web/src/router/guards.tsx @@ -4,25 +4,29 @@ import { AUTH_REDIRECT_PARAM, buildAuthRoute, getSafeRedirectPath } from "@/util import { ROUTES } from "./routes"; /** - * Entry-route component mounted at `/`. Performs authentication-aware redirection - * to the correct landing page before any business UI renders, preserving the - * original query string and hash so bookmarks like `/?filter=foo` keep working. + * Index-route gate mounted at `/`. Authenticated visitors fall through to the + * nested Home page; unauthenticated visitors are redirected to `/explore`, + * preserving the original query string and hash so bookmarks like `/?filter=foo` + * keep working. */ export const LandingRoute = () => { const currentUser = useCurrentUser(); const location = useLocation(); - const target = currentUser ? ROUTES.HOME : ROUTES.EXPLORE; - return ( - - ); + if (!currentUser) { + return ( + + ); + } + + return ; }; /** @@ -44,7 +48,7 @@ export const RequireAuthRoute = () => { /** * Guard for guest-only routes (sign-in and sign-up). Already-authenticated users - * are redirected to the requested `redirect` target (when safe) or to `/home`. + * are redirected to the requested `redirect` target (when safe) or to `/`. * * The OAuth callback route (`/auth/callback`) intentionally opts out of this guard: * an authenticated session in another tab must not prevent the callback from diff --git a/web/src/router/index.tsx b/web/src/router/index.tsx index 66c107e7..255543c5 100644 --- a/web/src/router/index.tsx +++ b/web/src/router/index.tsx @@ -1,5 +1,5 @@ import { lazy } from "react"; -import { createBrowserRouter, type RouteObject } from "react-router-dom"; +import { createBrowserRouter, Navigate, type RouteObject } from "react-router-dom"; import App from "@/App"; import { ChunkLoadErrorFallback } from "@/components/ErrorBoundary"; import MainLayout from "@/layouts/MainLayout"; @@ -72,20 +72,23 @@ export const routeConfig: RouteObject[] = [ }, ], }, - { index: true, element: }, + // Backward compatibility: the old `/home` URL now lives at `/`. + { path: "home", element: }, { - path: Routes.ENTRY, element: , children: [ { element: , children: [ + { + element: , + children: [{ index: true, element: }], + }, { path: Routes.EXPLORE, element: }, { path: "u/:username", element: }, { element: , children: [ - { path: Routes.HOME, element: }, { path: Routes.ARCHIVED, element: }, { path: Routes.SHORTCUTS, element: }, ], diff --git a/web/src/router/routes.ts b/web/src/router/routes.ts index 82a73da5..d4467265 100644 --- a/web/src/router/routes.ts +++ b/web/src/router/routes.ts @@ -1,8 +1,5 @@ export const ROUTES = { - // Entry-only route. Hosts the landing redirect, never a business page. - ENTRY: "/", - // The authenticated user's primary workspace page. - HOME: "/home", + HOME: "/", ATTACHMENTS: "/attachments", INBOX: "/inbox", ARCHIVED: "/archived", diff --git a/web/tests/guards.test.tsx b/web/tests/guards.test.tsx index 6f9c7897..51476100 100644 --- a/web/tests/guards.test.tsx +++ b/web/tests/guards.test.tsx @@ -25,19 +25,20 @@ const renderAt = (initialEntry: string, children: ReactNode) => render({children}); describe("LandingRoute", () => { - it("sends an authenticated visitor from the entry to /home", () => { + it("renders the nested home page for an authenticated visitor at /", () => { mockedUseCurrentUser.mockReturnValue(fakeUser); renderAt( "/", - } /> - } /> + }> + home} /> + } /> , ); - expect(screen.getByTestId("location").textContent).toBe("/home"); + expect(screen.getByTestId("home")).toHaveTextContent("home"); }); it("sends an unauthenticated visitor from the entry to /explore", () => { @@ -46,8 +47,9 @@ describe("LandingRoute", () => { renderAt( "/", - } /> - } /> + }> + home} /> + } /> , ); @@ -55,20 +57,6 @@ describe("LandingRoute", () => { expect(screen.getByTestId("location").textContent).toBe("/explore"); }); - it("preserves the query string and hash when redirecting an authenticated visitor", () => { - mockedUseCurrentUser.mockReturnValue(fakeUser); - - renderAt( - "/?filter=tag:work&sort=desc#top", - - } /> - } /> - , - ); - - expect(screen.getByTestId("location").textContent).toBe("/home?filter=tag:work&sort=desc#top"); - }); - it("preserves the query string and hash when redirecting an unauthenticated visitor", () => { // Covers the regression in issue #5846: bookmarks pointing at `/?filter=...` // must not drop their params on the trip through the landing redirect. @@ -77,7 +65,9 @@ describe("LandingRoute", () => { renderAt( "/?filter=tag:work#latest", - } /> + }> + home} /> + } /> , ); @@ -91,10 +81,10 @@ describe("RequireAuthRoute", () => { mockedUseCurrentUser.mockReturnValue(fakeUser); renderAt( - "/home", + "/setting", }> - secret} /> + secret} /> , ); @@ -106,16 +96,16 @@ describe("RequireAuthRoute", () => { mockedUseCurrentUser.mockReturnValue(undefined); renderAt( - "/home?tab=pins#latest", + "/setting?tab=pins#latest", }> - secret} /> + secret} /> } /> , ); - expect(screen.getByTestId("location").textContent).toBe("/auth?redirect=%2Fhome%3Ftab%3Dpins%23latest"); + expect(screen.getByTestId("location").textContent).toBe("/auth?redirect=%2Fsetting%3Ftab%3Dpins%23latest"); }); }); @@ -135,7 +125,7 @@ describe("RequireGuestRoute", () => { expect(screen.getByTestId("sign-in")).toHaveTextContent("sign in"); }); - it("redirects already-authenticated users to /home by default", () => { + it("redirects already-authenticated users to / by default", () => { mockedUseCurrentUser.mockReturnValue(fakeUser); renderAt( @@ -144,11 +134,11 @@ describe("RequireGuestRoute", () => { }> sign in} /> - } /> + } /> , ); - expect(screen.getByTestId("location").textContent).toBe("/home"); + expect(screen.getByTestId("location").textContent).toBe("/"); }); it("honours a safe redirect target from the query string", () => { @@ -161,14 +151,14 @@ describe("RequireGuestRoute", () => { sign in} /> } /> - } /> + } /> , ); expect(screen.getByTestId("location").textContent).toBe("/setting"); }); - it("ignores an auth-family redirect target and falls back to /home", () => { + it("ignores an auth-family redirect target and falls back to /", () => { mockedUseCurrentUser.mockReturnValue(fakeUser); renderAt( @@ -177,14 +167,14 @@ describe("RequireGuestRoute", () => { }> sign in} /> - } /> + } /> , ); - expect(screen.getByTestId("location").textContent).toBe("/home"); + expect(screen.getByTestId("location").textContent).toBe("/"); }); - it("ignores an external redirect target and falls back to /home", () => { + it("ignores an external redirect target and falls back to /", () => { mockedUseCurrentUser.mockReturnValue(fakeUser); renderAt( @@ -193,10 +183,10 @@ describe("RequireGuestRoute", () => { }> sign in} /> - } /> + } /> , ); - expect(screen.getByTestId("location").textContent).toBe("/home"); + expect(screen.getByTestId("location").textContent).toBe("/"); }); }); diff --git a/web/tests/router-config.test.tsx b/web/tests/router-config.test.tsx index 12ca06a4..0b272395 100644 --- a/web/tests/router-config.test.tsx +++ b/web/tests/router-config.test.tsx @@ -2,7 +2,7 @@ import { isValidElement } from "react"; import type { RouteObject } from "react-router-dom"; import { describe, expect, it } from "vitest"; import { routeConfig, ROUTES } from "@/router"; -import { LandingRoute, RequireAuthRoute, RequireGuestRoute } from "@/router/guards"; +import { RequireAuthRoute, RequireGuestRoute } from "@/router/guards"; // Walk the nested route config and find the first route with the given path, // starting from the provided roots. Returns undefined if nothing matches. @@ -39,12 +39,6 @@ function hasAncestorOfType(routes: RouteObject[], path: string, guardType: unkno } describe("router configuration", () => { - it("mounts the LandingRoute at the entry index", () => { - const root = routeConfig[0]; - const indexRoute = root.children?.find((r) => r.index); - expect(elementType(indexRoute)).toBe(LandingRoute); - }); - it("keeps /auth/callback outside the guest-only guard", () => { // Regression guard for issue #5846 follow-up: an authenticated tab elsewhere // must not short-circuit the OAuth callback via RequireGuestRoute. @@ -58,7 +52,7 @@ describe("router configuration", () => { }); it("wraps authenticated-only pages in RequireAuthRoute", () => { - for (const path of [ROUTES.HOME, ROUTES.ARCHIVED, ROUTES.ATTACHMENTS, ROUTES.INBOX, ROUTES.SETTING]) { + for (const path of [ROUTES.ARCHIVED, ROUTES.ATTACHMENTS, ROUTES.INBOX, ROUTES.SETTING]) { expect(hasAncestorOfType(routeConfig, path, RequireAuthRoute)).toBe(true); } });