feat(router): move stackiq off hash routing to clean path URLs - #899
Merged
Conversation
Stackiq is one of seven fleet apps still serving its SPA behind a `#`. This is the pilot for moving all of them: the source change is a single line, and everything else here is the test surface that assumed hashes. Verified BEFORE switching, because history mode fails at the SERVER when the AppHost SPA catch-all is missing (fleet #133 is why apps fell back to hash in the first place): /apps/stackiq/organisaties, /contracten and /organisaties/abc-123 all already returned 200 with the app shell, so the catch-all is present for this app. What moved with it: tests/e2e/spec-coverage/_helpers.ts gotoAppRoute built `${APP_BASE}#${route}` tests/e2e/manifest-pages.spec.ts same URL construction tests/e2e/spec-coverage/catalog-ratings.spec.ts 2 hash deep-links tests/e2e/smoke/app-mounts.spec.ts `/stackiq/#/organisaties` -> a real sub-path The helper docblock explaining vue-router 4's hash-relative `createHref` is rewritten rather than deleted: the id-based nav selector it defends is deliberately KEPT, because identifying the nav by a stable handle instead of an href format the router owns is what makes it survive this change. Verified in the browser against the published @conduction/nextcloud-vue (USE_LOCAL_LIB=false): /apps/stackiq/ -> 200, page id Dashboard, no hash /apps/stackiq/organisaties -> 200, page id Organisaties, no hash /apps/stackiq/contracten -> 200, page id Contracten, no hash RELOAD on /organisaties -> 200, still Organisaties That reload is the point: it is the case hash mode existed to avoid, and it is served by the catch-all rather than 404ing. Zero JS errors. The Playwright smoke project passes on both routes, including the organisations sub-route now that it is a real path. eslint exits 0 and webpack compiles.
History mode broke deep links on the /index.php/... URL form. Nextcloud serves the same app under BOTH /apps/stackiq/... and /index.php/apps/stackiq/..., but generateUrl() returns only the form the instance is configured for. Arriving on the other form left the path outside the router base, vue-router could not resolve it, and the catch-all redirected to '/' -- the visitor landed on the Dashboard with no error and the deep link was silently swallowed. Measured before the fix: /apps/stackiq/komplianties -> Compliance /index.php/apps/stackiq/komplianties -> Dashboard <- silently wrong Hash routing never had this: the route travelled in the fragment, so the path prefix was irrelevant. This is the one real regression the switch introduced, and it would have applied to every app in the rollout. The e2e suite navigates via /index.php/..., which is exactly how it was caught -- two index specs failed while manual browsing on the pretty URL looked fine. Now both forms resolve, with the path preserved.
Contributor
Quality Report — ConductionNL/stackiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue-demi | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ❌ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 130/130 | |||
| npm | ✅ | ✅ 711/711 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-09-01 06:15 UTC
Download the full PDF report from the workflow artifacts.
This was referenced Sep 1, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pilot for moving the fleet's seven hash-routed apps to clean path URLs. The source change is two lines; everything else here is what the switch turned out to require.
Verified BEFORE switching
History mode fails at the server when the AppHost SPA catch-all is missing (fleet #133 is why apps fell back to hash). Probed first:
/apps/stackiq/organisaties,/contractenand/organisaties/abc-123already returned 200 with the app shell.routes.php—Routes::standard()installs the catch-all without the string appearing. That grep named the wrong app: it flagged zaakafhandelapp (which is fine) and cleared larpinq, whose sub-paths actually 404.The regression the e2e caught, and the fix
Two index specs failed reproducibly while manual browsing looked fine. Cause:
/apps/stackiq/komplianties/index.php/apps/stackiq/kompliantiesNextcloud serves the app under both forms, but
generateUrl()returns only the one the instance is configured for. Arriving on the other left the path outside the router base, vue-router could not resolve it, and the catch-all redirected to/— the deep link silently swallowed, no error. Hash routing never had this, because the route travelled in the fragment.routerBase()now derives the base from the URL actually being served, falling back togenerateUrl(). This would have applied to every app in the rollout, so the pilot earned its keep.Test surface that moved with it
gotoAppRoute()and three specs built URLs as`${APP_BASE}#${route}`; the smoke spec listed/stackiq/#/organisaties. All now use real paths. The helper docblock about vue-router 4's hash-relativecreateHrefis rewritten rather than deleted — the id-based nav selector it defends is kept deliberately, because identifying the nav by a stable handle rather than an href format the router owns is what survives this change.Verification
manifest-pages.spec.ts: 18/18 pass (was 16/18 before the base fix, 18/18 ondevelopment)./,/organisaties,/contracteneach resolve to the correctdata-page-id; reload on a sub-path returns 200 and stays put — the case hash mode existed to avoid. Both URL forms now work. Zero JS errors.eslintexits 0,prettier --checkclean, webpack compiles.Rollout note
Safe to convert next (sub-paths verified serving the SPA): openregister, opencatalogi, zaakafhandelapp, pipelinq, keepiq. larpinq needs its catch-all added first — its sub-paths 404 today.