Skip to content

Architecture

This developer reference explains how Brand Navigation integrates with Discourse, renders navigation and validates settings. For installation and configuration, see the Administrator guide.

The initializer registers the configured full-application outlet only when EmbedMode.enabled is false. The bar and mobile-menu components repeat the same guard so an already-registered component still refuses to render if the context changes.

EmbedMode comes from the supported discourse/lib/embed-mode module. No hostname, parent-frame, DOM selector, or CSS visibility test is used.

Classic /embed/comments discussions use Discourse’s separate server-rendered embed layout. That layout exposes embedded_header, not the normal above-site-header, below-site-header, or home-logo__before application outlets used here. Brand Navigation provides no embedded_header customization.

Together these boundaries keep site-global content out of both supported embed paths without touching topic content, navigation, sign-in, reply, like, quote, or composer behavior.

  • brand-navigation initializer: guarded bar outlet and left/right site-header link outlet registration.
  • BrandNavigationBar: desktop and optional mobile-bar boundary.
  • brand-navigation-menu: guarded compact mobile header entry.
  • BrandNavigationContent: shared brand and navigation presentation.
  • BrandNavigationHeaderLinks: alignment-specific lists rendered through Discourse’s supported header outlets.
  • BrandNavigationHeaderIcon: shared guarded direct-link presentation for icon-only, label-only, and combined header entries.
  • brand-navigation library: audience and safe-link policy.

navigation_items uses Discourse’s supported nested objects theme setting. Ordering is the editor’s list order. A top-level entry with children uses a native details submenu. When that entry also has a URL, its label remains a normal link and a separate summary caret controls the submenu; without a URL, the complete summary is the submenu control. Only one child level is represented by the schema. Top-level entries are grouped into logical left and right sections; CSS logical properties keep that layout usable in both left-to-right and right-to-left interfaces.

The objects editor uses the component-specific schema identity brand_navigation_item_v1. Its save-stay behavior requires that identity. The component-wide appearance and bundle panel requires a theme component with the five-setting Brand Navigation signature. This avoids repository-location and mutable-name coupling while failing closed for incomplete signatures; it is a product signature, not cryptographic identity. Bundle imports validate the complete portable schema, preflight every target, snapshot the submitted data, and submit one Discourse theme update rather than issuing a separate request for each setting. While a request is pending, conflicting controls are disabled. After success, the model and appearance controls reconcile from the submitted snapshot. This does not assert a server-side transaction guarantee; after a server error, reload the current settings before retrying.

Top-level items with the site_header surface are excluded from the bar and rendered as direct links in alignment-specific lists. The initializer uses api.renderAfterWrapperOutlet("home-logo", ...) for the left list and api.renderInOutlet("before-header-panel", ...) for the right list. The right list therefore appears before the native authentication controls, not after Sign Up and Log In. These positions follow the reading direction on RTL sites. Header entries require a URL and do not support children. header_presentation selects icon-only (the backward-compatible default), label-only, or icon-and-label rendering. Text links use a bounded auto-width control, inherit header colors, and retain their full accessible label when the visible text is truncated. A locally authored lexical component factory binds each alignment to its list; the list passes validated items to the shared header-link component without repository-specific owner state.

Each item can render as icon-and-label, label-only, or icon-only. Labels remain required and provide accessible names even when visually omitted. An icon-only bar or submenu entry with no usable icon falls back to visible label text. Icon-only site-header entries with unavailable icons do not render, avoiding an empty core-header control. Label-only entries do not wait for or subscribe to icon readiness. Combined entries retain text while waiting for their optional icon. Availability is determined from the symbols actually loaded in Discourse’s production SVG sprite; development-only icon-list metadata is not used. One shared MutationObserver watches Discourse’s primary Font Awesome sprite until it contains symbols, then notifies all live Brand Navigation consumers once and disconnects. This avoids per-icon network requests. Until the primary sprite is ready, icon-only bar/submenu entries expose their labels and icon-only header entries stay omitted. Destroyed components unsubscribe, and the tracked readiness update is scheduled after the active render transaction. A retained direct-header component synchronizes its subscription whenever its responsive eligibility is reevaluated, so becoming eligible before sprite readiness still receives the single shared notification. Audience/device-ineligible entries do not create subscriptions. Top-level and child items independently select both, desktop, or mobile device visibility. The components evaluate that setting against Discourse’s supported capabilities.isMobileDevice state and omit nonmatching items from rendering, so phone rotation cannot reclassify items. Component-wide responsive layout continues to use site.mobileView through the bar and menu boundaries.

Navigation sinks independently recheck URL safety and fail closed for malformed, protocol-relative, credential-bearing, non-HTTPS external, and script URLs. The optional local bundle validator reads bounded files and never connects to a forum. Browser import/export uses the authenticated Discourse administrator session and the component’s administration page. Import validation also enforces Discourse’s 524,288-byte serialized object-setting limit before any settings request is sent; the complete bundle retains its separate 1,000,000-byte file limit.

Native details/summary provides keyboard operation without a custom focus state machine. An explicit zero tabindex on each summary also makes that native control discoverable to the compact menu’s trigger-to-content focus selector. Brand Navigation permits one open submenu at a time and closes it after outside interaction or link selection. In the bar, Escape from an open submenu closes it and returns focus to its summary. In compact mobile-menu mode, Discourse owns Escape dismissal of the containing menu and returns focus to the header menu button. Internal and external URLs use the same validated setting; _blank links automatically receive noopener noreferrer.


Source on GitHub