K KodePressdocumentation
Laravel 11 · MySQL 8 Specification Built 2026-10-10

07 — Navigation and mega menu#

Navigation is where "editable by a non-technical person" usually breaks down. KodePress treats menus as data (a tree of items with rules) and mega-menu panels as content (a block tree), which is why a mega panel is built with the page editor rather than a bespoke screen.

7.1 Menus#

Menus in the sidebar lists every menu with its location and item count.

Concept Rule
Multiple menus any number per site
Locations header, topbar, footer-1..footer-4, mobile, none
Location is a hint a menu_slot block names a menu explicitly; location is the fallback used by header presets and by the "use the header menu" default
Depth 3 levels (depth 0, 1, 2). The editor refuses a deeper drop and says why
Mobile menu if a menu has location = mobile it is used on small screens; otherwise the header menu is reused

Menu screen layout: the tree on the left (drag and drop, collapse/expand, multi-select), the selected item's settings in the middle, and a live preview of the rendered menu on the right. The preview uses the real renderer inside the real header, so the owner sees the actual result.

Bulk operations: select several items to move them under a new parent, delete them, toggle new_tab, or set visibility in one go.

Menu-level settings (menus.settings): desktop alignment (left/centre/right), submenu open trigger (hover/click), submenu animation (none/fade/slide), submenu indicator (chevron on/off), mobile behaviour (accordion/drawer), and auto_children (see 7.4).

Import/export: a menu exports as JSON (items with targets resolved to slugs, not ids) and imports into another menu or another site, re-resolving slugs and reporting anything it could not match. Copy menu duplicates within the site.

7.2 Item types#

Type Target Renders as
page target_id -> pages.id (type page/landing) link to the page path, resolved at render
post target_id -> pages.id (type post) link to the post path
category target_id -> categories.id link to the category archive
tag target_id -> tags.id link to the tag archive
url url external or internal link, optional new_tab, rel=nofollow option
anchor url = #section-id, optional page smooth-scroll link; if the anchor is on another page it links to path#anchor
phone url = tel:... tap-to-call, phone icon by default
email url = mailto:... mail link
button any of the above targets styled as a theme button, not a plain link
dropdown none a label that opens its children and is not itself a link
mega none, plus a mega tree opens a mega panel (7.5)
divider none a separator inside a dropdown
heading none a non-clickable group label inside a dropdown or mega panel

Per-item settings:

Setting Column Notes
Label label defaults to the target title, then becomes independent
Icon icon from the bundled icon set, searchable picker
Badge badge_text, badge_color e.g. New, Hot
Open in new tab new_tab
CSS class css_class Advanced
Highlight colour highlight_color token first, hex under Advanced
Visibility visibility JSON see 7.3
Active is_active hide an item without deleting it

7.3 Visibility rules#

menu_items.visibility:

{
  "auth":    "any",                 // any | guest | user
  "roles":   ["admin", "editor"],   // empty = no role restriction (only meaningful with auth=user)
  "devices": ["desktop", "mobile"], // empty = all
  "locales": ["bn"]                 // empty = all
}

Evaluation:

Because auth-dependent menus cannot be shared with anonymous visitors, a menu containing any auth != any item makes its containing header cacheable: false for logged-in users only: anonymous visitors still get the cached file (built with the guest view of the menu), and logged-in users render dynamically. See 13-caching-and-performance.md.

7.4 Auto-sync and integrity#

Event Effect on menus
Page title changes items whose label was never edited by hand follow the new title; edited labels stay
Page slug or parent changes nothing to update — targets are ids and URLs are resolved at render
Page unpublished the item renders only for users who can see drafts; for visitors it is dropped, and the menu screen flags it Not published
Page or category deleted the item is flagged Broken link in the menu screen and in Dashboard -> Needs attention; publicly the item is dropped
Category gains children + auto_children on children appear as a sub-menu automatically, ordered by categories.position

auto_children is a per-item toggle (settings.auto_children) that expands a category item with its child categories, optionally with a View all first entry. Manually added children and auto-children can coexist; manual ones come first.

Active-item highlighting: the renderer marks an item aria-current="page" when its resolved URL equals the current path, and adds kp-menu-ancestor to its ancestors. Because HTML is cached per path, the active state is baked into each page's cached copy — correct by construction, with no JavaScript.

7.5 Mega menus#

Any item at depth 0 has a Mega menu toggle. Turning it on opens the panel editor: the same section/column/block editor used for pages, in the mega context.

Ready-made panel layouts#

Offered when the toggle is first turned on (each is a templates row with kind = section and category mega):

Layout Structure
3-column links three columns, each a heading + icon_list
Links + featured card two link columns + one column with an image, heading, text and button
Categories + latest posts one column of category links + a post_list filtered by the hovered category
Tabbed panel a left column of headings acting as tabs, right column swapping content
Full width a single full-bleed section, free-form

The tabbed layout is the only one with behaviour beyond layout: tab state is Alpine-local, panels are all rendered and toggled with hidden, so there is no request on hover.

Panel settings (menu_items.settings)#

Setting Values Default
panel_width container, full, item (aligned to the trigger) container
background token name, or image with background_media_id surface
columns 1–6 (a convenience that sets the section columns) 3
open_on hover, click hover
animation none, fade, slide fade
max_height px, 0 = auto; scrolls past it 0
mobile_mode accordion, drawer, hidden accordion

Behaviour#

7.6 Rendering a menu#

MenuSlot block (menu_id, depth limit, style overrides)
  -> MenuRenderer::render(menu, ctx)
       |-- load items once, ordered by (parent_id, position), build the tree in PHP
       |-- resolve each target to a URL (page path, category path, raw url, tel:, mailto:)
       |-- drop items failing server-side visibility; add device CSS classes
       |-- mark active / ancestor against ctx.path
       |-- for each item with a non-empty `mega`: PanelRenderer::render(tree, ctx)
       +-- emit nav > ul > li markup with ARIA

One query loads all items for a menu; nothing in the loop touches the database. Target resolution batches its lookups (all page ids in one query, all category ids in one query). A menu with 60 items and 4 mega panels costs 4 queries, and in practice zero, because the result lands in the cached page HTML.

Markup contract:

<nav class="kp-menu kp-menu--header" aria-label="Main menu">
  <ul class="kp-menu__list">
    <li class="kp-menu__item kp-menu__item--has-panel">
      <button class="kp-menu__link" aria-expanded="false" aria-controls="kp-panel-12">
        Products <span class="kp-menu__chevron" aria-hidden="true"></span>
      </button>
      <div class="kp-panel" id="kp-panel-12" hidden> ... panel tree ... </div>
    </li>
    <li class="kp-menu__item">
      <a class="kp-menu__link" href="/about" aria-current="page">About</a>
    </li>
  </ul>
</nav>

7.7 Permissions#

Action Admin Editor Writer Designer
Create / delete a menu yes no no yes
Add / edit / reorder items yes yes no yes
Edit a mega-menu panel layout yes no no yes
Change menu-level settings yes no no yes

An Editor can keep navigation current (add a link to a new page) without being able to restructure the design. This split is enforced by MenuPolicy, not by hiding buttons.

7.8 Tests for this document#

Test Asserts
three_level_menu_renders nested ul depth is 3 and no deeper
depth_limit_enforced an attempt to nest at depth 3 is rejected
visibility_role_rule_hides_item_server_side the HTML for a guest contains no trace of the item
device_rule_adds_class_not_removal the item is present with kp-hide-mobile
mega_panel_renders_desktop_and_mobile the same tree produces a panel and an accordion
empty_mega_behaves_as_dropdown no panel markup, children render as a dropdown
page_rename_updates_unedited_label and leaves a hand-edited label alone
deleted_target_drops_item_publicly and flags it in the admin
active_item_marked aria-current="page" on the matching item, ancestor class on parents
menu_renders_in_constant_queries query count does not grow with item count
editor_cannot_edit_mega_layout MenuPolicy denies it, 403

KodePress documentation · generated from the Markdown sources by tools/build-docs-site.py · internal preview, not indexed.