examples/new/.opencode/skills/frontend-navigation/SKILL.md
React Router v7 navigation - route definitions, SSR integration, auth guards, Link/Navigate patterns, layout nesting, and project conventions
npx skillsauth add aexol-studio/axolotl frontend-navigationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
React Router v7 Data Router — RouteObject[] config, not JSX <Routes>/<Route>.
routeConfig: RouteObject[] in frontend/src/routes/index.tsx — single source of truthcreateBrowserRouter(routeConfig) in entry-client.tsx — local const, never exported from routescreateStaticHandler / createStaticRouter in entry-server.tsx for SSR'react-router' — no react-router-dom<Outlet /> wrappersfrontend/src/routes/index.tsx exports routeConfig: RouteObject[] — a flat config array, never JSX <Routes>.
Structure:
id: 'root') with <RootLayout /> and rootLoader<GuestLayout /> group → guest routes (/, /login, /verify-email)<ProtectedLayout /> group → protected routes (/app, /settings)/examples){ path: '*', element: <NotFound /> } catch-allTo add a route: add a RouteObject to the correct group's children[] in routes/index.tsx.
| Path | Component | Auth | Group |
| --------------- | ------------- | ----------------------- | ----------------- |
| / | Landing | No (→ /app if authed) | GuestLayout |
| /login | Login | No (→ /app if authed) | GuestLayout |
| /verify-email | VerifyEmail | No (→ /app if authed) | GuestLayout |
| /examples | Examples | No | Public (no wrap) |
| /app | Dashboard | Yes (→ /login) | ProtectedLayout |
| /settings | Settings | Yes (→ /login) | ProtectedLayout |
| * | NotFound | No | Root |
Auth is centralized in ProtectedLayout loader — page loaders do data fetching only.
// protected layout loader — extracts per-request queryClient from context
import { type LoaderFunctionArgs, redirect } from 'react-router';
import { isAuthenticated, queryClient, type AppLoadContext } from '@/lib/queryClient.js';
export const protectedLoader = ({ context }: LoaderFunctionArgs) => {
const qc = (context as AppLoadContext | undefined)?.queryClient ?? queryClient;
if (!isAuthenticated(qc)) return redirect('/login');
return null;
};
// guest loader — redirect if already authenticated
export const loginLoader = ({ context }: LoaderFunctionArgs) => {
const qc = (context as AppLoadContext | undefined)?.queryClient ?? queryClient;
if (isAuthenticated(qc)) return redirect('/app');
return { meta: { title: 'Sign In', description: '' } };
};
import { Link, Navigate, useNavigate } from 'react-router';
<Link to="/app">Dashboard</Link>
<Navigate to="/app" replace /> {/* replace on auth redirects — prevents back-button loops */}
const navigate = useNavigate();
navigate('/app'); navigate('/', { replace: true });
frontend/src/routes/
├── index.tsx # routeConfig + rootLoader
├── RootLayout.tsx # ThemeProvider > DynamiteProvider > QueryClientProvider > Outlet
├── ErrorPage.tsx # useRouteError() fallback — used as errorElement
├── MetaUpdater.tsx # document.title updater via useMatches (mounted in RootLayout)
├── meta.ts # RouteMeta interface + buildMetaHead() for SSR head injection
├── guest/Layout.tsx # GuestLayout — pure <Outlet />
├── guest/landing/ # path: '/'
├── guest/login/ # path: '/login'
├── guest/verify-email/ # path: '/verify-email'
├── protected/Layout.tsx # ProtectedLayout — pure <Outlet />
├── protected/dashboard/ # path: '/app'
├── protected/settings/ # path: '/settings'
├── public/examples/ # path: '/examples'
└── not-found/ # path: '*'
RouteObject[] config only — never JSX <Routes>/<Route><Outlet />return redirect('/login') — Guest: return redirect('/app').js extensions in imports — ESM requirement (from './Layout.js')const X = () => {}routes/{group}/{route}/components/| Task | Code/Pattern |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| Import router primitives | import { Link, useNavigate, useLoaderData } from 'react-router' |
| Navigate with link | <Link to="/app">Dashboard</Link> |
| Redirect component | <Navigate to="/" replace /> |
| Programmatic navigation | const navigate = useNavigate(); navigate('/app') |
| Auth guard (protected) | const qc = (context as AppLoadContext)?.queryClient ?? queryClient; if (!isAuthenticated(qc)) return redirect('/login') in loader |
| Auth guard (guest) | const qc = (context as AppLoadContext)?.queryClient ?? queryClient; if (isAuthenticated(qc)) return redirect('/app') in loader |
| Add route | Add RouteObject to correct layout's children in routes/index.tsx |
tools
Baseline architecture for Axolotl mobile starter (Expo Router + reusable blocks)
tools
Expo Router conventions for route groups, native headers, and starter navigation
development
i18n baseline and dev-translate setup for Expo mobile starter
development
Starter data layer pattern with React Query + Zeus for Expo app