Skip to content

Navigation — Improvement & Growth Plan

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

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.

  • 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 somethingNavigationHealthCheck 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 readscapabilities[] 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 shipscapell.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

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
  • 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 siteNavigationObserver::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 loadingNavigationItemsLoader::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/

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.
ItemBucketEffortImpactSection ref
Done/Shipped: Implement real NavigationHealthCheck probes. Evidence: storage, morph alias, main navigation, orphaned reference, and package-owned header hook diagnostics are covered.DoneMHigh2, 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.DoneMHigh2, 4
Add capell-app/core to composer requireDoneSMed2
Populate manifest capabilities[] + cacheSafety.invalidationSources[]DoneSHigh2, 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.LaterSMed1, 5
Rewrite marketplace summary + composer descriptionDoneSMed5
Add anonymous/non-admin public-output safety test for rendered menu HTMLDoneSHigh4
Done/Shipped: Move DB resolution out of frontend component render() into a resolver/composerDoneMHigh2, 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.DoneLHigh3
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.DoneMHigh3
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.DoneMMed3
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.DoneSMed3
Done/Shipped: Programmatic menu-handle registry (replace fixed NavigationHandle enum)DoneMMed3
Done/Shipped: Cross-request site/locale-scoped cached render model (hit 20ms budget)DoneMMed3, 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.DoneLMed3
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.DoneSLow2