logo
BETA

Dynamic Navigation Architecture

The core exposes a lightweight NavigationService that features use to register documentation sections and items during module initialization. The layout reads that registry to render sidebars and other navigation widgets. The general rule is: register navigation entries where the feature is implemented, and render them centrally.

Core concepts

  • RouterModule — provides the runtime registry.
  • NavigationService — API to register sections and items.
  • AutoRegisterModule — optional base helper for feature modules.

How to compute the active item (recommended)

To prevent a flash of incorrect sidebar state during client-side hydration, compute the active item on the server. In your controller compute the current request path (for example from the request URL) and call the core helper that returns the registry already decorated with an active flag on the matching item. Pass those sections down to the view so the initial HTML already reflects which link is active.

The snippet below shows a minimal controller pattern: determine the current path and ask the navigation service for decorated sections. This keeps the layout simple — it only reads item.active when rendering links.

TypeScript
1// features/eagles/eagles.controller.ts2import { Controller, Get, Req } from '@nestjs/common';3import { JsxRender } from '@harpy-js/core';4import EaglesPage from './views/eagles-page';5import DashboardLayout from '../../layouts/dashboard-layout';6import { NavigationService } from '@harpy-js/core';7import { getDictionary } from '../../i18n/get-dictionary';89@Controller('docs')10export class EaglesController {11  constructor(private readonly navigationService: NavigationService) {}1213  @Get('eagles')14  @JsxRender(EaglesPage, { layout: DashboardLayout })15  async eagles(16    @Req() req: FastifyRequest,17    @CurrentLocale() locale: string,18  ) {19    const currentPath = (req.originalUrl || req.url);2021    // Load the dictionary for the current locale22    const dict = await getDictionary(locale);2324    // Returns sections where items have '.active === true' for the25    // item that matches 'currentPath'.26    const sections = this.navigationService.getSectionsForRoute(currentPath);27    const activeItemId = this.navigationService.getActiveItemId(currentPath);2829    return { 30      sections,31      dict,32      locale,33      activeItemId,34    };35  }36}

Ordering and priorities

The navigation registry supports simple numeric ordering for both sections and items. When you register a section you may include an optional numeric order property; lower numbers appear earlier. Items within a section may also have an order which controls their position inside the section. This approach avoids imperative reordering in most cases and keeps ordering declarative and local to the feature.

Use small integers (for example 0, 10,100) to express priority. Reserve negative or very large numbers only for special cases. If a module registers later and you need to correct ordering at runtime, the core exposes a helper to move a section programmatically.

Declare section order

Example: register a section and provide an explicit order.

TypeScript
1// registering with an explicit order2navigationService.registerSection({3  id: 'core-concepts',4  title: 'Core Concepts',5  items: [],6  order: 0, // appears before sections with higher order or no order7});

Declare item priority inside a section

Example: add items to a section with explicit order values.

TypeScript
1// Registering items with explicit priority within a section2navigationService.addItemToSection('core-concepts', {3  id: 'routing',4  title: 'Routing',5  href: '/docs/routing',6  order: 10, // lower values appear earlier in the section7});89// Another item without order will appear after ordered items when10// those ordered items have lower numeric priorities.11navigationService.addItemToSection('core-concepts', {12  id: 'i18n',13  title: 'Internationalization',14  href: '/docs/internationalization',15});

Runtime adjustments

If a section needs to be promoted dynamically (for example admin-only or feature-flagged modules), use the core helper shown below.

TypeScript
1// packages/harpy-core/src/core/navigation.service.ts2// move an existing section to the front3navigationService.moveSectionToFront('core-concepts');

Illustrative examples

Below are short, focused snippets that demonstrate the recommended registration and server-side active-state pattern.

Feature module

TypeScript
1// features/eagles/eagles.module.ts2import { Module } from '@nestjs/common';3import { EaglesController } from './eagles.controller';4import { EaglesService } from './eagles.service';5import {6  NavigationService,7  NavigationRegistry,8  AutoRegisterModule,9} from '@harpy-js/core';1011@Module({12  controllers: [EaglesController],13  providers: [EaglesService],14})15export class EaglesModule extends AutoRegisterModule {16  constructor(17    navigationService: NavigationService,18    private readonly eaglesService: EaglesService,19  ) {20    super(navigationService);21  }2223  protected registerNavigation(navigation: NavigationRegistry): void {24    // Delegate to the feature service; AutoRegisterModule will call this25    this.eaglesService.registerNavigation(navigation);26  }27}

Feature service

TypeScript
1// features/eagles/eagles.service.ts2import { Injectable } from '@nestjs/common';3import { NavigationRegistry } from '@harpy-js/core';45@Injectable()6export class EaglesService {7  /**8   * Register feature documentation in the shared navigation9   * This is called during module initialization (OnModuleInit)10   */11  registerNavigation(navigationService: NavigationRegistry) {12    // Add this feature to the Core Concepts section13    navigationService.addItemToSection('core-concepts', {14      id: 'eagles',15      title: 'Eagles (Feature Example)',16      href: '/docs/eagles',17    });18  }19}

Controller / SSR

Compute the current path on the server and ask the navigation service for decorated sections (with active flags) so the sidebar is correct on first render.

TypeScript
1// features/eagles/eagles.controller.ts2import { Controller, Get, Req } from '@nestjs/common';3import { JsxRender } from '@harpy-js/core';4import EaglesPage from './views/eagles-page';5import DashboardLayout from '../../layouts/dashboard-layout';6import { NavigationService } from '@harpy-js/core';7import { getDictionary } from '../../i18n/get-dictionary';89@Controller('docs')10export class EaglesController {11  constructor(private readonly navigationService: NavigationService) {}1213  @Get('eagles')14  @JsxRender(EaglesPage, { layout: DashboardLayout })15  async eagles(16    @Req() req: FastifyRequest,17    @CurrentLocale() locale: string,18  ) {19    const currentPath = (req.originalUrl || req.url);2021    // Load the dictionary for the current locale22    const dict = await getDictionary(locale);2324    // Returns sections where items have '.active === true' for the25    // item that matches 'currentPath'.26    const sections = this.navigationService.getSectionsForRoute(currentPath);27    const activeItemId = this.navigationService.getActiveItemId(currentPath);2829    return { 30      sections,31      dict,32      locale,33      activeItemId,34    };35  }36}

Advanced tips

You can register sections early, use explicit ordering, or reorder at runtime. The examples below show each approach.

Item types & priorities

TypeScript
1// packages/harpy-core/src/core/types/nav.types.ts2export interface NavSection {3  id: string;4  title: string;5  items: NavItem[];6  order?: number; // lower numbers appear earlier7}

Move at runtime

TypeScript
1// packages/harpy-core/src/core/navigation.service.ts2// move an existing section to the front3navigationService.moveSectionToFront('core-concepts');

Benefits

  • No duplication: register once, render everywhere.
  • Scalable: features are self-contained.
  • SSR-friendly: compute active server-side to avoid hydration mismatch.