Getting started
vmo-router extends Vue Router with dynamic route management for Vue 3 applications that load pages from backend configuration.
Start with a static home page and one report registered on demand. The KeepAlive integration is included but its hook and wrapper can initially be omitted. Read why vmo-router for the design rationale, then extend this example with login registration, persistence and logout in the complete workflow.
Install
npm install vmo-router vue@^3.3.4 vue-router@^4.5.0 pinia@^2.3.0 ramda@^0.30.0Vue, Vue Router, Pinia and Ramda are peer dependencies. Check the versions already installed in your application.
Create a page
Create src/pages/report/page.vue:
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ id?: string }>()
const note = ref('')
</script>
<template>
<section>
<h1>Report {{ id }}</h1>
<input v-model="note" placeholder="Notes" />
</section>
</template>Create src/pages/report/index.pg.ts:
import type { VmoRouteRecordRaw } from 'vmo-router'
const page: VmoRouteRecordRaw<{ keepAlive?: boolean }> = {
path: '/report/:id',
props: true,
meta: { keepAlive: true },
component: () => import('./page.vue')
}
export default pageCreate src/pages/index.ts:
import { loadPageTemplateByImport } from 'vmo-router'
export default loadPageTemplateByImport(
import.meta.glob('./**/*.pg.ts', { eager: true, import: 'default' })
)The report folder becomes the template key Report. Template definitions load eagerly; page components remain lazy.
Create src/pages/home.vue:
<template><h1>Home</h1></template>Create the application
src/main.ts:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { createRouter, createWebHashHistory, useRouterStore } from 'vmo-router'
import type { VmoRouteToRaw } from 'vmo-router'
import App from './App.vue'
import templates from './pages'
import Home from './pages/home.vue'
type Meta = { keepAlive?: boolean }
const app = createApp(App)
app.use(createPinia())
const store = useRouterStore<VmoRouteToRaw<Meta>>()
const router = createRouter<Meta>(
{
history: createWebHashHistory(),
routes: [{ path: '/', name: 'home', component: Home }]
},
templates,
store
)
router.afterEach((to, _from, failure) => {
if (!failure && to.meta.keepAlive && typeof to.name === 'string') {
store.insertKeepAliveName(to.name)
}
})
app.use(router)
router.isReady().then(() => app.mount('#app'))src/App.vue:
<script setup lang="ts">
import { useRouterStore } from 'vmo-router'
const store = useRouterStore()
</script>
<template>
<router-view v-slot="{ Component }">
<keep-alive :include="store.getKeepAliveRouteNames">
<component :is="Component" />
</keep-alive>
</router-view>
</template>Navigate to a dynamic page
Call this from a component's setup:
import { useRouter, isNavigationFailure } from 'vmo-router'
const router = useRouter<{ keepAlive?: boolean }>()
async function openReport() {
try {
const failure = await router.push({
name: 'report-monthly',
params: { id: '2026-10' },
template: { pageKey: 'Report', route: { path: '/reports/:id' } }
})
if (isNavigationFailure(failure)) return
// Navigation has completed.
} catch (error) {
console.error('Failed to open report', error)
}
}To keep the page accessible after a reload, configure persistence.