Skip to content
cccskills
FreePublished on GitHubEnglish overview

spa-routes

Use this guide when adding SPA pages or changing routers. It helps keep route files focused on page entry points, place domain logic in features, and coordinate shared, desktop, and mobile paths, including loading skeletons and platform-specific differences.

OriginalShow the original description

Use for SPA routes and page segments: Web/desktop/mobile/popup routers, redirects and src/routes-to-features boundaries.

lobehub/lobehub83,506 stars(GitHub 83,506 + cccskills 0)

Run score Not tested

See how to install

Good for

  • Adding a page or dynamic route to an SPA
  • Deciding whether UI or logic belongs in routes or features
  • Keeping shared desktop and mobile routes in sync

Overview

What it does

This guide explains how to add and organize SPA routes while keeping page entry points separate from business logic and UI. It recommends putting thin layouts and page entry files in src/routes/ and organizing domain components and state in src/features/. It also covers registering common desktop routes in a shared definition, handling Web and Electron differences, matching mobile routes or documenting unsupported ones, and choosing where loading skeletons belong.

When it helps

Use it when adding a page or dynamic route, gradually reorganizing existing pages, or deciding whether a component belongs in a route or feature. It is also useful when several pages share domain features or when you want to keep route behavior aligned across Web, Electron, and mobile. The guide advises checking the current project structure before deciding where new code should go.

Example requests

  • “Add a settings page to this SPA and organize the route and feature files.”
  • “Should this screen’s UI and logic go in a route or a feature?”
  • “Check the mobile route counterpart and loading display for the route added to desktop.”

Notes

No special notes.

This overview was written by AI (OpenAI Codex) from the public SKILL.md. Check the original below for the exact behavior.

Document check

AI checked the SKILL.md for operations that need care and for setup requirements. The score comes only from actually running the skill (hands-on test).

SafetyAre sending data, deleting files, and running commands disclosed?
AGoodThe described work changes route structure; no sending or deletion is specified.
Easy setupDoes it need few tools or API keys, and are they documented?
AGoodNo API keys, external services, or installation prerequisites are specified.

Not counted in the score. Rated by AI (OpenAI Codex) as of Oct 26, 2026. Treat it as a guide and read the original before installing.

Disagree? Request a re-evaluation

Tell us which point is wrong and why. No login needed. We will check it against the SKILL.md, redo the evaluation, and show the result here.

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

Files included (1)

  • SKILL.md16.3 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

SPA Routes and Features Guide

SPA structure:

  • src/spa/ – Entry points (entry.web.tsx, entry.mobile.tsx, entry.desktop.tsx) and router config (router/). Router lives here to avoid confusion with src/routes/.
  • src/routes/ – Page segments only (roots).
  • src/features/ – Business logic and UI by domain.

This project uses a roots vs features split: src/routes/ only holds page segments; business logic and UI live in src/features/ by domain.

Agent constraint — shared desktop router: Common Web/Electron paths, nesting, metadata, lazy loaders, and preload groups belong in src/spa/router/desktopRouter.shared.tsx. The two desktopRouter.config* files are thin platform adapters; change them only for genuine runtime differences. Do not duplicate a common route in both adapters.

When to Use This Skill

  • Adding a new SPA route or route segment
  • Defining or refactoring layout/page files under src/routes/
  • Moving route-specific components or logic into src/features/
  • Deciding where to put a new component (route folder vs feature folder)

1. What Belongs in src/routes/ (roots)

Each route directory should contain only:

File / folderPurpose
_layout/index.tsx or layout.tsxLayout for this segment: wrap with <Outlet />, optional shell (e.g. sidebar + main). Should be thin: prefer re-exporting or composing from @/features/*.
index.tsx or page.tsxPage entry for this segment. Only import from features and render; no business logic.
[param]/index.tsx (e.g. [id], [cronId])Dynamic segment page. Same rule: thin, delegate to features.

Rule: Route files should only import and compose. No new features/ folders or heavy components inside src/routes/.


2. What Belongs in src/features/

Put domain-oriented UI and logic here:

  • Layout building blocks: sidebars, headers, body panels, drawers
  • Hooks and store usage for that domain
  • Domain-specific forms, lists, modals, etc.

Organize by domain (e.g. Pages, Home, Agent, PageEditor), not by route path. One route can use several features; one feature can be used by several routes.

Each feature should:

  • Live under src/features/<FeatureName>/
  • Export a clear public API via index.ts or index.tsx
  • Use @/features/<FeatureName>/... for internal imports when needed

3. How to Add a New SPA Route

  1. Choose the route group

    • (main)/ – desktop main app
    • (mobile)/ – mobile
    • (desktop)/ – Electron-specific
    • onboarding/, share/ – special flows
  2. Create only segment files under src/routes/

    • e.g. src/routes/(main)/my-feature/_layout/index.tsx and src/routes/(main)/my-feature/index.tsx (and optional [id]/index.tsx).
  3. Implement layout and page content in src/features/

    • Create or reuse a domain (e.g. src/features/MyFeature/).
    • Put layout (sidebar, header, body) and page UI there; export from the feature’s index.
  4. Keep route files thin

    • Layout: export { default } from '@/features/MyFeature/MyLayout' or compose a few feature components + <Outlet />.
    • Page: import from @/features/MyFeature (or a specific subpath) and render; no business logic in the route file.
  5. Register the route in the correct definition layer

    • Shared Web/Electron route: add the segment once in desktopRouter.shared.tsx with dynamicElement / dynamicLayout. Put its preloadId there as part of the shared lazy-loader definition.
    • Web-only or Electron-only route: add it to the corresponding thin desktopRouter.config.tsx adapter. Keep platform-only differences explicit and small.
    • Mobile counterpart (required for every desktop main-area route): see §3c. mobileRouter.parity.test.ts fails until the mobile router resolves the new URL to the same pattern, or the route is listed in MOBILE_UNSUPPORTED with a reason.
    • Mobile-only flow: use mobileRouter.config.tsx.
  6. Register a route skeleton (REQUIRED for every lazy route)

    • Every lazy route inside the main area renders RouteSegmentSkeleton (src/components/Skeleton/RouteSegment.tsx) while its chunk loads. It resolves via handle.meta.Skeleton only (deepest match wins, walking up through parent routes). Omitting Skeleton renders a blank pane — use NoRouteSkeleton when that is intentional.
    • Pick the skeleton when adding or restructuring a route:
      • A close-enough generic shape exists → Skeleton: createSurfaceSkeleton('list' | 'form' | 'grid' | 'editor' | 'detail') from @/components/Skeleton/Surface.
      • The page has a distinctive layout (dashboard, multi-panel, conversation) → author a bespoke component under src/components/Skeleton/ and register it (see ResourceHome.tsx, Generation.tsx, Conversation/).
    • Where to put it: on the route's routeMeta (feature routeMeta.ts or inline in desktopRouter.shared.tsx). A whole subtree sharing one shape can register once on the parent/layout route's handle — children with their own Skeleton still override.
    • When changing a page's layout, update its registered skeleton in the same PR — a stale skeleton that no longer matches the page is a regression.
    • Skeleton-only parent handles are safe for titles: title/icon resolution also walks deepest-first, and leaf metas keep winning.
    • The same deepest skeleton renders at every pending boundary of the chain (layout chunk, page chunk, then the page's own data wait). Make the page's in-component loading state the registered skeleton, or a body that continues it; a different shape there reads as a second, unrelated skeleton.
    • Route skeletons are always static imports: a fallback has to paint on its first render, and a lazy() skeleton (preloaded or not) renders blank until its chunk lands. That puts the skeleton's whole import graph in the entry chunk, so it may only reach UI primitives, other skeletons, constants and import type. Keep stores, services, feature components and value imports from barrels (@/types/* re-exports zod schemas) out of it; move the minimal piece next to the skeleton instead (see ResourceHome/Skeleton and Explorer/viewMode.ts).
    • A custom fallback (fallback: option, in-page Suspense, or an early return <XSkeleton />) keeps the handover continuous only through delayed() / DelayedFallback or RouteLoading, which register the skeleton as visible (retainSkeleton). Rendering a skeleton any other way lets the next boundary wait out its 200ms delay on a blank pane.

3a. Shared desktop route definition and platform adapters

FileRole
desktopRouter.shared.tsxSingle source of truth for common paths, nesting, metadata, lazy imports, and prioritized route preload groups.
desktopRouter.config.tsxThin Web adapter: mounts the common content tree at / and adds Web-only routes.
desktopRouter.config.desktop.tsxThin Electron adapter: injects per-tab Home behavior, TabHost root stubs, and the Electron-only /desktop-onboarding local setup.

Add or remove common routes only in desktopRouter.shared.tsx, or in a subtree module it imports (taskWorkspaceRoutes.tsx, projectWorkspaceRoutes.tsx, agentPageRoutes.tsx, memoryRoutes.tsx, resourceRoutes.tsx, workspaceSettingsRoutes.tsx for the padded /:slug/settings/* tabs) — the shared file is over the 1200-line limit, so large subtrees move out instead of growing it. Add any new subtree module to readRouterSources in desktopRouter.sync.test.tsx. Keep desktopRouter.sync.test.tsx passing so path behavior, lazy boundaries, preload ownership, and the intentional platform differences remain verified.


3b. Other .desktop.{ts,tsx} variants inside src/routes/

The thin router adapters are not duplicated trees. Other route modules may still colocate a <name>.desktop.{ts,tsx} next to a base <name>.{ts,tsx}; Vite's resolver swaps in the .desktop file for Electron builds. Those paired module implementations still carry a drift risk.

Known variants today:

Base file (web)Desktop file (Electron)Purpose
src/features/Settings/features/componentMap.tssrc/features/Settings/features/componentMap.desktop.tsSettings tab → component map. Web uses dynamic import(); desktop uses sync imports. componentMap.sync.test.ts enforces identical keys.
src/routes/(main)/agent/index.tsxsrc/routes/(main)/agent/index.desktop.tsxPage entry. Desktop variant overrides the web page wholesale (e.g. extra popup guards).
src/routes/(main)/group/index.tsxsrc/routes/(main)/group/index.desktop.tsxSame pattern as agent.

Rules:

  1. After editing any .ts/.tsx under src/routes/, glob the same directory for a <filename>.desktop.{ts,tsx} sibling. If one exists, apply the equivalent change there in the same commit.
  2. When adding a new SettingsTab, register it in both componentMap.ts (with dynamic(...)) and componentMap.desktop.ts (with a sync import). componentMap.sync.test.ts will fail the build otherwise.
  3. When adding a new desktop-only page wholesale-override, prefer a single base file with platform-aware code over introducing a new .desktop.tsx variant — only add a new variant when the two trees genuinely diverge (different store wiring, different popup guards, etc.).
  4. When deleting, remove both files together.

3c. Mobile counterparts of desktop routes

The mobile web build has its own router (mobileRouter.config.tsx, __MOBILE__), but it mounts the same page segments as desktop. Do not copy a desktop route into the mobile files: reuse the subtree module and adjust only what differs with overlayRoutes (routeOverlay.ts; withHandle, dropRoute, or a mobile element).

Mobile fileReuses
mobileAgentRoutes.tsxagentPageChildren under the agent scope layout; group profile / permission
mobileWorkspaceRoutes.tsxcreateTaskWorkspaceChildren, projectWorkspaceRoute, resourceRoute, memoryChildren, inside the mobile page layout
mobileWorkspaceSettingsRoutes.tsxworkspaceSettingsContentRoute children plus the mobile settings menu and full-bleed tabs
  • A desktop subtree added to one of those modules reaches mobile automatically. A new top-level desktop surface needs one entry in the matching mobile file; wrap full-screen pages in pageLayout ((mobile)/_page-layout).
  • Mobile NavHeader draws back + the route's NavPanelPortal sidebar as a drawer, and falls back to the route titleKey when the page header has no title. A page that paints no NavHeader gets one from the route handle mobileShellHeader: true | 'untitled' (and mobileShellTitle for a record name). Do not add a second back button inside the page; opt out with showTogglePanelButton={false} when the page keeps its own.
  • Desktop-only surfaces go in mobileUnsupportedRoutes and MOBILE_UNSUPPORTED in mobileRouter.parity.test.ts, so they redirect home instead of being read as a /:workspaceSlug.

4. How to Divide Files (route vs feature)

QuestionPut in src/routes/Put in src/features/
Is it the route’s layout wrapper or page entry?Yes – _layout/index.tsx, index.tsx, [id]/index.tsxNo
Does it contain business logic or non-trivial UI?NoYes – under the right domain
Is it a reusable layout piece (sidebar, header, body)?NoYes
Is it a hook, store usage, or domain logic?NoYes
Is it only re-exporting or composing feature components?YesNo

Examples

  • Route (thin):
    src/routes/(main)/page/_layout/index.tsx → export { default } from '@/features/Pages/PageLayout'
  • Feature (real implementation):
    src/features/Pages/PageLayout/ → Sidebar, DataSync, Body, Header, styles, etc.
  • Route (thin):
    src/routes/(main)/page/index.tsx → Import PageTitle, PageExplorerPlaceholder from @/features/Pages and @/features/PageExplorer; render with <PageTitle /> and placeholder.
  • Feature:
    Page list, actions, drawers, and hooks live under src/features/Pages/.

5. Progressive Migration (existing code)

Existing routes follow this structure unevenly: some are already thin segment files (e.g. src/routes/(main)/page/, src/routes/(main)/settings/), while others still hold logic or a features/ folder under src/routes/. Check the route's current state instead of assuming.

When touching an old route that still has logic or features/ inside src/routes/:

  1. Prefer adding new code in src/features/<Domain>/ and importing from routes.
  2. For larger refactors, move existing route-only logic into the right feature and then thin out the route files (re-export or compose from features).
  3. Use git mv when moving files so history is preserved.

6. Reference Structure (after Phase 1)

Route (thin):

src/routes/(main)/page/
├── _layout/index.tsx   → re-export or compose from @/features/Pages/PageLayout
├── index.tsx          → import from @/features/Pages, @/features/PageExplorer
└── [id]/index.tsx     → import from @/features/Pages, @/features/PageExplorer

Feature (implementation):

src/features/Pages/
├── index.ts            → export PageLayout, PageTitle
├── PageTitle.tsx
└── PageLayout/
    ├── index.tsx       → Sidebar + Outlet + DataSync
    ├── DataSync.tsx
    ├── Sidebar.tsx
    ├── style.ts
    ├── Body/           → list, actions, drawer, etc.
    └── Header/         → breadcrumb, add button, etc.

Router config continues to point at route paths (e.g. @/routes/(main)/page, @/routes/(main)/page/_layout); route files then delegate to features.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

acceptance

FreeSummary

Use this skill to verify a delivery by exercising the real product through a suitable surface such as a CLI, web app, desktop app, or iOS Simulator. It helps create or find a verification plan, capture evidence, and publish an acceptance report.

  • Verifying a completed product change
  • Collecting screenshots or other evidence
  • Creating a verification plan and report
lobehub/lobehub83.5KUpdated Oct 26, 2026

Use this skill to add an AI model provider to LobeHub across runtime integration, model cards, settings, branding, documentation, and acceptance checks. It also covers provider documentation updates and distinguishes new providers from models added to an existing provider.

  • Integrating a new AI model provider into LobeHub
  • Updating setup or usage documentation for a provider
  • Checking whether a model needs a new provider integration
lobehub/lobehub83.5KUpdated Oct 26, 2026

add-setting-env

FreeSummary

Use this skill to add a server-side environment variable that sets a default for a user setting. It walks through validation, server configuration and store merging, plus updating the example environment file and self-hosting documentation.

  • Adding an environment variable for a setting default
  • Wiring a new setting domain into server config
  • Documenting a new variable for self-hosting
lobehub/lobehub83.5KUpdated Oct 26, 2026

Provides guidance for registering hooks around an agent’s execution steps, tool calls, human approvals, context compression, and sub-agent calls. Hooks can observe events, supply local mock tool results, or use webhooks to allow or deny tool results under configured policies.

  • Observing agent steps and tool errors
  • Mocking tool results during local execution
  • Requiring webhook decisions before or after a tool call
lobehub/lobehub83.5KUpdated Oct 26, 2026

agent-signal

FreeSummary

Build event-driven background workflows for agents by defining sources, interpreting them as signals, and running actions through policies. It also guides asynchronous handoffs, deduplication, scoped execution, completion receipts, and observability.

  • Adding a new event source to an agent workflow
  • Routing interpreted signals to concrete background actions
  • Enqueuing work for asynchronous processing
lobehub/lobehub83.5KUpdated Oct 26, 2026

Use this skill to run end-to-end acceptance checks for bot channels in real desktop chat apps. It drives apps such as Discord, Slack, Telegram, WeChat, Lark, and QQ to send a message, wait for a response, and capture the result.

  • Checking a bot response in a real Discord channel
  • Testing chat behavior through Slack or Telegram
  • Capturing a bot interaction in a native macOS app
lobehub/lobehub83.5KUpdated Oct 26, 2026

See all skills by lobehub

Report a problem with this skill