# Navigation — Improvement & Growth Plan

> Package: capell-app/navigation · Kind: package · Tier: free · Product group: Capell Foundation · Bundle: foundation · Status: Draft

## 1. Snapshot

Navigation owns editable, site- and language-scoped menu trees stored as a JSON `items` column on the `navigations` table (one model, `src/Models/Navigation.php`), plus the page-level "which menus reference this page" panel, sync/replication actions, and the foundation header render hook. Surfaces are `admin`, `frontend`, `console`. Key Actions: `BuildNavigationRenderModelAction` (view model assembly + per-request memoization), `BuildPageNavigationReferencesAction` (reverse lookup), `AddPageToNavigationAction` / `RemovePageFromNavigationAction` (item mutation with row locking), `ReplicateSiteNavigationsAction` (site clone). Frontend component rendering is now thin: `View/Components/Header/MainNavigation` and `View/Components/Menu` pass context into Blade, and `View/Composers/NavigationRenderModelComposer` resolves the navigation and hydrated `NavigationRenderData` for the view. Composer and manifest dependencies now both declare `capell-app/admin`, `capell-app/core`, and `capell-app/frontend`. Current marketplace summary (verbatim): "Build and manage multilingual, per-site menus visually — link to any page or URL, nest dropdowns, and render them in your theme with one tag. Active-state, publish windows, and site cloning included." Manifest now promotes the marketplace card plus the styled create/edit and site relation-manager captures; empty index, unrelated page-tab, and frontend dashboard captures remain runner evidence until recaptured.

## 2. Improvements (existing functionality)

- **Done/Shipped: Replaced the JSON `LIKE` page-references lookup.** `BuildPageNavigationReferencesAction` now loads candidate navigations through an indexed `whereExists` against `navigation_page_references`, then keeps the decoded item-tree guard so stale indexed references cannot show false positives in the page form. Coverage asserts no `items LIKE`, no standalone pivot scan, the exists lookup is used, and stale rows are ignored. — `src/Actions/BuildPageNavigationReferencesAction.php`, `tests/Integration/Actions/BuildPageNavigationReferencesActionTest.php` — M
- **Done/Shipped: moved frontend navigation resolution out of component `render()`.** `Menu::render()` and `Header\MainNavigation::render()` now return views with context only. `NavigationRenderModelComposer` resolves `Frontend::site()/page()/language()`, loaded site-domain relations, `NavigationLoader`, and `BuildNavigationRenderModelAction`, then injects `NavigationRenderData` into the public views. The Blade views render nothing unless the composer provides a non-empty render model. — `src/View/Components/Menu.php`, `src/View/Components/Header/MainNavigation.php`, `src/View/Composers/NavigationRenderModelComposer.php`, `src/Providers/NavigationServiceProvider.php` — M
- **Done/Shipped: Make the declared health check actually check something** — `NavigationHealthCheck` now probes storage tables, the model morph alias, required main navigations, orphaned pageable references, and Navigation's own Foundation header render-hook scenarios. Coverage proves unrelated or partial header hooks fail instead of producing a false-green critical check. — `src/Health/NavigationHealthCheck.php`, `src/Support/RenderHooks/RegisterFoundationHeaderNavigationHook.php`, `tests/Feature/Health/NavigationHealthCheckTest.php` — M
- **Shipped: populate the manifest fields downstream tooling reads** — `capabilities[]` now declares the package contract (`navigation-menu-builder`, `navigation-page-field`, `navigation-render-model`, `navigation-site-replication`) and `cacheSafety.invalidationSources[]` now records the Navigation, Page, and Site invalidation triggers. Evidence: `capell.json`, `tests/Unit/PackageMetadataTest.php`. — S
- **Shipped: add `capell-app/core` to composer `require`** — the manifest and composer metadata both declare the direct core dependency already used by the package's `Capell\Core\…` imports. This package does not ship a `composer.local.json`, so no package-local overlay needed alignment. Evidence: `composer.json`, `capell.json`, `tests/Unit/PackageMetadataTest.php`. — S
- **Reopened: Reconcile marketplace screenshots with what ships** — `capell.json` now promotes only buyer-worthy Navigation captures: the marketplace card, create/edit form, and site relation-manager in light/dark modes. `docs/screenshots.json` still keeps the 5 runner capture surfaces and maps each one to its committed `darkScreenshotPath`, but index/page-tab/frontend screenshots need populated recapture before marketplace promotion. Evidence: `capell.json`, `docs/screenshots.json`, `tests/Unit/PackageMetadataTest.php`. — S
- **Done/Shipped: dropped the per-page `SiteDomain` fallback query in the item loader.** `NavigationItemsLoader::getPagesByMorphKey()` now preloads the loaded pages' `(site_id, language_id)` domain scopes once into a keyed map, then attaches the matching `SiteDomain` relation from memory when a page URL differs from the active domain. — `src/Support/Loader/NavigationItemsLoader.php` — S
- **Done/Shipped: reverse-references are memoized for the admin panel.** `BuildPageNavigationReferencesAction` uses a `REQUEST_CACHE_KEY`, stores lookups in request attributes, exposes `flushRequestCache()`, and has coverage proving repeated page form renders reuse the cached references. — `src/Actions/BuildPageNavigationReferencesAction.php`, `tests/Integration/Actions/BuildPageNavigationReferencesActionTest.php` — S

## 3. Missing Features (gaps)

With manifest `capabilities[]` now populated, the items below remain real feature gaps relative to navigation norms.

- **Done/Shipped: role/permission/auth-conditional item visibility.** Navigation items now keep the existing `is_visible` boolean and can additionally set `data.visibility` to everyone, guests, authenticated users, Gate ability, or host-provided role. Ability checks use `Gate::allows()`, role checks call `hasRole()` only when the authenticated user model provides it, and missing ability/role configuration hides the item. — `src/Enums/NavigationItemVisibility.php`, `src/Support/Loader/NavigationItemsLoader.php`, `src/Filament/Configurators/Navigations/DefaultNavigationConfigurator.php`
- **Done/Shipped: breadcrumb builder and component.** `BuildNavigationBreadcrumbsAction` extracts the active branch from `NavigationRenderData`, and `<x-capell-navigation::breadcrumbs>` renders a public breadcrumb trail using the same composer-backed navigation context as `<x-capell-navigation::menu>`. — `src/Actions/BuildNavigationBreadcrumbsAction.php`, `src/View/Components/Breadcrumbs.php`, `resources/views/components/breadcrumbs.blade.php`, `src/Providers/NavigationServiceProvider.php`
- **Done/Shipped: richer external links and targets.** `NavigationItemType` now has a first-class `ExternalLink` case, `NavigationItemTarget` supports `_self`, `_blank`, and `_parent`, the admin item form exposes an optional `rel` attribute, render models carry `rel`, and public menu Blade emits safe `rel="noopener noreferrer"` defaults for new-tab external links when no custom rel is set. Remaining richer-item depth: dividers and arbitrary content-model links. — `src/Enums/NavigationItemType.php`, `src/Enums/NavigationItemTarget.php`, `src/Filament/Configurators/Navigations/DefaultNavigationConfigurator.php`, `src/Actions/BuildNavigationRenderModelAction.php`, `resources/views/components/menu-items.blade.php`, `resources/views/components/header/menu/item.blade.php`
- **Done/Shipped: Mega-menu / column layout support.** Parent items can opt into `dropdown_layout=mega`, choose one to four child-link columns, and show an optional panel heading/description/link in the header dropdown renderer. The render model passes only public layout keys through the existing view-data allow-list. — `src/Enums/NavigationDropdownLayout.php`, `src/Filament/Configurators/Navigations/DefaultNavigationConfigurator.php`, `src/Actions/BuildNavigationRenderModelAction.php`, `resources/views/components/header/menu/dropdown.blade.php`
- **Done/Shipped: per-menu / per-handle programmatic registration.** `NavigationHandleRegistry` now preserves the built-in enum defaults while allowing themes and packages to register extra handles such as `account-menu` or `mega-header`. The admin form reads registry options, while stored navigation keys remain strings so existing enum callers and custom handles can coexist. — `src/Support/Registry/NavigationHandleRegistry.php`, `src/Filament/Configurators/Navigations/DefaultNavigationConfigurator.php`
- **Done/Shipped: opt-in starts-with active-state mode.** Navigation items now default to exact active matching but can opt into `starts_with` via `data.active_mode` from the admin item form. Link/external-link items use the mode directly, and page items keep exact record matching first before falling back to their resolved URL for section highlighting. — `src/Enums/NavigationItemActiveMode.php`, `src/Support/Loader/NavigationItemsLoader.php`, `src/Filament/Configurators/Navigations/DefaultNavigationConfigurator.php`
- **Done/Shipped: public render-model cache (cross-request).** `BuildNavigationRenderModelAction` now keeps the existing per-request cache and adds a short cross-request cache for anonymous render contexts only, keyed by navigation/site/language/page/domain and relevant `updated_at` stamps. Authenticated requests remain request-local so role/ability visibility decisions cannot leak between users. Navigation saves and page URL changes flush the tag-backed shared render cache where supported. — `src/Actions/BuildNavigationRenderModelAction.php`, `src/Observers/NavigationObserver.php`, `src/Providers/NavigationServiceProvider.php`

## 4. Issues / Risks

- **Done/Shipped: admin page reverse lookup avoids JSON `LIKE`.** The page form now uses the `navigation_page_references` lookup table through a correlated `exists` query and still validates decoded navigation items to guard stale index rows. — `src/Actions/BuildPageNavigationReferencesAction.php`, `tests/Integration/Actions/BuildPageNavigationReferencesActionTest.php`
- **Done/Shipped: critical health check is real.** Navigation Diagnostics now fail on missing tables, missing morph alias, missing main navigation coverage, orphaned pageable references, and missing/partial package-owned header render-hook registration. — `src/Health/NavigationHealthCheck.php`, `tests/Feature/Health/NavigationHealthCheckTest.php`
- **Done/Shipped: public components no longer lazy-load in `render()`.** The view composer owns navigation/render-model resolution and the components return views with context only, keeping the component render methods free of loader/action calls. — `src/View/Components/Menu.php`, `src/View/Components/Header/MainNavigation.php`, `src/View/Composers/NavigationRenderModelComposer.php`
- **`NavigationLoader::getNavigation()` falls back to `whereNull('language_id')` site-wide query then in-PHP sort** — when `siteOnlyFallback` is true and no language match is found, it loads _all_ published navigations for the site into memory and sorts in PHP. Fine for a handful of menus, but unbounded by design. — `src/Support/Loader/NavigationLoader.php`
- **Observer cache invalidation can fan out to every site** — `NavigationObserver::clearCache()` merges `Site::query()->pluck('id')` (all sites) into the keys-to-clear set whenever `site_id` is null on either the current or original record. A single global-navigation save invalidates per-site keys for the entire install. Correct, but a thundering-herd risk on large multi-site installs; the manifest sets `queueInvalidation: true` but `invalidationSources: []` is undocumented. — `src/Observers/NavigationObserver.php`
- **No cross-request cache safety test for anonymous output** — Capell requires tests proving anonymous/non-admin render safety for cache/render changes. There are frontend render tests (`tests/Feature/Frontend/Page/PageNavigationTest.php`, `PagePerformanceNavigationTest.php`) and the boundary arch test (`tests/Arch/NavigationBoundaryTest.php`) only asserts it doesn't import downstream packages and uses strict equality. There is no test asserting the rendered menu HTML leaks no admin internals (model IDs, field paths, `pageable_type`, signed editor URLs) for an anonymous visitor. The admin page panel does emit `NavigationResource::getUrl('edit')` links — confirm that Blade (`page/navigations.blade.php`) is never reachable on a public surface. — `tests/Arch/NavigationBoundaryTest.php`, `resources/views/components/page/navigations.blade.php`
- **`NavigationItemData` is mutated in place during loading** — `NavigationItemsLoader::load()` reassigns `$this->navigation->items` and flips `$item->active = true` on shared data objects; `BuildNavigationRenderModelAction` memoizes by a cache key that includes `spl_object_id` for unsaved navigations. Mutating model state during a read path is a subtle cache-coherence footgun if the same `Navigation` instance is rendered in two contexts. — `src/Support/Loader/NavigationItemsLoader.php`
- **i18n**: item-type and target labels are translated (`getLabel()` via `__()`), and the lang file is `resources/lang/en/generic.php`. Coverage looks thin (one `generic.php`); verify the header Blade's hardcoded `aria-label`/`sr-only` strings all route through `__()` (several do, e.g. `capell-navigation::generic.main_navigation`). — `resources/lang/en/generic.php`
- **Test gaps**: well-covered — all 6 Actions, both loaders, observer, adapters, content-graph extractor, both commands, policy, registry, and Filament resources/relation-manager all have Pest files. Notably **untested**: cross-request public-output safety (above), the `LIKE`-fragment edge cases in `BuildPageNavigationReferencesAction` (e.g. a `pageable_id` substring collision like 12 matching 123 — the `%"pageable_id":12%` fragment can false-positive before the in-PHP `navigationContainsRecord` filter rescues it), and active-state for external links. — `tests/`

## 5. Marketplace & Positioning

This is a free, foundation-bundle, first-party package — correctly positioned: navigation is non-negotiable infrastructure every Capell site needs, so it belongs in the free foundation tier as a platform credibility piece, not a revenue line.

- **Shipped marketplace `summary`**: "Build and manage multilingual, per-site menus visually — link to any page or URL, nest dropdowns, and render them in your theme with one tag. Active-state, publish windows, and site cloning included." Evidence: `capell.json`, `tests/Unit/PackageMetadataTest.php`.
- **Shipped composer `description`**: "Site- and language-scoped navigation menus for Capell: visual menu builder, page & link items, nested dropdowns, active-state rendering, publish scheduling, and multi-site replication." Evidence: `composer.json`, `tests/Unit/PackageMetadataTest.php`.
- **free/bundle vs premium**: keep free/foundation. The premium upsell is _not_ this package — it is a future `navigation-pro` (mega-menus, role-conditional visibility, breadcrumbs-as-a-service, A/B menu variants) that _depends on_ this one's contract. That dependency only works if `capabilities[]` is populated (Section 2/4).
- **Screenshot/media coverage**: manifest now advertises the marketplace card plus the create/edit and site relation-manager captures. The empty index, unrelated page-tab, and frontend dashboard captures were demoted and must be recaptured with real menu data before completion.
- **Platform-pitch contribution**: navigation is the proof that Capell's "editors manage structure without touching theme code" promise is real. The README's "Why It Helps Your Capell Workflow" framing is the right pitch; surface it in the marketplace summary, not just the README.
- **Keywords/tags (8–12)**: `navigation`, `menus`, `menu-builder`, `multilingual`, `multi-site`, `dropdown`, `mega-menu`, `breadcrumbs`, `active-state`, `cms`, `filament`, `foundation`.

## 6. Prioritized Roadmap

| Item                                                                                                                                                                                                                                                                                                             | Bucket | Effort | Impact | Section ref |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ | ------ | ----------- |
| Done/Shipped: Implement real `NavigationHealthCheck` probes. Evidence: storage, morph alias, main navigation, orphaned reference, and package-owned header hook diagnostics are covered.                                                                                                                         | Done   | M      | High   | 2, 4        |
| Done/Shipped: Replace JSON `LIKE '%...%'` reverse-lookup with pivot/indexed lookup. Evidence: reverse lookup now uses `whereExists` against `navigation_page_references`, avoids standalone pivot scans, and ignores stale indexed references.                                                                   | Done   | M      | High   | 2, 4        |
| Add `capell-app/core` to composer `require`                                                                                                                                                                                                                                                                      | Done   | S      | Med    | 2           |
| Populate manifest `capabilities[]` + `cacheSafety.invalidationSources[]`                                                                                                                                                                                                                                         | Done   | S      | High   | 2, 4        |
| Recapture populated index/page-reference/frontend-menu screenshots before promoting the remaining Navigation product media. Evidence: weak captures were demoted while styled create/edit and site relation-manager captures remain promoted. Deferred until styled runner recapture; no implementation blocker. | Later  | S      | Med    | 1, 5        |
| Rewrite marketplace `summary` + composer `description`                                                                                                                                                                                                                                                           | Done   | S      | Med    | 5           |
| Add anonymous/non-admin public-output safety test for rendered menu HTML                                                                                                                                                                                                                                         | Done   | S      | High   | 4           |
| Done/Shipped: Move DB resolution out of frontend component `render()` into a resolver/composer                                                                                                                                                                                                                   | Done   | M      | High   | 2, 4        |
| Done/Shipped: Add role/permission/auth-conditional item visibility. Evidence: item visibility supports everyone, guests, authenticated users, Gate ability, and host-provided `hasRole()` role checks from the loader and admin form.                                                                            | Done   | L      | High   | 3           |
| Done/Shipped: Add breadcrumb builder action + component. Evidence: `BuildNavigationBreadcrumbsAction` extracts the active render branch, and `<x-capell-navigation::breadcrumbs>` renders it through the existing composer-backed context.                                                                       | Done   | M      | High   | 3           |
| Done/Shipped: Add ancestor/starts-with active-state mode for section highlighting. Evidence: items can opt into `starts_with` active matching while exact remains the default; page items preserve record matching before URL-prefix fallback.                                                                   | Done   | M      | Med    | 3           |
| Done/Shipped: First-class external-link item type + `rel`/`_self`/`_parent` targets. Evidence: `ExternalLink` item type, expanded target enum, admin rel field, render-model rel data, and public Blade rel output are wired.                                                                                    | Done   | S      | Med    | 3           |
| Done/Shipped: Programmatic menu-handle registry (replace fixed `NavigationHandle` enum)                                                                                                                                                                                                                          | Done   | M      | Med    | 3           |
| Done/Shipped: Cross-request site/locale-scoped cached render model (hit 20ms budget)                                                                                                                                                                                                                             | Done   | M      | Med    | 3, 4        |
| Done/Shipped: Mega-menu / multi-column dropdown primitive. Evidence: parent items now expose `dropdown_layout=mega`, column count, and optional intro panel fields, and the header dropdown renderer switches to a valid HTML div/panel plus grid list for mega menus.                                           | Done   | L      | Med    | 3           |
| Done/Shipped: Preload site domains once in item loader (drop per-page fallback query). Evidence: fallback `pageUrl` domains are loaded into a keyed `(site_id, language_id)` map before the page loop.                                                                                                           | Done   | S      | Low    | 2           |