diff --git a/web/src/components/DiffToolbar.tsx b/web/src/components/DiffToolbar.tsx
index 70ec5db14..e457b66b2 100644
--- a/web/src/components/DiffToolbar.tsx
+++ b/web/src/components/DiffToolbar.tsx
@@ -96,10 +96,10 @@ export function DiffToolbar({
type="button"
onClick={onToggleTreeDesktop}
aria-label={desktopTreeOpen ? t("repo.hide_file_tree") : t("repo.show_file_tree")}
- aria-pressed={desktopTreeOpen}
+ aria-pressed={!!desktopTreeOpen}
// `pl-1` nudges the icon right so it visually aligns with
// the sidebar's collapsed-rail edge on desktop.
- className="hidden size-6 cursor-pointer place-items-center rounded text-(--color-muted-foreground) hover:bg-(--color-surface) hover:text-(--color-foreground) lg:grid pl-1"
+ className="hidden size-6 cursor-pointer place-items-center rounded text-(--color-muted-foreground) hover:bg-(--color-surface) hover:text-(--color-foreground) lg:grid lg:pl-1"
>
{desktopTreeOpen ? (
diff --git a/web/src/router.tsx b/web/src/router.tsx
index 8d083e80b..0b984a38c 100644
--- a/web/src/router.tsx
+++ b/web/src/router.tsx
@@ -1,34 +1,17 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import {
- Outlet,
- RouterProvider,
- createRootRouteWithContext,
- createRoute,
- createRouter,
- notFound,
-} from "@tanstack/react-router";
+import { Outlet, RouterProvider, createRootRouteWithContext, createRoute, createRouter } from "@tanstack/react-router";
import { Footer } from "@/components/Footer";
import { Navbar } from "@/components/Navbar";
import { TooltipProvider } from "@/components/ui/tooltip";
import { webContext } from "@/lib/context";
-import { LoaderResponseError, loaderResponseError } from "@/lib/loader-error";
-import { repoHeaderQuery } from "@/lib/queries/repo";
-import { subUrl } from "@/lib/url";
import type { UserInfo } from "@/lib/user-info";
import { Landing } from "@/pages/Landing";
import { NotFound } from "@/pages/NotFound";
import { ServerError } from "@/pages/ServerError";
-import { RepoCommit, type RepoCommitPage } from "@/pages/repo/Commit";
-import { validateRepoCommitSearch } from "@/pages/repo/Commit.search";
+import { createRepoRoutes } from "@/routes/repo";
import { createUserRoutes } from "@/routes/user";
-// Match the legacy server-side route constraint (see `web.go` near the
-// `/commit/:sha([a-f0-9]{7,40})$` declaration). The server enforces the same
-// shape for SEO and to skip the SPA shell for malformed paths; this client
-// check short-circuits the loader so we render 404 without a wasted fetch.
-const SHA_RE = /^[a-f0-9]{7,40}$/;
-
interface RouterContext {
user: UserInfo | null;
queryClient: QueryClient;
@@ -55,57 +38,7 @@ const landingRoute = createRoute({
component: Landing,
});
-const repoCommitRoute = createRoute({
- getParentRoute: () => rootRoute,
- path: "/$owner/$repo/commit/$sha",
- validateSearch: validateRepoCommitSearch,
- // Reject malformed SHA at parse time so the route doesn't match for paths
- // like `/owner/repo/commit/garbage`. The thrown `notFound()` bubbles to the
- // root route's NotFound component.
- params: {
- parse: (raw: { owner: string; repo: string; sha: string }) => {
- if (!SHA_RE.test(raw.sha)) {
- // eslint-disable-next-line @typescript-eslint/only-throw-error -- `notFound()` is the documented TanStack Router signal for 404, not an Error subclass.
- throw notFound();
- }
- return raw;
- },
- stringify: (params: { owner: string; repo: string; sha: string }) => params,
- },
- loaderDeps: ({ search }) => ({ whitespace: search.whitespace }),
- loader: async ({ params, deps, context }): Promise => {
- const metaURL = subUrl(`/api/web/${params.owner}/${params.repo}/commit/${params.sha}`);
- const rawBase = subUrl(`/${params.owner}/${params.repo}/commit/${params.sha}.diff`);
- const rawURL = deps.whitespace ? `${rawBase}?whitespace=${encodeURIComponent(deps.whitespace)}` : rawBase;
- // Three requests in parallel: repo header (via Query cache for cross-page
- // reuse), commit metadata JSON, raw patch text. Splitting the patch out
- // skips JSON-string escaping and lets the browser cache the (often large)
- // patch separately from the metadata.
- try {
- const [, meta, patch] = await Promise.all([
- context.queryClient.ensureQueryData(repoHeaderQuery(params.owner, params.repo)),
- fetch(metaURL, { credentials: "same-origin" }).then(async (res) => {
- if (!res.ok) throw await loaderResponseError(res);
- return (await res.json()) as Omit;
- }),
- fetch(rawURL, { credentials: "same-origin" }).then(async (res) => {
- if (!res.ok) throw await loaderResponseError(res);
- return res.text();
- }),
- ]);
- return { ...meta, patch };
- } catch (err) {
- if (err instanceof LoaderResponseError && err.status === 404) {
- // eslint-disable-next-line @typescript-eslint/only-throw-error -- `notFound()` is the documented TanStack Router signal for 404, not an Error subclass.
- throw notFound();
- }
- throw err;
- }
- },
- component: RepoCommit,
-});
-
-const routeTree = rootRoute.addChildren([landingRoute, ...createUserRoutes(rootRoute), repoCommitRoute]);
+const routeTree = rootRoute.addChildren([landingRoute, ...createUserRoutes(rootRoute), ...createRepoRoutes(rootRoute)]);
function makeRouter(context: RouterContext) {
return createRouter({
diff --git a/web/src/routes/repo.tsx b/web/src/routes/repo.tsx
new file mode 100644
index 000000000..d0ccc7bf6
--- /dev/null
+++ b/web/src/routes/repo.tsx
@@ -0,0 +1,73 @@
+import type { QueryClient } from "@tanstack/react-query";
+import { type AnyRoute, createRoute, notFound } from "@tanstack/react-router";
+
+import { LoaderResponseError, loaderResponseError } from "@/lib/loader-error";
+import { repoHeaderQuery } from "@/lib/queries/repo";
+import { subUrl } from "@/lib/url";
+import { RepoCommit, type RepoCommitPage } from "@/pages/repo/Commit";
+import { type RepoCommitSearch, validateRepoCommitSearch } from "@/pages/repo/Commit.search";
+
+// Match the legacy server-side route constraint (see `web.go` near the
+// `/commit/:sha([a-f0-9]{7,40})$` declaration). The server enforces the same
+// shape for SEO and to skip the SPA shell for malformed paths; this client
+// check short-circuits the loader so we render 404 without a wasted fetch.
+const SHA_RE = /^[a-f0-9]{7,40}$/;
+
+interface RouterContext {
+ queryClient: QueryClient;
+}
+
+export function createRepoRoutes(rootRoute: AnyRoute) {
+ const repoCommitRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: "/$owner/$repo/commit/$sha",
+ validateSearch: validateRepoCommitSearch,
+ // Reject malformed SHA at parse time so the route doesn't match for paths
+ // like `/owner/repo/commit/garbage`. The thrown `notFound()` bubbles to the
+ // root route's NotFound component.
+ params: {
+ parse: (raw: { owner: string; repo: string; sha: string }) => {
+ if (!SHA_RE.test(raw.sha)) {
+ // eslint-disable-next-line @typescript-eslint/only-throw-error -- `notFound()` is the documented TanStack Router signal for 404, not an Error subclass.
+ throw notFound();
+ }
+ return raw;
+ },
+ stringify: (params: { owner: string; repo: string; sha: string }) => params,
+ },
+ loaderDeps: ({ search }: { search: RepoCommitSearch }) => ({ whitespace: search.whitespace }),
+ loader: async ({ params, deps, context }): Promise => {
+ const metaURL = subUrl(`/api/web/${params.owner}/${params.repo}/commit/${params.sha}`);
+ const rawBase = subUrl(`/${params.owner}/${params.repo}/commit/${params.sha}.diff`);
+ const rawURL = deps.whitespace ? `${rawBase}?whitespace=${encodeURIComponent(deps.whitespace)}` : rawBase;
+ const routerContext = context as RouterContext;
+ // Three requests in parallel: repo header (via Query cache for cross-page
+ // reuse), commit metadata JSON, raw patch text. Splitting the patch out
+ // skips JSON-string escaping and lets the browser cache the (often large)
+ // patch separately from the metadata.
+ try {
+ const [, meta, patch] = await Promise.all([
+ routerContext.queryClient.ensureQueryData(repoHeaderQuery(params.owner, params.repo)),
+ fetch(metaURL, { credentials: "same-origin" }).then(async (res) => {
+ if (!res.ok) throw await loaderResponseError(res);
+ return (await res.json()) as Omit;
+ }),
+ fetch(rawURL, { credentials: "same-origin" }).then(async (res) => {
+ if (!res.ok) throw await loaderResponseError(res);
+ return res.text();
+ }),
+ ]);
+ return { ...meta, patch };
+ } catch (err) {
+ if (err instanceof LoaderResponseError && err.status === 404) {
+ // eslint-disable-next-line @typescript-eslint/only-throw-error -- `notFound()` is the documented TanStack Router signal for 404, not an Error subclass.
+ throw notFound();
+ }
+ throw err;
+ }
+ },
+ component: RepoCommit,
+ });
+
+ return [repoCommitRoute];
+}
diff --git a/web/vite.config.ts b/web/vite.config.ts
index 478e1bf82..09acc59de 100644
--- a/web/vite.config.ts
+++ b/web/vite.config.ts
@@ -13,10 +13,10 @@ export default defineConfig({
},
server: {
port: 5173,
- // The dev page is served by the gogs Go server (e.g. https://gogs.localhost)
- // which reverse-proxies HTTP to this Vite dev server. That proxy is HTTP-only,
- // so the HMR client's WebSocket can't tunnel through it. Point HMR's WS
- // directly at the Vite dev port instead, bypassing gogs entirely.
+ // The dev page is served by the Go server (e.g., https://gogs.localhost)
+ // which reverse-proxies HTTP to this Vite dev server. That proxy is
+ // HTTP-only, so the HMR client's WebSocket can't tunnel through it. Point
+ // HMR's WS directly at the Vite dev port instead, bypassing gogs entirely.
hmr: {
protocol: "ws",
host: "localhost",