From 9c0a6b580586ed3a15def02004461cad996a9630 Mon Sep 17 00:00:00 2001 From: Besser Sehen Landshut Date: Mon, 24 Aug 2026 17:10:50 +0200 Subject: [PATCH 1/2] fix(ui): give pages a main landmark and mark the sidebar as navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No page of the interface carries a `main` landmark — `grep` for ` --- ui/src/pages/SideNavLayout/index.tsx | 8 ++++---- ui/src/pages/SideNavLayoutWithoutFooter/index.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ui/src/pages/SideNavLayout/index.tsx b/ui/src/pages/SideNavLayout/index.tsx index b9bc38b9c..52ecb854d 100644 --- a/ui/src/pages/SideNavLayout/index.tsx +++ b/ui/src/pages/SideNavLayout/index.tsx @@ -27,16 +27,16 @@ import '@/common/sideNavLayout.scss'; const Index: FC = () => { return (
-
-
+
-
+
-
+
diff --git a/ui/src/pages/SideNavLayoutWithoutFooter/index.tsx b/ui/src/pages/SideNavLayoutWithoutFooter/index.tsx index 5f931f78a..4ca38715a 100644 --- a/ui/src/pages/SideNavLayoutWithoutFooter/index.tsx +++ b/ui/src/pages/SideNavLayoutWithoutFooter/index.tsx @@ -27,16 +27,16 @@ import '@/common/sideNavLayout.scss'; const Index: FC = () => { return (
-
-
+
-
+
-
+
From 4a28f0ea442b158da4191354f25b6c18b0b09ac5 Mon Sep 17 00:00:00 2001 From: Besser Sehen Landshut Date: Mon, 24 Aug 2026 19:12:23 +0200 Subject: [PATCH 2/2] fix(ui): extend the main landmark to every route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The admin area has a layout of its own and sits directly under `pages/Layout`, so the two changes above did not reach it. Nineteen further routes — signing in, registering, recovering an account, the error pages — have no layout at all; they get a pathless one that is nothing but a `main`. `pages/Layout` itself would be the wrong place: it wraps the header, so a landmark there would put the navigation inside the content region and "skip to content" would land before it. Verified structurally — 88 paths before, the same 88 after, no page removed — and by building the front end and stepping through the affected routes: each renders inside exactly one `main` and draws its content. Co-Authored-By: Claude Opus 5 (1M context) --- ui/src/pages/Admin/index.tsx | 8 +- ui/src/pages/PlainLayout/index.tsx | 43 +++++ ui/src/router/routes.ts | 244 ++++++++++++++++------------- 3 files changed, 181 insertions(+), 114 deletions(-) create mode 100644 ui/src/pages/PlainLayout/index.tsx diff --git a/ui/src/pages/Admin/index.tsx b/ui/src/pages/Admin/index.tsx index da167ac40..5114a841f 100644 --- a/ui/src/pages/Admin/index.tsx +++ b/ui/src/pages/Admin/index.tsx @@ -36,20 +36,20 @@ const Index: FC = () => { }); return (
-
-
+
-
+
-
+
diff --git a/ui/src/pages/PlainLayout/index.tsx b/ui/src/pages/PlainLayout/index.tsx new file mode 100644 index 000000000..6364219c6 --- /dev/null +++ b/ui/src/pages/PlainLayout/index.tsx @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* A layout that is nothing but a main landmark. + * + * Most pages reach their `main` through `SideNavLayout` or `Admin`. The rest — + * signing in, registering, recovering an account, the error pages — hang + * straight off `pages/Layout`, which wraps the header and would therefore put + * the navigation inside `main` if the landmark were added there. + * + * A pathless layout route solves that without touching a single page: the + * children keep their paths, their order and their guards, and gain a landmark + * they can be skipped to. + */ + +import { FC, memo } from 'react'; +import { Outlet } from 'react-router-dom'; + +const Index: FC = () => { + return ( +
+ +
+ ); +}; + +export default memo(Index); diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index 8423fb7a3..66616ba3d 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -249,93 +249,101 @@ const routes: RouteNode[] = [ ], }, { - path: 'users/login', - page: 'pages/Users/Login', - guard: () => { - const notLogged = guard.notLogged(); - if (notLogged.ok) { - return notLogged; - } + // Pages with no layout of their own — signing in, recovering an + // account, the error pages. Wrapped so they reach a main landmark + // as well; see pages/PlainLayout. + page: 'pages/PlainLayout', + children: [ + { + path: 'users/login', + page: 'pages/Users/Login', + guard: () => { + const notLogged = guard.notLogged(); + if (notLogged.ok) { + return notLogged; + } - return guard.notActivated(); - }, - }, - { - path: 'users/register', - page: 'pages/Users/Register', - guard: () => { - const allowNew = guard.allowNewRegistration(); - if (!allowNew.ok) { - return allowNew; - } - const notLogged = guard.notLogged(); - if (notLogged.ok) { - const sa = guard.singUpAgent(); - if (!sa.ok) { - return sa; - } - } - return notLogged; - }, - }, - { - path: 'users/logout', - page: 'pages/Users/Logout', - guard: () => { - return guard.loggedRedirectHome(); - }, - }, - { - path: 'users/account-recovery', - page: 'pages/Users/AccountForgot', - guard: () => { - return guard.notLogged(); - }, - }, - { - path: 'users/change-email', - page: 'pages/Users/ChangeEmail', - }, - { - path: 'users/password-reset', - page: 'pages/Users/PasswordReset', - }, - { - path: 'users/account-activation', - page: 'pages/Users/ActiveEmail', - }, - { - path: 'users/account-activation/success', - page: 'pages/Users/ActivationResult', - guard: () => { - return guard.activated(); - }, - }, - { - path: '/users/account-activation/failed', - page: 'pages/Users/ActivationResult', - guard: () => { - return guard.notActivated(); - }, - }, - { - path: '/users/confirm-new-email', - page: 'pages/Users/ConfirmNewEmail', - }, - { - path: '/users/account-suspended', - page: 'pages/Users/Suspended', - guard: () => { - return guard.notLogged(); - }, - }, - { - path: '/users/confirm-email', - page: 'pages/Users/OauthBindEmail', - }, - { - path: '/users/auth-landing', - page: 'pages/Users/AuthCallback', + return guard.notActivated(); + }, + }, + { + path: 'users/register', + page: 'pages/Users/Register', + guard: () => { + const allowNew = guard.allowNewRegistration(); + if (!allowNew.ok) { + return allowNew; + } + const notLogged = guard.notLogged(); + if (notLogged.ok) { + const sa = guard.singUpAgent(); + if (!sa.ok) { + return sa; + } + } + return notLogged; + }, + }, + { + path: 'users/logout', + page: 'pages/Users/Logout', + guard: () => { + return guard.loggedRedirectHome(); + }, + }, + { + path: 'users/account-recovery', + page: 'pages/Users/AccountForgot', + guard: () => { + return guard.notLogged(); + }, + }, + { + path: 'users/change-email', + page: 'pages/Users/ChangeEmail', + }, + { + path: 'users/password-reset', + page: 'pages/Users/PasswordReset', + }, + { + path: 'users/account-activation', + page: 'pages/Users/ActiveEmail', + }, + { + path: 'users/account-activation/success', + page: 'pages/Users/ActivationResult', + guard: () => { + return guard.activated(); + }, + }, + { + path: '/users/account-activation/failed', + page: 'pages/Users/ActivationResult', + guard: () => { + return guard.notActivated(); + }, + }, + { + path: '/users/confirm-new-email', + page: 'pages/Users/ConfirmNewEmail', + }, + { + path: '/users/account-suspended', + page: 'pages/Users/Suspended', + guard: () => { + return guard.notLogged(); + }, + }, + { + path: '/users/confirm-email', + page: 'pages/Users/OauthBindEmail', + }, + { + path: '/users/auth-landing', + page: 'pages/Users/AuthCallback', + }, + ], }, // for admin { @@ -464,24 +472,32 @@ const routes: RouteNode[] = [ ], }, { - path: '/user-center/auth', - page: 'pages/UserCenter/Auth', - guard: () => { - const notLogged = guard.notLogged(); - return notLogged; - }, - }, - { - path: '/user-center/auth-failed', - page: 'pages/UserCenter/AuthFailed', - }, - { - path: '*', - page: 'pages/404', - }, - { - path: '50x', - page: 'pages/50X', + // Pages with no layout of their own — signing in, recovering an + // account, the error pages. Wrapped so they reach a main landmark + // as well; see pages/PlainLayout. + page: 'pages/PlainLayout', + children: [ + { + path: '/user-center/auth', + page: 'pages/UserCenter/Auth', + guard: () => { + const notLogged = guard.notLogged(); + return notLogged; + }, + }, + { + path: '/user-center/auth-failed', + page: 'pages/UserCenter/AuthFailed', + }, + { + path: '*', + page: 'pages/404', + }, + { + path: '50x', + page: 'pages/50X', + }, + ], }, // ai { @@ -532,12 +548,20 @@ const routes: RouteNode[] = [ ], }, { - path: '/users/unsubscribe', - page: 'pages/Users/Unsubscribe', - }, - { - path: '403', - page: 'pages/403', + // Pages with no layout of their own — signing in, recovering an + // account, the error pages. Wrapped so they reach a main landmark + // as well; see pages/PlainLayout. + page: 'pages/PlainLayout', + children: [ + { + path: '/users/unsubscribe', + page: 'pages/Users/Unsubscribe', + }, + { + path: '403', + page: 'pages/403', + }, + ], }, ], },