Developer Docs

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

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
}

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" and rel="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'
};

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