fix(routing): redirect before rendering, not while rendering

/start and /login are pure forwarders, and both did their `await navigateTo()`
in `<script setup>` with no template at all. That await suspends the first
render and the navigation aborts it, so the address bar flips to the target
while nothing is ever painted — a dark, empty page until the visitor reloads.

That is what people hit coming from the website: logging in there sends an
already-signed-in user to /start, which redirects here, and here they landed on
nothing. Reloading worked, which is exactly the tell — a cold load of the
target has no suspended render to abort.

Both redirects moved into route middleware, which runs before the page renders,
and both pages keep a placeholder template so a component without one can never
be what is on screen. /start still passes its query string through.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
itsrubberduck
2026-07-30 08:55:03 +02:00
parent 49090d37f7
commit 3cd754962c
2 changed files with 42 additions and 20 deletions

View File

@@ -8,21 +8,33 @@ import { buildIssuerLoginUrl } from '~~/shared/utils/ssoHandoff'
* monorepo that path resolved to the website's login form. Since the split it
* resolved to nothing, so signing out dropped the user on a 404.
*/
const route = useRoute()
const config = useRuntimeConfig()
definePageMeta({
layout: false,
// Forwarding from middleware, not from setup: an `await navigateTo()` in
// setup aborts the render it suspends, which leaves a cold load sitting on a
// blank page instead of moving it along.
middleware: [
(to) => {
const config = useRuntimeConfig()
const issuer = String(config.public.authIssuer || '').replace(/\/+$/, '')
const raw = String(to.query.redirect || '/')
// Only same-origin paths — never bounce onward to an absolute URL from the query.
const target = raw.startsWith('/') && !raw.startsWith('//') ? raw : '/'
definePageMeta({ layout: false })
if (issuer) {
return navigateTo(buildIssuerLoginUrl(issuer, window.location.origin, target), {
external: true,
})
}
const issuer = String(config.public.authIssuer || '').replace(/\/+$/, '')
const raw = String(route.query.redirect || '/')
// Only same-origin paths — never bounce onward to an absolute URL from the query.
const target = raw.startsWith('/') && !raw.startsWith('//') ? raw : '/'
if (issuer) {
await navigateTo(buildIssuerLoginUrl(issuer, window.location.origin, target), { external: true })
}
else {
// AUTH_MODE=open: no login exists at all, every request is the local identity.
await navigateTo(target, { replace: true })
}
// AUTH_MODE=open: no login exists at all, every request is the local identity.
return navigateTo(target, { replace: true })
},
],
})
</script>
<template>
<!-- Never seen: the middleware above forwards before this page renders. -->
<div />
</template>

View File

@@ -1,12 +1,22 @@
<script setup lang="ts">
const route = useRoute()
definePageMeta({ layout: false })
// This page was /start in the monorepo and became the app's front page in the
// split (see index.vue). Bookmarks, the website's redirect and every link that
// predates the split still point at /start, so catch it here instead of
// showing a 404 — the app should not depend on the website being redeployed
// for its own old URLs to work.
await navigateTo({ path: '/', query: route.query }, { replace: true })
definePageMeta({
layout: false,
// The redirect has to happen in middleware, not in setup. `await navigateTo()`
// inside setup suspends the very first render, and the navigation it waits on
// aborts that render: the URL flips to `/` but nothing is ever painted, so the
// visitor sits on a blank page until they reload. Middleware runs before the
// page renders at all, which is the only place a pure redirect route belongs.
middleware: [to => navigateTo({ path: '/', query: to.query }, { replace: true })],
})
</script>
<template>
<!-- Never seen: the middleware above redirects before this page renders. It
exists so a component without a template can never be what is on screen. -->
<div />
</template>