AbsoluteJS

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:

TS
// 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:

TS
// 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:

TS
// 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>;
urlrequest pathname (plus search) on the server, current window URL on the client.
isServertrue 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.
setRedirect(location, status?)server-only. Call to short-circuit the SSR render and emit an HTTP redirect instead. Status defaults to 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:

HTML
<!-- 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>
createRouterfrom vue-router. Pair with createMemoryHistory() on the server and createWebHistory() on the client.
RouterLink / RouterViewvue-router's components for navigation and the active-route outlet.
useRouter / useRoutecomposables for accessing the router instance and the current route's reactive state.
setupAppAbsoluteJS-specific hook your page module exports. The handler invokes it between createSSRApp() and renderToWebStream() — the only window where vue-router can be installed and resolved before SSR starts.
applyVueRouterRedirectAbsoluteJS helper that translates a vue-router redirect (from a guard or redirect rule) into a 302 response.

#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:

TS
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);
1
After await router.isReady()
vue-router's currentRoute.value.fullPath reflects every guard, redirect rule, and next('/foo') call.
2
If the resolved path differs from the requested URL, a redirect happened.
applyVueRouterRedirect compares them and calls setRedirect if different.
3
The page handler sees setRedirect was called
and returns a 302 response with the redirect target as the Location header — no HTML rendered for the redirected route.

Pass a custom status (e.g. 308) for permanent redirects.