Framework7 Router: History Modes, Async Routes, and Navigation Guards Explained
Framework7 Router guide: history modes (hash vs browser), async routes, navigation guards, nested route pitfalls, and replaceState. Includes server rewrites and verification steps.
29 Apr 2026, 14:47 UTC

The Problem: Clean URLs Without 404s on Refresh
Framework7's router handles client-side navigation for single-page applications targeting iOS, Android, and desktop. The immediate decision every project faces: which history mode to use. The default hash mode works everywhere without server configuration but produces URLs like example.com/#/settings. The browser mode gives clean URLs (example.com/settings) but requires server-side rewrite rules — miss them and users hit 404 on refresh or deep links. This article walks through choosing a mode, configuring routes with lazy loading, using navigation guards for auth, and avoiding the silent failures that waste debugging time.
Choose Your History Mode
Framework7 v8+ supports three modes:
- hash (default):
example.com/#/path. Works on any static host (GitHub Pages, Netlify, S3) with zero config. - browser:
example.com/path. Requires the web server to rewrite all non-asset requests toindex.html. - hashbang:
example.com/#!/path. Legacy; avoid for new projects.
For production SPAs, browser is the standard choice — but only if you configure rewrites. Here's a minimal nginx snippet:
location / {
try_files $uri $uri/ /index.html;
}
On Apache, add to .htaccess:
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
For Netlify or Vercel, create a _redirects or vercel.json with a catch-all rewrite to /index.html. After deploy, verify by loading a deep route directly (e.g., https://your-app.com/settings/profile) in a fresh browser tab — no 404 means rewrites work.
Define Routes with Async Components for Code-Splitting
Routes live in an array passed to the Framework7 app params. Each route object needs a path and a component. Use asyncComponent for lazy loading — the router resolves the promise before navigation completes, keeping the initial bundle small.
// src/js/routes.js (generated by framework7-cli)
export default [
{
path: '/',
component: () => import('./pages/Home.vue'),
},
{
path: '/about/',
asyncComponent: () => import('./pages/About.vue'),
},
{
path: '/profile/:id/',
asyncComponent: () => import('./pages/Profile.vue'),
beforeEnter: async (to, from, resolve, reject) => {
const auth = await checkAuth();
if (!auth) return resolve({ redirect: '/login/' });
resolve();
},
},
{
path: '/admin/',
component: () => import('./pages/Admin.vue'),
beforeEnter: (to, from, resolve, reject) => {
if (!userIsAdmin()) return reject();
resolve();
},
},
];
Critical detail: The imported module must export a default component. Named exports (export const About = ...) cause "component is undefined" errors with no clear stack trace. Check your bundle output if using a custom Webpack/Vite config.
Route params (:id) and query strings are reactive in Vue/React/Svelte bindings. Access via $route.params.id or $route.query.tab. In vanilla JS, use router.currentRoute.params. Params update without component remount when only params change on the same path.
Navigation Guards: Auth, Redirects, and Cancellation
Guards run in sequence on every navigation, including browser back/forward. Three hooks exist:
beforeEnter(to, from, resolve, reject)— before the target component mountsbeforeLeave(to, from, resolve, reject)— before leaving the current componentresolve(to, from, resolve, reject)— after async component loads, before mount
Call resolve() to proceed, resolve({ redirect: '/login/' }) to redirect, or reject() to cancel and keep the URL unchanged. Guards are async; await API calls or auth checks inside them.
Example: protect a profile route with a token check that redirects to login on failure:
{
path: '/profile/:id/',
asyncComponent: () => import('./pages/Profile.vue'),
beforeEnter: async (to, from, resolve, reject) => {
try {
const token = localStorage.getItem('auth_token');
if (!token) return resolve({ redirect: '/login/?next=' + encodeURIComponent(to.url) });
const valid = await validateToken(token);
if (!valid) return resolve({ redirect: '/login/' });
resolve();
} catch (e) {
reject(); // network error — cancel navigation, stay on current page
}
},
}
Performance caution: Heavy logic in beforeEnter blocks the UI. Cache auth state in memory or IndexedDB; debounce repeated checks. The guard runs on every navigation, including history traversal.
Nested Routes Require a Router View in the Parent
Child routes inherit the parent's path prefix. The parent component must contain a router outlet — <router-view> in Vue, <Router.View> in React/Svelte (capitalized!). Missing the outlet is a silent failure: child components never mount, no error appears.
// Parent route with children
{
path: '/settings/',
component: () => import('./pages/SettingsLayout.vue'),
children: [
{ path: 'profile/', asyncComponent: () => import('./pages/SettingsProfile.vue') },
{ path: 'notifications/', asyncComponent: () => import('./pages/SettingsNotifications.vue') },
]
}
In SettingsLayout.vue:
<template>
<div class="page">
<nav>...</nav>
<router-view /> <!-- required for children to render -->
</div>
</template>
In React/Svelte, use <Router.View> (capital V). Lowercase <router-view> works only in Vue; mismatch yields blank pages without console errors.
Programmatic Navigation and replaceState
The router instance is available as app.router or this.$f7router in bindings. Navigate programmatically:
// With animation (default)
router.navigate('/profile/123/', { animate: true });
// Replace history entry — use for login flows to prevent back-button returning to auth page
router.navigate('/dashboard/', { replaceState: true });
// Navigate without adding to history (rare)
router.navigate('/modal-content/', { history: false });
replaceState: true is essential after login: it swaps the login page for the dashboard in history so the back button exits the app instead of returning to the auth screen.
Limitations and Common Mistakes
- No built-in scroll restoration. Framework7's router doesn't save/restore scroll position. Implement in
beforeLeave(save) andbeforeEnter(restore) if needed. - Case-sensitive paths.
/Profileand/profileare different routes. Enforce lowercase in definitions and normalize inbound links. - Async component default export required. Named exports break silently. Verify with
console.log(await import('./Page.vue'))in dev tools. - Browser history mode fails on static hosts without rewrites. Always test a direct deep-link load after deploy.
- Guards run on back/forward. Don't assume they only fire on link clicks.
Verify Your Setup
- Create a minimal app:
npx framework7-cli create --framework vue --bundler vite(run in your project directory, no special permissions). Inspectsrc/js/routes.jsfor the generated structure. - Build and serve locally:
npx vite build && npx serve dist. Refresh a deep route (/settings/profile) — it should load without 404 if rewrites work. - Add a
beforeEnterguard that callsreject(). Click a link to that route, then press browser back — URL should stay unchanged. - Open Network tab, navigate to a lazy route, confirm a separate chunk loads.
- Review the current Framework7 Router docs at
framework7.io/docs/router.htmlfor version-specific API changes.
Quick Reference: Route Object Properties
| Property | Type | Purpose |
|---|---|---|
| path | string | URL pattern with optional :param segments |
| component | Component | Synchronously imported component |
| asyncComponent | function | Returns a promise resolving to default export (lazy load) |
| beforeEnter | function | Guard: (to, from, resolve, reject) => {} |
| beforeLeave | function | Guard before leaving current route |
| children | array | Nested routes inheriting parent path |
| redirect | string/object | Redirect target (can be used in guard via resolve({ redirect })) |
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.