Vue SPA
Drive client-side sub-route navigation inside a Vue page using vue-router. Vue's SSR pipeline requires the router to be installed and resolved before renderToWebStream() runs — AbsoluteJS exposes a setupApp hook that opens that lifecycle window for you.
#How It Works
vue-router's SSR contract is strict: app.use(router); router.push(url); await router.isReady(); all have to run between createSSRApp() and renderToWebStream(). AbsoluteJS pages export a setupApp function that the handler invokes at exactly that point — that's the entire contract for wiring vue-router into AbsoluteJS SSR.
#Wildcard Route
Refresh on any sub-route hits the server with the actual URL. Register a wildcard pattern so the same handler responds for every URL the page's router knows about:
// backend/server.ts — register a wildcard route per page so refresh /
// deep-link on any sub-route hits the same handler. The native router
// then dispatches based on the URL.
new Elysia()
.get('/dashboard', dashboardHandler)
.get('/dashboard/*', dashboardHandler)
.listen(3000);#Page Handler
Pass request through to the page handler. The Vue handler doesn't merge it into props (vue-router needs the URL via router.push(), not via component props) — instead it forwards the URL into the page module's setupApp context:
// backend/server.ts
import { handleVuePageRequest } from '@absolutejs/absolute/vue';
import type VueSpa from '../frontend/vue/pages/VueSpa.vue';
const vueHandler = ({ request }: { request: Request }) =>
handleVuePageRequest<typeof VueSpa>({
headTag: generateHeadElement({
cssPath: asset(manifest, 'SpaCSS'),
title: 'AbsoluteJS SPA — Vue',
}),
indexPath: asset(manifest, 'VueSpaIndex'),
pagePath: asset(manifest, 'VueSpa'),
request, // ← passed into the page module's setupApp() hook
});
new Elysia()
.get('/vue', vueHandler)
.get('/vue/*', vueHandler);#setupApp Hook
Export a setupApp function from your page module. The Vue page handler invokes it after creating the Vue app and before rendering — pass the app to app.use(router) here:
// The Vue page handler invokes the page module's exported setupApp()
// hook between createSSRApp() and renderToWebStream(). This is the
// only place to install vue-router (or vue-i18n, or pinia, etc.) so
// the renderer sees the configured app.
// Signature:
type SetupApp = (
app: App,
ctx: {
url: string; // request pathname + search (server) / window URL (client)
isServer: boolean; // true on SSR, false on hydration
setRedirect: (
location: string,
status?: number // defaults to 302
) => void;
}
) => void | Promise<void>;true during SSR, false during client hydration. Use it to pick createMemoryHistory() vs createWebHistory(), and to gate router.push(url) + the redirect bridge to server-only.302.Important: await router.isReady() on both server and client. Awaiting only on the server causes a hydration mismatch — the active-link class on <RouterLink> flickers on the first render before the client router resolves.
#Page Component
The page is a Vue SFC. Define routes alongside the template; install the router via the exported setupApp:
<!-- frontend/vue/pages/VueSpa.vue -->
<script setup lang="ts">
import {
createMemoryHistory,
createRouter,
createWebHistory,
RouterLink,
RouterView,
} from 'vue-router';
const routes = [
{ path: '/vue', component: { template: '<h2>Home</h2>' } },
{ path: '/vue/settings', component: { template: '<h2>Settings</h2>' } },
{ path: '/vue/profile', component: { template: '<h2>Profile</h2>' } },
];
</script>
<template>
<nav>
<RouterLink to="/vue">Home</RouterLink>
<RouterLink to="/vue/settings">Settings</RouterLink>
<RouterLink to="/vue/profile">Profile</RouterLink>
</nav>
<RouterView />
</template>
<script lang="ts">
// vue-router's history mode differs between server (memory) and client
// (web). The router must be installed via app.use() AND awaited via
// router.isReady() BEFORE renderToWebStream — otherwise the active
// route doesn't resolve in time for SSR. AbsoluteJS exposes a
// setupApp() hook for exactly this lifecycle window.
import type { App } from 'vue';
import { applyVueRouterRedirect } from '@absolutejs/absolute/vue';
export const setupApp = async (
app: App,
{ url, isServer, setRedirect }: {
url: string;
isServer: boolean;
setRedirect: (location: string, status?: number) => void;
}
) => {
const router = createRouter({
history: isServer ? createMemoryHistory() : createWebHistory(),
routes,
});
app.use(router);
if (isServer) await router.push(url);
await router.isReady(); // ← required on BOTH sides for clean hydration
// Optional: convert vue-router redirects (from guards or redirect rules)
// into HTTP 302s instead of rendering the redirected route's HTML.
if (isServer) applyVueRouterRedirect(router, url, setRedirect);
};
</script>vue-router. Pair with createMemoryHistory() on the server and createWebHistory() on the client.createSSRApp() and renderToWebStream() — the only window where vue-router can be installed and resolved before SSR starts.#Redirects
vue-router supports redirects via beforeEach guards (return a string or route-object), redirect rules on a route, and manual router.push() from inside a guard. To turn any of these into an HTTP 302 during SSR, call the AbsoluteJS-shipped helper:
import { applyVueRouterRedirect } from '@absolutejs/absolute/vue';
// applyVueRouterRedirect compares the requested URL against
// router.currentRoute.value.fullPath after router.isReady(). If a
// vue-router guard or redirect rule sent the user somewhere else, it
// invokes setRedirect() so the page handler emits a 302 instead of
// rendering the redirected page's HTML.
export const setupApp = async (app, { url, isServer, setRedirect }) => {
const router = createRouter({
history: isServer ? createMemoryHistory() : createWebHistory(),
routes,
});
router.beforeEach((to) => {
if (to.meta.requiresAuth && !isLoggedIn()) return '/login';
});
app.use(router);
if (isServer) await router.push(url);
await router.isReady();
// After isReady(), if the auth guard above redirected, this fires.
if (isServer) applyVueRouterRedirect(router, url, setRedirect);
};
// Pass a custom status for permanent redirects:
// applyVueRouterRedirect(router, url, setRedirect, 308);currentRoute.value.fullPath reflects every guard, redirect rule, and next('/foo') call.applyVueRouterRedirect compares them and calls setRedirect if different.Location header — no HTML rendered for the redirected route.Pass a custom status (e.g. 308) for permanent redirects.