Malet Navigation Architecture
This document covers the architecture of the customizable Malet navigation system, which allows Malet Owners to rename, reorder, toggle, and extend the tabs shown on their Malet's navigation bar.
Overview
Every Malet displays a horizontal navigation bar below its header banner. The system supports three layers of tab resolution, evaluated in priority order:
| Priority | Source | Description |
|---|---|---|
| 1 (highest) | Owner NavConfig | Custom tabs persisted in the navConfig field on the Malet entity |
| 2 | Vertical Defaults | Hardcoded label maps (CATALOG_LABEL_MAP, BLOG_LABEL_MAP) keyed by verticalType |
| 3 | Platform Defaults | The standard six tabs: Home, Catalog, Blog, Community, Contact, About |
NOTE
When a Malet Owner saves a navConfig, it takes complete precedence over vertical defaults. There is no merge โ the saved config is the single source of truth for that Malet's nav bar.
Backend Schema
NavTab Object
Stored as an embedded sub-document within the Malet entity. Defined in apps/malets/src/malet/attributes/nav.ts.
type NavTab {
key: String! # Internal identifier (e.g. "catalog", "custom-1")
label: String! # Display text (e.g. "Menu", "Portfolio")
route: String! # Sub-route path or full URL
position: Int! # Display order (0 = leftmost)
visible: Boolean! # Whether shown in the nav bar
isCustom: Boolean! # True for owner-created tabs
icon: String # Optional emoji identifier
}
NavConfig Object
Wrapper containing an ordered list of NavTab items:
type NavConfig {
tabs: [NavTab!]! # Max 10 (6 platform + 4 custom)
}
Input Types
NavTabInput and NavConfigInput mirror the object types with class-validator decorators:
| Field | Validation |
|---|---|
key |
@IsString(), @MaxLength(30) |
label |
@IsString(), @MaxLength(30) |
route |
@IsString(), @MaxLength(500) |
position |
@IsInt(), @Min(0), @Max(20) |
tabs (NavConfigInput) |
@ArrayMaxSize(10), @ValidateNested({ each: true }) |
Entity Integration
The navConfig field is added to the Malet entity as an optional embedded document:
@Field(() => NavConfig, {
nullable: true,
description: 'Navigation tab configuration (labels, ordering, visibility)',
})
@prop({ type: () => NavConfig, _id: false })
navConfig?: NavConfig;
Because UpdateMaletInput extends PartialType(MaletInputDTO), the navConfig field is automatically available in both create and update mutations.
GraphQL Queries
Fetching NavConfig
The GET_MALET_BY_HANDLE query includes the full navConfig fragment:
query GetMaletByHandle($handle: String!) {
malets(paging: { first: 1 }, filter: { maletHandle: { eq: $handle } }) {
edges {
node {
# ... other fields
navConfig {
tabs {
key
label
route
position
visible
isCustom
icon
}
}
}
}
}
}
Updating NavConfig
The UPDATE_MALET mutation accepts navConfig in its input payload and returns the updated config:
mutation UpdateMalet($id: ID!, $update: UpdateMaletInput!) {
updateMalet(id: $id, update: $update) {
id
navConfig {
tabs { key label route position visible isCustom icon }
}
}
}
Frontend Architecture
MaletNav Component
File: src/routes/[malet]/MaletNav.svelte
The component accepts two navigation-relevant props:
let {
verticalType = null, // For vertical-aware fallback labels
navConfig = null // Owner's saved NavConfig (if any)
} = $props();
Resolution Chain
navConfig?.tabs โ configuredLinks (filtered by visible, sorted by position)
โ (if null)
visibleTabs filter โ defaultLinks with vertical-aware labels
โ (if null)
defaultLinks (all 6 platform tabs)
Custom Tab Handling
Custom tabs with external URLs (http:// or //) render as standard <a> tags with:
target="_blank"andrel="noopener noreferrer"- A small external-link SVG icon
- Distinct styling class
.external-tab
Platform tabs render with data-sveltekit-noscroll for SvelteKit client-side navigation.
Route Resolution
Platform tab keys are mapped to sub-routes via ROUTE_MAP:
const ROUTE_MAP = {
home: '',
catalog: '/catalog',
blog: '/blog',
community: '/community',
contact: '/contact',
about: '/about'
};
NavEditor Component
File: src/routes/[malet]/manage/settings/NavEditor.svelte
Embedded in the owner's Settings page, this component provides a full navigation management UI.
Props
interface Props {
navConfig: NavConfig | null; // Current saved config
verticalType: string | null; // For generating smart defaults
onchange: (config: NavConfig | null) => void; // Callback on edits
}
Features
| Feature | Description |
|---|---|
| Reorder | โ/โ arrow buttons to change tab position |
| Rename | Inline editable label field (max 30 chars) |
| Toggle | ๐๏ธ button to show/hide individual tabs |
| Add Custom | Form to create new tabs with emoji icon, label, and URL |
| Remove | โ button on custom tabs only (platform tabs cannot be removed) |
| Live Preview | Dark preview strip simulating the rendered nav bar |
| Reset | "โบ Reset to Defaults" clears the config, reverting to vertical-aware defaults |
Tab Limits
- Platform tabs: 6 (Home, Catalog, Blog, Community, Contact, About) โ cannot be added or removed, only renamed, reordered, or hidden
- Custom tabs: Maximum 4 โ owner-created, can be fully managed
- Total maximum: 10 tabs (
@ArrayMaxSize(10)enforced server-side)
State Flow
NavEditor.onchange(config)
โ settings/+page.svelte: navConfig = config
โ handleSubmit(): includes navConfig in UPDATE_MALET payload
โ Backend: validates and persists to Malet document
โ GET_MALET_BY_HANDLE: returns updated navConfig
โ +layout.svelte: passes to MaletNav
โ MaletNav: renders configured tabs
Layout Integration
The [malet]/+layout.svelte passes navConfig from the loaded Malet data to MaletNav:
<MaletNav
verticalType={malet?.verticalType ?? malet?.verticalTypes?.[0] ?? null}
navConfig={malet?.navConfig ?? null}
/>
Vertical-Aware Defaults
When no navConfig is saved, the system falls back to two hardcoded label maps:
Catalog Label Map
| Vertical | Label |
|---|---|
| restaurant | Menu |
| photographer | Gallery |
| professional | Services |
| fashion | Collection |
| entertainment | Events |
| wellness | Memberships |
| culture | Exhibitions |
| tech | Products |
| arcade | Games |
| tour | Trips |
| author | Library |
Blog Label Map
| Vertical | Label |
|---|---|
| author | Articles |
| photographer | Stories |
| professional | Insights |
All other verticals use the default labels ("Catalog" and "Blog").
File Reference
| File | Description |
|---|---|
apps/malets/src/malet/attributes/nav.ts |
NavTab + NavConfig types and inputs |
apps/malets/src/malet/malet.entity.ts |
Malet entity (navConfig field) |
apps/malets/src/malet/dto/create-malet.input.ts |
Create/Update input DTO |
src/lib/queries/malet.ts |
GQL queries, mutations, TypeScript interfaces |
src/routes/[malet]/MaletNav.svelte |
Navigation bar component |
src/routes/[malet]/+layout.svelte |
Malet layout (passes navConfig prop) |
src/routes/[malet]/manage/settings/NavEditor.svelte |
Navigation customizer UI |
src/routes/[malet]/manage/settings/+page.svelte |
Owner settings page |
Related
- Owner Destinations โ The Deck, Register & Pricing โ Workspace taxonomy and Malet Owner funnel
- Settings Architecture โ Modular component architecture for the settings page
- Vertical Seeding Infrastructure โ Vertical type registry and seeding
- Onboarding & Malet Creation โ 5-step Malet creation wizard