Framework7 Router: Declarative Navigation That Actually Works in Production
Framework7's router gives you declarative, deep-linkable navigation with lifecycle hooks — here's a working config, how to verify it, and the mistakes that break it in production.
20 Aug 2025, 10:11 UTC

If you're building a Framework7 app and want page navigation that feels native without full reloads, the built-in router is the answer: you declare routes as a plain JavaScript object, and the router handles history state, template rendering, and lifecycle events for you. The catch is that a few small misconfigurations — relative template paths, missing return values in lifecycle hooks — cause silent failures that only show up in production. This guide walks through a working configuration, explains what the router actually does with it, and flags the mistakes that bite most often.
What the router gives you out of the box
Framework7's router maps URL paths to pages. Each route entry points to a component, template, or template URL, and can register callbacks for the page lifecycle: pageBeforeIn, pageAfterIn, pageBeforeOut, and pageAfterOut. When you navigate, the router intercepts the call, pushes a new entry with the History API (history.pushState), loads and renders the template with Framework7's Template7 engine, mounts the page DOM, and fires your registered events in order. The browser's back button works because the router listens for popstate and reverses the same sequence.
The practical result: you get deep-linkable URLs, working back-button behavior, and per-page setup/teardown hooks without writing any of that plumbing yourself.
A working configuration
Here is a minimal but complete setup. Run it in a Framework7 project scaffolded with the Framework7 CLI (npm init framework7) or any local dev server that serves your template files from the same origin:
var app = new Framework7({\n root: '#app',\n routes: [\n {\n path: '/about/',\n componentUrl: './about.html',\n on: {\n pageBeforeIn: function (event, page) {\n console.log('About page loading');\n }\n }\n }\n ],\n view: {\n main: true\n }\n});\n\nvar mainView = app.views.main;\n\n// Trigger a navigation, e.g. from a click handler:\nmainView.router.navigate('/about/', { animate: true });The about.html file is a Template7 template containing your page markup. When navigate() runs, the router fetches about.html, renders it, inserts a new element with the page class into the view, and fires pageBeforeIn before the transition starts — so you should see the console log before the page visually appears.
How to verify it works
Three checks confirm the full pipeline:
- Lifecycle order: open the browser console, navigate to
/about/, and confirm the log appears before the page transition completes. - History integration: after navigation,
window.location.pathnameshould be/about/. Clicking the browser back button should fire yourpageBeforeOut/pageAfterOuthandlers if defined. - DOM mount: inspect the view container — the new page element should carry the
pageclass and contain the markup fromabout.html, confirming Template7 rendered it.
Limits worth knowing before you scale
Two constraints matter in larger apps:
- Deep nesting costs performance. Routes nested more than about five levels deep can stutter on low-end devices, because each level triggers its own DOM insertion and transition. Prefer flattening navigation or using tabs for deep hierarchies.
- No automatic code splitting. The router loads what you point it at, but it won't lazy-load heavy component bundles for you. If a page pulls in large dependencies, split that code manually with your bundler and load it in
pageBeforeInor via dynamic import.
Common mistakes and how to avoid them
1. Cancelling navigation incorrectly
If you intend to block a page from closing in a pageBeforeOut handler (for example, to guard unsaved form data), you must return false. Forgetting the return value lets the navigation proceed silently, and the user loses their state with no error anywhere.
2. Relative template URLs breaking in production
Using componentUrl: 'about.html' without a leading ./ or without configuring the app's root path often works in dev and 404s in production, where the app is served from a subpath. Resolve template URLs against a known base, and test a production build before shipping.
3. Cross-origin template requests
Template files must be served from the same origin as the app. Cross-origin requests are blocked by the browser's same-origin policy, and the failure is silent — the page simply never appears. If templates live on a CDN, either proxy them or inline them.
4. Bundler asset handling
With Webpack or Vite, a plain componentUrl string referencing a file the bundler doesn't know about can resolve to undefined at runtime, throwing a navigation error. Import templates as raw strings (via the appropriate loader, e.g. ?raw in Vite) and pass them with the route's template or component property instead of a URL.
Bottom line
Declare routes, let the router own history and lifecycle, and verify with the three checks above. Most production issues trace back to template path resolution or a missing return false — both cheap to fix once you know to look for them.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.