Architecture
This developer reference explains how Brand Navigation integrates with Discourse, renders navigation and validates settings. For installation and configuration, see the Administrator guide.
Render boundary
Section titled “Render boundary”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.
Components
Section titled “Components”brand-navigationinitializer: 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-navigationlibrary: audience and safe-link policy.
Configuration model
Section titled “Configuration model”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.
Intentional constraints
Section titled “Intentional constraints”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.