| name | avo-navigation-search |
| description | Shape how people move through and find things in an Avo admin — the sidebar / profile / header menus in `config/initializers/avo.rb`, per-resource `self.search`, the Cmd+K global search palette, breadcrumbs, and keyboard shortcuts. Use when the user wants to reorder or group the sidebar, split resources into sections, add a link (docs/billing) or dashboard or action to the admin nav, hide a resource from the menu for non-admins, add a keyboard shortcut or command palette, make a resource searchable, let admins search users by email or across all records, add breadcrumbs to a custom page, or add a sign-out link to the profile menu — including product-shaped phrasings that never say "Avo": "organize the admin sidebar", "group my models into sections", "add a Cmd+K / command palette / global search", "make X searchable in the admin", "let staff look up users by email", "add a keyboard shortcut to jump to Orders", "collapse the menu sections by default". |
| allowed-tools | Read, Edit, Write, Glob, Grep, Bash, WebFetch |
| metadata | {"requires-gem":"avo-menu (https://avohq.io/addons/menu-editor), avo-advanced_search (https://avohq.io/addons/global-search) — the rest is Community"} |
Avo Navigation & Search
This skill owns everything about getting around an Avo admin and finding records in it: the three configurable menus, per-resource search, the global Cmd+K palette, breadcrumbs, and keyboard shortcuts.
Two files do almost all the work:
config/initializers/avo.rb — the menus (config.main_menu, config.profile_menu, config.header_menu), the global search hash (config.global_search), keyboard-shortcut master switches (config.hotkeys), and the starting breadcrumb (config.set_initial_breadcrumbs).
app/avo/resources/<name>.rb — per-resource self.search = { query: … } (what makes a resource searchable at all), plus self.hotkey and self.visible_on_sidebar on the resource class.
Licensing — state this up front, it changes what you can offer:
- The menu editor (
main_menu / profile_menu / header_menu DSL) is a paid add-on (avo-menu). Without it, Avo auto-generates the sidebar from registered resources and you tune the menu only through self.visible_on_sidebar / self.icon / self.hotkey on each resource.
- Global search (the Cmd+K palette) is a paid add-on (
avo-advanced_search). Without it, per-resource self.search still gives each resource its own Index search bar.
- Per-resource
self.search, breadcrumbs, and keyboard shortcuts are Community — always available.
If the user's install lacks the add-on, say so and fall back to the Community path rather than writing DSL that won't load.
Docs
Authoritative docs — fetch on demand rather than guessing, and verify every option name against the docs or the app's installed Avo source before writing it:
When this applies
Explicit (Avo named): "reorder the Avo sidebar", "add a main_menu section", "group resources in the Avo menu", "add a link_to to our docs in the sidebar", "hide TeamMembership from the Avo menu", "add self.search to the User resource", "set up the Avo global search / Cmd+K", "add a hotkey to a menu item", "add breadcrumbs to a custom Avo page", "add a sign-out link to the Avo profile menu".
Implicit (product-shaped, no mention of Avo): "organize the admin sidebar into sections", "group my models under headings", "put a billing link in the admin nav", "let non-admins not see the audit-log resource in the menu", "add a command palette / Cmd+K to the admin", "make users searchable in the admin", "let support staff look up a customer by email", "search across all records at once", "jump to Orders with a keyboard shortcut", "collapse the sidebar groups by default", "add a way to sign out from the profile dropdown".
Boundary: self.search lives on the resource file, so it overlaps the resources vertical — this skill owns search. For the resource file's other attributes (title, icon, includes, visible_on_sidebar semantics) cross-link avo-resources. For the search? / index? policy methods cross-link avo-authorization. For picking menu-item icon names cross-link avo-menu-icons; for menu/appearance styling cross-link avo-branding-appearance.
Workflow
- Read
config/initializers/avo.rb. Does it already define config.main_menu? If yes, the menu editor is in play (edit the DSL). If no, the sidebar is auto-generated — either propose adding config.main_menu (needs the avo-menu add-on) or tune per-resource attributes.
- Classify the request as menus (§Menus), search (§Search), breadcrumbs, or shortcuts. Many requests are one of these squarely; a few ("command palette that searches everything") touch both menus and search.
- Confirm the license for what you're about to write — menu DSL and global search both require paid add-ons (see above). Fall back to Community equivalents when the add-on isn't present.
- Fetch the matching doc page from the Docs list before writing DSL, and verify option names against it or the installed source.
- Edit the right file, preserving surrounding indentation and style. Menus and global search → the initializer;
self.search / self.hotkey → the resource file; breadcrumbs on a custom page → that page's controller action.
- Report what you changed and any add-on / policy / model prerequisite still needed (see §Report).
Menus
Avo has three menus, all built from the same DSL, all assigned a Proc in the initializer:
| Config key | Renders in | Item types it renders |
|---|
config.main_menu | The sidebar | All item types |
config.profile_menu | The profile widget (sidebar footer) | link_to only (sign-out auto-added) |
config.header_menu | The top navigation bar | link_to only |
Recommended hierarchy for main_menu: section → group → item. Sections are top-level containers with an icon header; groups are collapsable sub-categories inside them.
Avo.configure do |config|
config.main_menu = -> {
section "Resources", icon: "tabler/outline/building-store", collapsable: true do
group "Company", collapsable: true do
resource :projects do
link_to "First project", path: "/admin/resources/projects/1"
resource :tasks
end
resource :team, icon: "heroicons/outline/user-group"
resource :reviews, icon: "heroicons/outline/star"
end
end
section "Other", icon: "tabler/outline/dots", collapsable: true, collapsed: true do
link_to "Avo HQ", path: "https://avohq.io", target: :_blank
end
}
end
Item types (main_menu)
link_to "Docs", path: "https://…", target: :_blank — any path, internal or external (link is an alias). :_blank also renders an external-link icon.
resource :users — link to a resource's Index. Pass the plural symbol (:users) or the full class string ("Avo::Resources::User"). Unknown resources are silently skipped.
dashboard :sales — link to a dashboard by id or name.
page "Avo::Pages::Settings" and form "Avo::Forms::AppSettings" — pages/forms (needs the avo-forms add-on). Pass the class name as a String so it isn't autoloaded while the initializer parses.
board 1 — a kanban board by id (needs the avo-kanban add-on).
action Avo::Actions::ExportData, resource: :projects — triggers a standalone action's modal from the menu.
render "avo/sidebar/items/custom_tool" — a partial or render SomeComponent.new(...) for anything custom.
Change any item's label with label: (resource :posts, label: "News posts").
Add everything at once
section "App", icon: "heroicons/outline/beaker" do
group "Dashboards" do
all_dashboards
end
group "Resources" do
all_resources except: [:users, :orders]
end
group "All tools" do
all_tools
end
end
all_dashboards, all_pages, all_forms, all_boards, all_tools behave the same. all_resources respects authorization — it only shows resources whose policy index? passes, so make sure def index? is defined (see avo-authorization).
Trigger an action from the menu
class Avo::Actions::ExportData < Avo::BaseAction
self.name = "Export data"
self.standalone = true
end
config.main_menu = -> {
action Avo::Actions::ExportData, resource: :projects, label: "Export"
resource :projects do
action Avo::Actions::ExportData
end
}
Non-standalone actions are skipped with a log warning.
Visibility & authorization
visible: (boolean or block) is available on every item. The block gets current_user, context, params, and view_context (route helpers via view_context.main_app.…). A group auto-hides when every item inside it is invisible.
resource :audit_logs, visible: -> { current_user.admin? }
resource :team, visible: -> {
authorize current_user, Team, "index?", raise_exception: false
}
Icons, hotkeys, data, collapsing
icon: — on section and individual items only; not on group or on sub-items nested inside a resource block. Use "tabler/outline/…" (preferred) or "heroicons/outline/…". For resource items with no icon:, the resource class's self.icon is used. Choosing names → avo-menu-icons.
hotkey: "g p" — renders a <kbd> badge and registers a jump-to shortcut. Uses @github/hotkey syntax; space = a sequence ("g p" = press g then p). For resource items it falls back to the resource class's self.hotkey.
data: { turbo: false } or data: { turbo_method: :delete } — arbitrary data attributes.
collapsable: true (+ optional collapsed: true for a default-collapsed first visit) — on section and group. State is stored in the browser's Local Storage; a stored preference always beats collapsed:.
Sub-items
Pass a block to resource to nest child items under it. Any item type nests; a nested resource/dashboard/page/board/action resolves its own URL (only link_to needs an explicit path:), a nested action inherits the enclosing resource, and nested items render no icon. Avo highlights the active sub-item by longest-path match. Optionally wrap them in a subitems do … end block (purely for readability).
Profile menu & header menu
Both render link_to only — other item types are ignored.
config.profile_menu = -> {
link_to "Profile", path: "/profile", icon: "user-circle"
link_to "Sign out", path: main_app.destroy_user_session_path, icon: "user-circle", method: :post
}
config.header_menu = -> {
link_to "Docs", path: "https://docs.avohq.io", target: :_blank
link_to "Sign out", path: main_app.destroy_user_session_path, method: :delete
}
link_to supports method: and params: (honored in the profile and header menus) and title: (hover tooltip). For heavier custom content in the profile menu, eject _profile_menu_extra.html.erb with bin/rails generate avo:eject --partial :profile_menu_extra.
Community fallback (no avo-menu)
Without the menu editor the sidebar is auto-generated; control it from each resource file: self.visible_on_sidebar = false to hide, self.icon for the icon, self.hotkey for a jump shortcut. (These attributes belong to avo-resources; the sidebar effect is the navigation part.)
Search
Search is configured once per resource via self.search, and that one query: proc powers every surface: the Index search bar, the global Cmd+K palette, searchable association pickers, and kanban card pickers. Without self.search, a resource has no Index search bar and is skipped by global search.
class Avo::Resources::User < Avo::BaseResource
self.search = {
query: -> { query.ransack(first_name_cont: q, last_name_cont: q, m: "or").result(distinct: false) }
}
end
q — the stripped search string. (params[:q] for the raw value.)
query — the base scope with authorization scopes already applied; always search off it.
- Avo recommends ransack but it isn't mandatory — the proc can run any query. If using ransack, add
gem "ransack".
Search by email / across fields, and through associations
query.ransack(first_name_cont: q, last_name_cont: q, email_cont: q, m: "or").result(distinct: false)
To match on an associated model's columns, join it and prefix the ransack keys with the association name (assuming Application belongs_to :client):
class Avo::Resources::Application < Avo::BaseResource
self.search = {
query: -> {
query.joins(:client).ransack(
id_eq: q,
name_cont: q,
client_email_cont: q,
client_phone_number_cont: q,
m: "or"
).result(distinct: false)
}
}
end
Different query per surface (search_type)
One proc can branch on search_type to run a wider search in the global palette than in an association picker:
self.search = {
query: -> {
case search_type
when :global
query.ransack(first_name_cont: q, last_name_cont: q, email_cont: q, m: "or").result(distinct: false)
when :association
query.ransack(first_name_cont: q).result(distinct: false)
else
query.ransack(first_name_cont: q, last_name_cont: q, m: "or").result(distinct: false)
end
}
}
search_type is :resource, :global, or :association. It is injected only by the paid search layer (avo-advanced_search) — on a Community-only install the local is undefined and referencing it raises. The kanban card picker doesn't inject it (or a q local) either. Guard with defined?(search_type) when a Community install or the kanban picker might hit the proc; the kanban picker reads the term from params[:q].
Authorize search
Search obeys the search? policy method — if it returns false the resource is dropped from global search and its Index search bar is hidden.
class UserPolicy < ApplicationPolicy
def search?
true
end
end
(If search? is already used for something else, alias it via config.authorization_methods = { search: "avo_search?" }.) See avo-authorization.
Limit results
Avo caps each resource's results at config.search_results_count (default 8) — unless your proc already calls .limit(), in which case your limit wins. The dedicated global results page ignores the cap.
config.search_results_count = 16
Global search (Cmd+K palette) — paid add-on
Searches every resource that has self.search, all at once. Opened via the navbar trigger or Cmd+K / Ctrl+K. It also does direct ID lookup (paste a UUID, a prefixed id like plan_1234, or a plain number → a "Direct match" section) and shows a "Go to" navigation section of resource Index links.
config.global_search = {
enabled: true,
navigation_section: true,
search_on_type: true
}
Per-resource global-search tweaks live in the self.search hash:
hide_on_global: true — keep the resource's own Index search bar but drop it from the palette/direct-match/results page.
display_count: false — skip the "Users (8 of 21)" counting query (or a lambda) when it's expensive.
item: — customize how each result row renders (title/description/image/path); without it rows fall back to the record's title.
self.search = {
query: -> { query.ransack(name_cont: q, body_cont: q, m: "or").result(distinct: false) },
hide_on_global: false,
display_count: true,
item: -> {
{
title: "[#{record.id}] #{record.name}",
description: record.truncated_body,
image_url: main_app.url_for(record.cover_photo),
image_format: :rounded,
path: avo.resources_post_path(record, custom: "search")
}
}
}
Custom (non-ActiveRecord) providers
Back search with Elasticsearch etc. by returning an array of hashes from query: instead of a relation:
query: -> {
[
{ _id: 1, _label: "Record One", _url: "https://example.com/1", _description: "…", _avatar: "https://…", _avatar_type: :rounded }
].first(config_or_number)
}
Breadcrumbs (Community)
Avo builds a breadcrumb trail automatically for resource views. Two things you'd configure:
Change where every trail starts — config.set_initial_breadcrumbs in the initializer (runs in controller context; use the avo proxy for engine paths):
config.set_initial_breadcrumbs do
add_breadcrumb title: "Home", path: avo.root_path, icon: "tabler/outline/home"
add_breadcrumb title: "Team", path: avo.resources_teams_path
end
Add crumbs on a custom page — call add_breadcrumb in the page's controller action:
class Avo::ToolsController < Avo::ApplicationController
def custom_tool
add_breadcrumb title: "Custom tool", path: avo.custom_tool_path
end
end
add_breadcrumb options: title: (required), path: (omit → plain text), icon:, initials:, avatar:. Internal Avo links need the avo. prefix (Rails engine path rules).
Keyboard shortcuts (Community)
All shortcuts are on by default; each bound control shows a small <kbd> badge, and ? opens the reference modal for the current page. Shortcuts never fire while typing in an input/textarea/select/contenteditable.
config.hotkeys = {
enabled: true,
show_key_badges: true
}
Built-in highlights: Cmd/Ctrl+K global search · Shift+\ toggle sidebar · B go back · / focus Index search · C new record · A actions menu · V T / V G / V M switch table/grid/map view. (Full table on the keyboard-shortcuts doc page.)
Add your own on any control with a data-hotkey attribute (re-bound on every Turbo navigation):
<a href="/avo/posts/new" data-hotkey="c">New post</a>
<button data-hotkey="Meta+Enter Control+Enter">Save</button>
Jump-to-menu-item shortcuts go through the menu hotkey: option, or self.hotkey on the resource class (see §Menus).
Gotchas
- Menu editor and global search are paid add-ons. Writing
config.main_menu or relying on the Cmd+K palette on an install without avo-menu / the global-search add-on won't work — check first, and fall back to per-resource self.visible_on_sidebar / self.search (both Community).
all_resources respects index? authorization. A resource missing from the menu is usually a policy returning false, not a bug.
- Menu
action items must be standalone. self.standalone = true is required (the menu has no selected record); others are skipped with a log warning. Top-level action also needs resource:; nested it's inherited.
profile_menu and header_menu render only link_to. resource, dashboard, action, etc. are silently ignored there. The profile menu adds sign-out for you.
icon: isn't universal. It works on section and individual items, not on group or on sub-items nested inside a resource block.
- Ransack v4+ needs an allowlist. Add
ransackable_attributes (and often ransackable_associations) to any model you search, or the query raises.
search_type is undefined on Community-only installs and in the kanban picker. It's injected by the paid search layer for the index/global/association surfaces only. Guard with defined?(search_type); the kanban picker reads params[:q] and detects the board via params[:for_kanban_board].
- Custom array-result providers aren't auto-capped.
config.search_results_count only applies to relations without their own .limit() — cap arrays yourself with .first(N).
.limit() in your query: proc always wins over config.search_results_count.
search_on_type can't be a lambda. Any Proc is truthy, so it behaves as ; use a plain boolean. ( and do accept lambdas.)
Report
When done, tell the user:
- Which file(s) you edited (full paths) — the initializer, which resource file(s), and/or which custom-page controller.
- What you changed: menu structure (sections/groups/items added, reordered, or hidden), which
self.search procs you added and the fields/associations they cover, global-search settings, breadcrumbs, or hotkeys.
- Any add-on the change depends on (
avo-menu for the menu DSL, avo-advanced_search for Cmd+K global search) and whether it appears to be installed.
- Any prerequisite still needed: a policy method (
index? for all_resources, search? for search — see avo-authorization), ransackable_attributes on the searched model, self.standalone = true on a menu action, or gem "ransack".
- Note when you fell back to a Community path (per-resource
visible_on_sidebar / self.search) because an add-on wasn't present.