Skip to content

Menu Management

FORGE provides a hierarchical navigation menu system that allows administrators to build and manage site navigation without code changes. Menus support multi-level nesting, visibility rules, translations, and dynamic rendering on the frontend.

Overview

The menu system powers dynamic navigation components such as the site header and footer. Administrators can create menu trees, set per-item visibility based on authentication status, reorder items via drag-and-drop, and manage translations for each language.

Database Schema

The menus table stores all navigation items:

ColumnTypeDescription
idUUIDPrimary key
titleVARCHAR(255)Display label
urlVARCHAR(500)Link target (absolute or relative path)
iconVARCHAR(100)Optional icon identifier
targetVARCHAR(10)Link target attribute (_self or _blank)
visibilityVARCHAR(10)Who can see this item: public, auth, or guest
translationsJSONBPer-language translated labels
parent_idUUID (nullable)Self-referencing foreign key for hierarchy
is_activeBOOLEANWhether the item is visible
sort_orderINTEGERDisplay ordering within the same level
deleted_atTIMESTAMPSoft delete timestamp
created_atTIMESTAMPRecord creation timestamp
updated_atTIMESTAMPLast update timestamp

Hierarchy

Menus use a self-referencing parent_id to build a tree structure. Top-level items have parent_id = NULL, and child items reference their parent.

Header Navigation
├── Home          (parent_id: NULL, sort_order: 1)
├── Products      (parent_id: NULL, sort_order: 2)
│   ├── Software  (parent_id: Products, sort_order: 1)
│   └── Hardware  (parent_id: Products, sort_order: 2)
├── About         (parent_id: NULL, sort_order: 3)
└── Contact       (parent_id: NULL, sort_order: 4)

Visibility Rules

Each menu item has a visibility field that controls who can see it:

ValueDescription
publicVisible to all visitors
authVisible only to authenticated users
guestVisible only to unauthenticated visitors

TIP

Use guest visibility for items like "Login" or "Register" that should disappear once the user is signed in. Use auth for items like "Dashboard" or "Profile".

Translations Object Structure

json
{
  "en": { "title": "About Us" },
  "ar": { "title": "من نحن" }
}

API Endpoints

MethodPathDescription
GET/api/menusReturns the full menu tree, filtered by visibility

The API returns menus as a nested tree structure. The response is automatically filtered based on the requesting user's authentication status.

Example Response

json
[
  {
    "id": "uuid-1",
    "title": "Home",
    "url": "/",
    "icon": null,
    "target": "_self",
    "sort_order": 1,
    "children": []
  },
  {
    "id": "uuid-2",
    "title": "Products",
    "url": "/products",
    "icon": "package",
    "target": "_self",
    "sort_order": 2,
    "children": [
      {
        "id": "uuid-3",
        "title": "Software",
        "url": "/products/software",
        "icon": null,
        "target": "_self",
        "sort_order": 1,
        "children": []
      }
    ]
  }
]

Admin Interface

The admin panel provides a tree-view menu editor:

  • Tree View -- Visual representation of the menu hierarchy
  • Drag-and-Drop -- Reorder items and change parent-child relationships by dragging
  • Add / Edit / Delete -- Inline forms for creating and modifying menu items
  • Visibility Control -- Set who can see each item (public, authenticated, or guest)
  • Translation Tabs -- Translate menu labels for each active language

WARNING

Deleting a parent menu item will cascade the soft delete to all child items. Restoring a parent does not automatically restore its children.

Frontend Components

FORGE generates dynamic navigation components that consume the menu API:

Renders the primary navigation bar with support for nested dropdowns:

tsx
// components/site-header.tsx
export function SiteHeader() {
  const { menus } = useMenus();

  return (
    <header>
      <nav>
        {menus.map((item) => (
          <NavItem key={item.id} item={item} />
        ))}
      </nav>
    </header>
  );
}

Renders footer navigation links in a flat or grouped layout:

tsx
// components/site-footer.tsx
export function SiteFooter() {
  const { menus } = useMenus();

  return (
    <footer>
      {menus.map((item) => (
        <FooterLink key={item.id} item={item} />
      ))}
    </footer>
  );
}

DynamicNav

A reusable navigation component that adapts to the menu data:

tsx
// components/dynamic-nav.tsx
export function DynamicNav({ location }: { location: "header" | "footer" }) {
  const { menus, isLoading } = useMenus(location);

  if (isLoading) return <NavSkeleton />;

  return (
    <nav aria-label={`${location} navigation`}>
      {menus.map((item) => (
        <MenuItem key={item.id} item={item} />
      ))}
    </nav>
  );
}

TIP

Menu data is fetched once on initial page load and cached. The frontend automatically displays the correct translation based on the active language.

Released under the MIT License.