Module

core.menu

Menu system for navigation and site structure.

Provides menu building from configuration, page frontmatter, and section hierarchy. Supports hierarchical menus, active state tracking, and i18n localization. Menus are built during content discovery and cached for template access.

Key Concepts:

  • Menu sources: Config files, page frontmatter, section structure
  • Hierarchical menus: Parent-child relationships with weight-based sorting
  • Active state: Current page and active trail tracking
  • i18n menus: Localized menu variants per language

Related Modules:

  • bengal.orchestration.menu: Menu building orchestration
  • bengal.core.site: Site container that holds menus
  • bengal.rendering.template_functions.navigation: Template access to menus

See Also:

  • bengal/core/menu.py: MenuItem class for menu item representation
  • bengal/core/menu.py: MenuBuilder class for menu construction

Classes

MenuItem dataclass
Represents a single menu item with optional hierarchy. Menu items form hierarchical navigation str…
5

Represents a single menu item with optional hierarchy.

Menu items form hierarchical navigation structures with parent-child relationships. Items can be marked as active based on current page URL, and support weight-based sorting for display order.

Creation:

Config file: Explicit menu definitions in bengal.toml
Page frontmatter: Pages register themselves via menu metadata
Section structure: Auto-generated from section hierarchy

Attributes

Name Type Description
name str

Display name for the menu item

url str

URL path for the menu item

weight int

Sort weight (lower values appear first)

parent str | None

Parent menu identifier (for hierarchical menus)

identifier str | None

Unique identifier (auto-generated from name if not provided)

icon str | None

Icon name from frontmatter (e.g., 'book', 'folder', 'terminal')

children list[MenuItem]

Child menu items (populated during menu building)

active bool

Whether this item matches the current page URL

active_trail bool

Whether this item is in the active path (has active child)

Relationships
  • Used by: MenuBuilder for menu construction - Used by: MenuOrchestrator for menu building - Used in: Templates via site.menu for navigation rendering

Methods 4

add_child
Add a child menu item and sort children by weight. Adds the child to the child…
1 None
def add_child(self, child: MenuItem) -> None

Add a child menu item and sort children by weight.

Adds the child to the children list and immediately sorts all children by weight (ascending). Lower weights appear first in the list.

Parameters 1
child MenuItem

MenuItem to add as a child

mark_active
Mark this item as active if URL matches current page. Recursively checks this …
1 bool
def mark_active(self, current_url: str) -> bool

Mark this item as active if URL matches current page.

Recursively checks this item and all children for URL matches. Sets activeflag if this item matches, andactive_trailflag if any child matches. URLs are normalized (trailing slashes removed) before comparison.

Parameters 1
current_url str

Current page URL to match against (will be normalized)

Returns

bool

True if this item or any child is active, False otherwise

reset_active
Reset active states for this item and all children. Recursively clears `active…
0 None
def reset_active(self) -> None

Reset active states for this item and all children.

Recursively clearsactiveandactive_trailflags. Called before each page render to ensure fresh state for active item detection.

to_dict
Convert menu item to dictionary for template access. Creates a dictionary repr…
0 dict[str, Any]
def to_dict(self) -> dict[str, Any]

Convert menu item to dictionary for template access.

Creates a dictionary representation suitable for JSON serialization and template rendering. Recursively converts children to dictionaries.

Returns

dict[str, Any]

Dictionary with name, url, icon, active, active_trail, and children fields. Children are recursively converted to dictionaries.

Internal Methods 1
__post_init__
Set identifier from name if not provided. Automatically generates a slug-like …
0 None
def __post_init__(self) -> None

Set identifier from name if not provided.

Automatically generates a slug-like identifier from the menu item name by lowercasing and replacing spaces/underscores with hyphens. This ensures every menu item has a unique identifier for parent-child relationships.

MenuBuilder
Builds hierarchical menu structures from various sources. Constructs menu hierarchies from config …
9

Builds hierarchical menu structures from various sources.

Constructs menu hierarchies from config definitions, page frontmatter, and section structure. Handles deduplication, cycle detection, and hierarchy building with parent-child relationships.

Creation:

Direct instantiation: MenuBuilder()
  • Created by MenuOrchestrator for menu building
  • Fresh instance created for each menu build

Attributes

Name Type Description
items

List of MenuItem objects (flat list before hierarchy building)

_seen_identifiers

Set of seen identifiers for deduplication

_seen_urls

Set of seen URLs for deduplication

_seen_names

Set of seen names for deduplication Behavior Notes: - Identifiers: Each MenuItem has an identifier (slug from name by default). Parent references use identifiers. - Cycle detection: build_hierarchy() detects circular references and raises ValueError when a cycle is found. Consumers should surface this early as a configuration error. - Deduplication: Automatically prevents duplicate items by identifier, URL, and name.

Relationships
  • Uses: MenuItem for menu item representation - Used by: MenuOrchestrator for menu building - Used in: Menu building during content discovery phase

Methods 4

add_from_config
Add menu items from configuration file. Parses menu configuration from bengal.…
1 None
def add_from_config(self, menu_config: list[dict[str, Any]]) -> None

Add menu items from configuration file.

Parses menu configuration from bengal.toml or config files and creates MenuItem objects. Skips duplicates automatically and logs debug messages for skipped items.

Parameters 1
menu_config list[dict[str, Any]]

List of menu item dictionaries from config file. Each dict should have: name, url, weight (optional), parent (optional), identifier (optional)

add_from_page
Add a page to menu based on frontmatter metadata. Creates a MenuItem from page…
3 None
def add_from_page(self, page: Any, menu_name: str, menu_config: dict[str, Any]) -> None

Add a page to menu based on frontmatter metadata.

Creates a MenuItem from page frontmatter menu configuration. Uses page's relative_url for menu item URL (baseurl applied in templates). Skips duplicates automatically.

Parameters 3
page Any

Page object with frontmatter menu configuration

menu_name str

Name of the menu (e.g., 'main', 'footer'). Currently used for logging, all menus share same builder

menu_config dict[str, Any]

Menu configuration dictionary from page frontmatter. Should have: name (optional, defaults to page.title), url (optional, defaults to page.relative_url), weight (optional), parent (optional), identifier (optional)

build_hierarchy
Build hierarchical tree from flat list with validation. Converts flat list of …
0 list[MenuItem]
def build_hierarchy(self) -> list[MenuItem]

Build hierarchical tree from flat list with validation.

Converts flat list of MenuItem objects into hierarchical tree structure based on parent-child relationships. Validates parent references and detects circular dependencies.

Process:

1. Create lookup map by identifier
2. Validate parent references (warn about orphaned items)
3. Build parent-child relationships
4. Detect cycles (raises ValueError if found)
5. Return root items (items with no parent)
Returns

list[MenuItem]

List of root MenuItem objects (no parent) with children populated. Empty list if no items or all items have parents.

mark_active_items
Mark active items in menu tree based on current page URL. Recursively marks me…
2 None
def mark_active_items(self, current_url: str, menu_items: list[MenuItem]) -> None

Mark active items in menu tree based on current page URL.

Recursively marks menu items as active if their URL matches the current page URL. Also marks items in the active trail (items with active children). Resets all active states before marking to ensure clean state.

Parameters 2
current_url str

Current page URL to match against (will be normalized)

menu_items list[MenuItem]

List of root MenuItem objects to process (hierarchical tree)

Internal Methods 5
__init__
0 None
def __init__(self) -> None
_is_duplicate
Check if an item is a duplicate based on identifier, URL, or name. Checks agai…
3 bool
def _is_duplicate(self, item_id: str | None, item_url: str, item_name: str) -> bool

Check if an item is a duplicate based on identifier, URL, or name.

Checks against previously seen identifiers, URLs, and names to prevent duplicate menu items. An item is considered duplicate if any of these match a previously added item.

Parameters 3
item_id str | None

Item identifier (if any). None is valid (not checked).

item_url str

Item URL (normalized, trailing slash removed).

item_name str

Item name (lowercased for case-insensitive comparison).

Returns

bool

True if duplicate found (identifier, URL, or name matches), False otherwise

_track_item
Track an item to prevent future duplicates. Adds the item's identifier, URL, a…
1 None
def _track_item(self, item: MenuItem) -> None

Track an item to prevent future duplicates.

Adds the item's identifier, URL, and name to the seen sets for duplicate detection. Called automatically when items are added via add_from_config() or add_from_page().

Parameters 1
item MenuItem

MenuItem to track

_has_cycle
Detect circular references in menu tree using DFS. Uses depth-first search to …
3 bool
def _has_cycle(self, item: MenuItem, visited: set[str], path: set[str]) -> bool

Detect circular references in menu tree using DFS.

Uses depth-first search to detect cycles in parent-child relationships. A cycle exists if an item appears in its own descendant chain.

Parameters 3
item MenuItem

Current menu item being checked

visited set[str]

Set of all visited identifiers (for optimization)

path set[str]

Current path identifiers from root to current item (for cycle detection)

Returns

bool

True if cycle detected (item appears in its own descendant chain), False otherwise

Algorithm:

  • If item.identifier in path: cycle detected
  • Add item to path and visited
  • Recursively check all children
  • Return True if any child has cycle

_get_depth
Get maximum depth of menu tree from this item. Recursively calculates the maxi…
1 int
def _get_depth(self, item: MenuItem) -> int

Get maximum depth of menu tree from this item.

Recursively calculates the maximum depth of the menu tree starting from the given item. Used for logging and validation.

Parameters 1
item MenuItem

Root menu item to calculate depth from

Returns

int

Maximum depth as integer:

  • 1: Item has no children
  • 2: Item has children but no grandchildren
  • N: Maximum depth of deepest descendant