docs: document new nav content extensibility

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2026-02-15 12:21:54 +01:00
parent 1fcffa72e3
commit a2133bebd4
3 changed files with 52 additions and 35 deletions
@@ -686,7 +686,7 @@ createApp({
#### App Root Sidebar
New apps feature a built-in sidebar extension which is created by using the `NavContentBlueprint` in `src/modules/nav/Sidebar.tsx`. The default implementation of the sidebar in this blueprint will render some items explicitly in different groups, and then render the rest of the items which are the other `NavItem` extensions provided by the system.
New apps feature a built-in sidebar extension which is created by using the `NavContentBlueprint` in `src/modules/nav/Sidebar.tsx`. The default implementation of the sidebar in this blueprint will render some items explicitly in different groups, and then render the rest of the items. Nav items are auto-discovered from page extensions registered under `app/routes` (no explicit `NavItemBlueprint` required), with metadata from page config, nav item extensions, or plugin defaults.
In order to migrate your existing sidebar, you will want to create an override for the `app/nav` extension. You can do this by copying the standard of having a `src/modules/nav/` folder, which can contain an extension which you can install into the `app` in the form of a `module`.
@@ -702,38 +702,56 @@ export const navModule = createFrontendModule({
Then in the actual implementation for the `SidebarContent` extension, you can provide something like the following, where you implement the entire `Sidebar` component.
The component receives a `navItems` prop with `take(id)` and `rest()` methods for placing specific items in custom positions. Use `navItems.take('page:home')` to take a specific item by extension ID (e.g. for a header slot), and `navItems.rest()` to get all remaining items.
```tsx title="in packages/app/src/modules/nav/Sidebar.tsx"
import { NavContentBlueprint } from '@backstage/plugin-app-react';
export const SidebarContent = NavContentBlueprint.make({
params: {
component: ({ items }) => (
<Sidebar>
<SidebarLogo />
<SidebarGroup label="Search" icon={<SearchIcon />} to="/search">
<SidebarSearchModal />
</SidebarGroup>
<SidebarDivider />
<SidebarGroup label="Menu" icon={<MenuIcon />}>
...
</SidebarGroup>
<SidebarGroup label="Plugins">
<SidebarScrollWrapper>
{/* Items in this group will be scrollable if they run out of space */}
{items.map((item, index) => (
<SidebarItem {...item} key={index} />
))}
</SidebarScrollWrapper>
</SidebarGroup>
</Sidebar>
),
component: ({ navItems }) => {
// Take specific items for custom placement
const home = navItems.take('page:home');
// Get all remaining items
const rest = navItems.rest();
return (
<Sidebar>
<SidebarLogo />
{home && (
<SidebarItem to={home.href} text={home.title} icon={home.icon} />
)}
<SidebarGroup label="Search" icon={<SearchIcon />} to="/search">
<SidebarSearchModal />
</SidebarGroup>
<SidebarDivider />
<SidebarGroup label="Menu" icon={<MenuIcon />}>
...
</SidebarGroup>
<SidebarGroup label="Plugins">
<SidebarScrollWrapper>
{rest.map(item => (
<SidebarItem
key={item.node.spec.id}
to={item.href}
text={item.title}
icon={item.icon}
/>
))}
</SidebarScrollWrapper>
</SidebarGroup>
</Sidebar>
);
},
},
});
```
The `items` property is a list of all extensions provided by the `NavItemBlueprint` that are currently installed in the App. If you don't want to auto populate this list you can simply remove the rendering of that `SidebarGroup`, but otherwise you can see from the above example how a `SidebarItem` element is rendered for each of the items in the list.
The deprecated `items` prop (a flat list compatible with `<SidebarItem {...item} />`) remains supported for backward compatibility. If you don't want to auto-populate the list, simply remove the rendering of that `SidebarGroup`.
You might also notice that when you're rendering additional fixed icons for plugins that these might become duplicated as the plugin provides a `NavItem` extension and you're also rendering one in the `Sidebar` manually. In order to remove the item from the list of `items` which is passed through, we recommend that you disable that extension using config:
You might also notice that when you're rendering additional fixed icons for plugins (e.g. Search in a dedicated group) these might become duplicated, since that page is also included in `navItems.rest()`. To exclude an item from the remaining list, use `navItems.take('page:search')` before calling `navItems.rest()`. Items that have been taken will not appear in `rest()`.
You can also use the old `NavItemBlueprint`-based nav item extensions to disable items from the nav bar, these can be disabled in config without affecting the page itself:
```yaml title="in app-config.yaml"
app:
@@ -742,15 +760,6 @@ app:
- nav-item:catalog: false
```
You can also determine the order of the provided auto installed `NavItems` that you get from the system in config. The below example ensures that the `catalog` navigation item will proceed the `search` navigation item when being passed through as the `item` prop.
```yaml title="in app-config.yaml"
app:
extensions:
- nav-item:catalog
- nav-item:search
```
#### App Root Routes
Your top-level routes are the routes directly under the `AppRouter` component with the `<FlatRoutes>` element. In a small app they might look something like this: