AgentSkillsCN

pages-and-routing-guidelines

添加路由并保持导航菜单的同步更新。在新增客户端路由时,可使用此技能。

SKILL.md
--- frontmatter
name: pages-and-routing-guidelines
description: Adding routes and keeping navigation menus in sync. Use this when adding client routes.
title: Routes & Navigation
summary: "Routes defined in `src/client/routes/index.ts`. Add to `navItems`/`menuItems` in `NavLinks.tsx` if user-accessible. Options: `public`, `fullScreen`, `adminOnly`."
priority: 3

SPA Routing Guidelines

This document outlines the process for adding new routes to our Single Page Application (SPA) routing system.

Overview

Our application uses a custom SPA routing system built on top of Next.js. The routing system is implemented in the src/client/router directory and consists of:

  1. A RouterProvider component that manages navigation and renders the current route
  2. Route components organized in folders within the src/client/routes directory
  3. Route configuration in the src/client/routes/index.ts file
  4. Navigation components in the src/client/components/layout directory
  5. Route persistence via useUIStore (Zustand) - remembers the last visited route for PWA instant boot

Adding a New Route

Follow these steps to add a new route to the application:

1. Create a Route Component Folder

Create a new folder in the src/client/routes directory with the name of your route component:

code
src/client/routes/
├── NewRoute/
│   ├── NewRoute.tsx
│   └── index.ts

Component Organization Guidelines

Follow these best practices for route components:

  • Keep route components focused and small: The main route component should be primarily responsible for layout and composition, not complex logic.

  • Split large components: If a route component is getting too large (over 200-300 lines), split it into multiple smaller components within the same route folder.

  • Route-specific components: Components that are only used by a specific route should be placed in that route's folder.

  • Shared components: If a component is used by multiple routes, move it to src/client/components directory.

  • Component hierarchy:

    code
    src/client/routes/NewRoute/           # Route-specific folder
    ├── NewRoute.tsx                      # Main route component (exported)
    ├── hooks.ts                          # React Query hooks for this route
    ├── NewRouteHeader.tsx                # Route-specific component
    ├── NewRouteContent.tsx               # Route-specific component
    └── index.ts                          # Exports the main component
    
    src/client/components/                # Shared components
    ├── SharedComponent.tsx               # Used by multiple routes
    └── ...
    
  • Colocate React Query hooks in a hooks.ts file within the route folder (unless shared across routes)

  • Extract business logic into separate hooks or utility functions

  • Follow the naming convention of PascalCase for component files and folders

  • Use named exports (avoid default exports as per our guidelines)

  • Keep related components and utilities in the same folder

Data Fetching Pattern

Routes that fetch data should use React Query hooks (NOT direct API calls):

tsx
// src/client/routes/Todos/hooks.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { getTodos, createTodo } from '@/apis/todos/client';

export const todosQueryKey = ['todos'] as const;

export function useTodos() {
    return useQuery({
        queryKey: todosQueryKey,
        queryFn: async () => {
            const response = await getTodos({});
            if (response.data?.error) throw new Error(response.data.error);
            return response.data;
        },
    });
}

// src/client/routes/Todos/Todos.tsx
export const Todos = () => {
    const { data, isLoading, error } = useTodos();
    
    if (isLoading && !data) return <LoadingSpinner />;
    if (error) return <ErrorDisplay error={error} />;
    
    return <TodoList todos={data?.todos || []} />;
};

Key points:

  • Data loads instantly from localStorage cache (React Query persistence)
  • Background revalidation keeps data fresh
  • Use isLoading && !data to show loading only on initial load (not cache)
  • See state-management-guidelines.mdc and docs/react-query-mutations.md for React Query patterns

2. Register the Route in the Routes Configuration

Add your new route to the routes configuration in src/client/routes/index.ts:

tsx
// Import your new route component
import { NewRoute } from './NewRoute';

// Add it to the routes configuration
export const routes = createRoutes({
  '/': Home,
  '/ai-chat': AIChat,
  '/settings': Settings,
  '/file-manager': FileManager,
  '/new-route': NewRoute, // Add your new route here
  '/not-found': NotFound,
});

Route Metadata

Routes can be configured with metadata using the RouteConfig object format:

tsx
export const routes = createRoutes({
  // Simple route (requires authentication)
  '/dashboard': Dashboard,
  
  // Public route (no authentication required)
  '/share/:id': { component: SharePage, public: true },
  
  // Admin-only route
  '/admin/reports': { component: Reports, adminOnly: true },
});
PropertyTypeDefaultDescription
componentReact.ComponentTyperequiredThe route component
publicbooleanfalseIf true, bypasses authentication in AuthWrapper
adminOnlybooleanfalseIf true, requires admin access

When to use public: true:

  • Share pages that should be accessible without login
  • Landing pages or marketing pages
  • Public API documentation pages

How it works:

  • RouterProvider computes isPublicRoute from route metadata and provides it via context
  • AuthWrapper (inside RouterProvider) uses useRouter() to get isPublicRoute
  • Public routes render immediately without waiting for auth validation
  • AuthWrapper re-renders on navigation, ensuring auth is checked when navigating to protected routes

Route path naming conventions:

  • Use kebab-case for route paths (e.g., /new-route, not /newRoute)
  • Keep paths descriptive but concise
  • Avoid deep nesting when possible

3. Add Navigation Item

Update the navigation items in src/client/components/NavLinks.tsx to include your new route:

tsx
import { Extension } from 'lucide-react'; // Choose an appropriate icon

export const navItems: NavItem[] = [
  { path: '/', label: 'Home', icon: <Home /> },
  { path: '/ai-chat', label: 'AI Chat', icon: <MessageSquare /> },
  { path: '/file-manager', label: 'Files', icon: <Folder /> },
  { path: '/new-route', label: 'New Route', icon: <Extension /> }, // Add your new route here
  { path: '/settings', label: 'Settings', icon: <Settings /> },
];

Navigation item guidelines:

  • Choose a descriptive but concise label
  • Select an appropriate icon from lucide-react that represents the route's purpose
  • Consider the order of items in the navigation (most important/frequently used routes should be more accessible)

Using the Router

Navigation

To navigate between routes in your components, use the useRouter hook:

tsx
import { useRouter } from '../../router';

const MyComponent = () => {
  const { navigate } = useRouter();
  
  const handleClick = () => {
    navigate('/new-route');
  };
  
  // You can also replace the current history entry
  const handleReplace = () => {
    navigate('/new-route', { replace: true });
  };
  
  return (
    <Button onClick={handleClick}>Go to New Route</Button>
  );
};

Navigation Guidelines

  • Always use the navigation API from useRouter: Never use window.location.href for navigation as it causes a full page reload and breaks the SPA behavior.
tsx
// ❌ Don't do this
window.location.href = '/some-route';

// ✅ Do this instead
const { navigate } = useRouter();
navigate('/some-route');
  • This ensures consistent navigation behavior throughout the application
  • Preserves the SPA (Single Page Application) experience
  • Maintains application state during navigation
  • Enables proper history management

Navigating with Parameters

When navigating to routes that require parameters (like IDs), construct the path with the parameters included:

tsx
// Navigating to a route with a parameter
const { navigate } = useRouter();

// Navigate to a video page with a specific video ID
const handleVideoClick = (videoId) => {
  navigate(`/video/${videoId}`);
};

Getting Current Route

You can access the current route path using the useRouter hook:

tsx
import { useRouter } from '../../router';

const MyComponent = () => {
  const { currentPath } = useRouter();
  
  return (
    <div>
      <p>Current path: {currentPath}</p>
    </div>
  );
};

Advanced Routing Features

Route Parameters

Our router automatically parses route parameters from the URL path. To define a route with parameters, use the colon syntax in your route path:

tsx
// In src/client/routes/index.ts
export const routes = createRoutes({
  // Other routes...
  '/items/:id': ItemDetail,
});

Then access the parameters in your component using the useRouter hook:

tsx
// src/client/routes/ItemDetail/ItemDetail.tsx
import { useRouter } from '../../router';

export const ItemDetail = () => {
  const { routeParams } = useRouter();
  const itemId = routeParams.id;
  
  return (
    <div>
      <h1>Item Detail</h1>
      {itemId ? <p>Item ID: {itemId}</p> : <p>Invalid item ID</p>}
    </div>
  );
};

Query Parameters

The router also automatically parses query parameters from the URL. Access them in your component using the useRouter hook:

tsx
// src/client/routes/SearchResults/SearchResults.tsx
import { useRouter } from '../../router';

export const SearchResults = () => {
  const { queryParams } = useRouter();
  const searchQuery = queryParams.q || '';
  
  return (
    <div>
      <h1>Search Results</h1>
      <p>Query: {searchQuery}</p>
    </div>
  );
};

PWA Route Persistence

The router automatically persists the current route to useUIStore (Zustand with localStorage).

How it works:

  1. When navigating, the route is saved to lastRoute in the UI store
  2. On app startup, if the URL is / (root), the last route is restored
  3. Auth-related routes (/login, /register, etc.) are NOT persisted

Excluded routes (not persisted):

  • /login
  • /register
  • /logout
  • /forgot-password

This enables PWA instant boot - when iOS kills the app, reopening it returns to the last viewed page.