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:
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
title | VARCHAR(255) | Display label |
url | VARCHAR(500) | Link target (absolute or relative path) |
icon | VARCHAR(100) | Optional icon identifier |
target | VARCHAR(10) | Link target attribute (_self or _blank) |
visibility | VARCHAR(10) | Who can see this item: public, auth, or guest |
translations | JSONB | Per-language translated labels |
parent_id | UUID (nullable) | Self-referencing foreign key for hierarchy |
is_active | BOOLEAN | Whether the item is visible |
sort_order | INTEGER | Display ordering within the same level |
deleted_at | TIMESTAMP | Soft delete timestamp |
created_at | TIMESTAMP | Record creation timestamp |
updated_at | TIMESTAMP | Last 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:
| Value | Description |
|---|---|
public | Visible to all visitors |
auth | Visible only to authenticated users |
guest | Visible 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
{
"en": { "title": "About Us" },
"ar": { "title": "من نحن" }
}API Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/menus | Returns 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
[
{
"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:
SiteHeader
Renders the primary navigation bar with support for nested dropdowns:
// components/site-header.tsx
export function SiteHeader() {
const { menus } = useMenus();
return (
<header>
<nav>
{menus.map((item) => (
<NavItem key={item.id} item={item} />
))}
</nav>
</header>
);
}SiteFooter
Renders footer navigation links in a flat or grouped layout:
// 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:
// 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.