Building Navigation
Building Navigation
Recipes for building navigation menus from ModulaCMS routes. Routes are lightweight connectors between a slug and a content tree. The routes API returns the data needed to build nav menus, breadcrumbs, and sitemaps.
For background on how routes work, see routing.
Route Types
Routes in ModulaCMS have a slug, title, and status. The status field is a numeric flag:
| Status | Meaning |
|---|---|
0 |
Inactive (hidden) |
1 |
Active (visible) |
Admin routes are a separate system from content-facing routes. Use /api/v1/routes for public navigation and /api/v1/adminroutes for admin navigation.
List All Routes
curl:
curl http://localhost:8080/api/v1/routes \
-H "Authorization: Bearer YOUR_API_KEY"
Response:
[
{
"route_id": "01HXK4N2F8RJZGP6VTQY3MCSW9",
"slug": "homepage",
"title": "Home Page",
"status": 1,
"author_id": "01JMKW8N3QRYZ7T1B5K6F2P4HD",
"date_created": "2026-01-15T10:00:00Z",
"date_modified": "2026-01-15T10:00:00Z"
},
{
"route_id": "01HXK5P3G7SWAH2C6VRXE8Q1KN",
"slug": "about",
"title": "About Us",
"status": 1,
"author_id": "01JMKW8N3QRYZ7T1B5K6F2P4HD",
"date_created": "2026-01-15T10:05:00Z",
"date_modified": "2026-01-15T10:05:00Z"
}
]
Go SDK:
routes, err := client.Routes.List(ctx)
if err != nil {
// handle error
}
for _, r := range routes {
fmt.Printf("%s -> /%s\n", r.Title, r.Slug)
}
TypeScript SDK (read-only):
const routes = await client.listRoutes()
for (const r of routes) {
console.log(`${r.title} -> /${r.slug}`)
}
TypeScript SDK (admin):
const routes = await admin.routes.list()
Filter Routes by Status
Only show active routes in navigation.
Go SDK:
routes, err := client.Routes.List(ctx)
if err != nil {
// handle error
}
var activeRoutes []modula.Route
for _, r := range routes {
if r.Status == 1 {
activeRoutes = append(activeRoutes, r)
}
}
TypeScript SDK:
const routes = await client.listRoutes()
const activeRoutes = routes.filter(r => r.status === 1)
Build a Nav Menu from Routes
Map routes into a navigation structure suitable for rendering.
Go SDK:
type NavItem struct {
Label string
Href string
}
routes, err := client.Routes.List(ctx)
if err != nil {
// handle error
}
var nav []NavItem
for _, r := range routes {
if r.Status != 1 {
continue
}
nav = append(nav, NavItem{
Label: r.Title,
Href: "/" + string(r.Slug),
})
}
// Use nav to render your menu
for _, item := range nav {
fmt.Printf("<a href=\"%s\">%s</a>\n", item.Href, item.Label)
}
TypeScript SDK:
interface NavItem {
label: string
href: string
}
const routes = await client.listRoutes()
const nav: NavItem[] = routes
.filter(r => r.status === 1)
.map(r => ({
label: r.title,
href: `/${r.slug}`,
}))
// Render as HTML
const html = nav
.map(item => `<a href="${item.href}">${item.label}</a>`)
.join('\n')
Build a Hierarchical Nav Menu
Routes are flat, but you can use slug conventions to build nested navigation. For example, slugs like blog, blog/tutorials, and blog/news imply a hierarchy.
Go SDK:
type NavNode struct {
Label string
Href string
Children []*NavNode
}
routes, err := client.Routes.List(ctx)
if err != nil {
// handle error
}
// Build lookup by slug
nodes := make(map[string]*NavNode)
var roots []*NavNode
for _, r := range routes {
if r.Status != 1 {
continue
}
slug := string(r.Slug)
node := &NavNode{
Label: r.Title,
Href: "/" + slug,
}
nodes[slug] = node
// Find parent by trimming the last segment
lastSlash := strings.LastIndex(slug, "/")
if lastSlash > 0 {
parentSlug := slug[:lastSlash]
if parent, ok := nodes[parentSlug]; ok {
parent.Children = append(parent.Children, node)
continue
}
}
roots = append(roots, node)
}
TypeScript SDK:
interface NavNode {
label: string
href: string
children: NavNode[]
}
const routes = await client.listRoutes()
const nodes = new Map<string, NavNode>()
const roots: NavNode[] = []
for (const r of routes.filter(r => r.status === 1)) {
const node: NavNode = {
label: r.title,
href: `/${r.slug}`,
children: [],
}
nodes.set(r.slug, node)
const lastSlash = r.slug.lastIndexOf('/')
if (lastSlash > 0) {
const parentSlug = r.slug.substring(0, lastSlash)
const parent = nodes.get(parentSlug)
if (parent) {
parent.children.push(node)
continue
}
}
roots.push(node)
}
Ordered Admin Routes
Admin routes support server-side ordering via the ?ordered=true parameter. This reads each route's root content node "Order" field and sorts numerically.
curl:
curl "http://localhost:8080/api/v1/adminroutes?ordered=true" \
-H "Authorization: Bearer YOUR_API_KEY"
Go SDK:
// Admin routes use the AdminRoutes resource
adminRoutes, err := client.AdminRoutes.List(ctx)
if err != nil {
// handle error
}
TypeScript SDK (admin):
// Ordered admin routes for sidebar navigation
const adminRoutes = await admin.adminRoutes.listOrdered()
const adminNav = adminRoutes.map(r => ({
label: r.title,
href: `/admin/${r.slug}`,
}))
Get the Content Tree for a Route
Once you have a route slug, you can fetch its full content tree via the content delivery endpoint or the admin tree endpoint.
curl (public content delivery):
curl http://localhost:8080/api/v1/content/homepage
curl (admin tree):
curl http://localhost:8080/api/v1/admin/tree/homepage \
-H "Authorization: Bearer YOUR_API_KEY"
Go SDK:
// Public content
page, err := client.Content.GetPage(ctx, "homepage", "clean", "")
// Admin tree
tree, err := client.AdminTree.Get(ctx, "homepage", "")
TypeScript SDK:
// Public content
const page = await admin.contentDelivery.getPage('homepage')
// Admin tree
const tree = await admin.adminTree.get('homepage' as Slug)
Next Steps
- Fetching Content -- full content retrieval recipes
- Search and Filter -- query content by datatype and field values