Dashboard deep-linking
Each dashboard has its own URL — paste, bookmark, or share it. Visit
/apps/mydash/{slug-chain} and the workspace opens directly on that
dashboard. Switching dashboards in the sidebar updates the URL via
pushState; back/forward navigates between dashboards.
URL format
/apps/mydash/ → resolver default
/apps/mydash/finance → top-level slug
/apps/mydash/finance/q1-roadmap → nested via slug-chain
/apps/mydash/finance/q1-roadmap/details → arbitrarily deep
Slugs use lowercase ASCII letters, digits, and dashes (per the dashboards capability's REQ-DASH-024). Multi-segment chains reflect parent → child hierarchy.
How it works
Inbound (URL → state)
- User visits
/apps/mydash/some-slug. - PHP catch-all route
page#deepLinkmatches and dispatches toPageController::deepLink($deepLink). - The controller resolves the slug-chain via
DashboardTreeService::resolvePath(). - If resolved AND the user can read the dashboard → uses it as active.
- If unresolved or no permission → silently falls back to the seven-step resolver, logs a warning. Never 404s — old bookmarks always land on something.
- Server pushes
deepLinkPath(the canonical slug-chain) into initial state. - Frontend reads
deepLinkPathandreplaceStates the URL to the canonical form. Stale URLs (parent renamed) get normalised in-place without a reload.
Outbound (state → URL)
- User clicks a row in the sidebar.
switchDashboard()updatesactiveDashboard.uuid.- A watcher on
activeDashboard.uuidfetches the canonical path viaGET /api/dashboards/{uuid}/path. - If non-empty →
history.pushState({uuid}, '', '/apps/mydash/' + path). - If empty (NULL slug — unaddressable dashboard) → no
pushState.
Back / forward
A popstate listener on Views.vue mount:
- Strips
/apps/mydash/fromwindow.location.pathname. - Calls
getDashboardByPath(path)to resolve. - Calls
switchDashboard(uuid).
Back / forward between dashboards then matches user expectation.
Why the catch-all is safe for /api/...
The route registration:
['name' => 'page#deepLink', 'url' => '/{deepLink}', 'verb' => 'GET',
'requirements' => ['deepLink' => '(?!api(?:/|$)).+']],
The negative-lookahead (?!api(?:/|$)) excludes any path starting
with api/ (or just api). The Newman integration suite asserts
GET /api/health returns 200 as a regression check.
The catch-all MUST stay LAST in appinfo/routes.php so every literal
/api/... route matches first.
Endpoints
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /api/dashboards/by-path/{path} | Resolve slug-chain → dashboard envelope (requirements path: .+) |
| GET | /api/dashboards/{uuid}/path | Compute canonical slug-chain for a dashboard UUID |
| GET | /{deepLink} (catch-all) | Renders the workspace page with the dashboard pre-resolved |
Tutorials
Spec reference
openspec/specs/dashboard-deeplinking/spec.md
— REQ-DDL-001 through REQ-DDL-007.