Documentation Index
Fetch the complete documentation index at: https://mintlify.com/meteor/meteor/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Routing drives your application’s user interface based on URLs. In client-rendered Meteor apps, routing happens on the client side, allowing for instant navigation without server round-trips while maintaining browser features like bookmarking, back/forward buttons, and sharing links.
Why Client-Side Routing?
Unlike traditional server-rendered apps where each URL change triggers a server request, Meteor apps:
- Load once and update the UI based on URL changes
- Navigate instantly without page reloads
- Maintain state across navigation
- Still support all browser URL features
The URL represents linkable state - the parts of your application that users should be able to bookmark, share, or navigate to directly.
Flow Router
The recommended routing package for Meteor is Flow Router Extra:
meteor add ostrio:flow-router-extra
Flow Router Extra extends the original Flow Router with additional features like waitOn for handling subscriptions and built-in template context.
Defining Routes
Basic Route
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
FlowRouter.route('/about', {
name: 'about',
action() {
console.log('User viewing about page');
}
});
Route with Parameters
FlowRouter.route('/lists/:_id', {
name: 'Lists.show',
action(params, queryParams) {
console.log('List ID:', params._id);
console.log('Query params:', queryParams);
}
});
URL Pattern Matching
// Pattern: /lists/:_id
// Matches:
'/lists/abc123' // params: { _id: 'abc123' }
'/lists/123?sort=name' // params: { _id: '123' }, queryParams: { sort: 'name' }
// Doesn't match:
'/lists' // No ID parameter
'/lists/abc/def' // Too many segments
Rendering Templates with Blaze
Using Blaze Layout
Install the Blaze Layout package:
meteor add kadira:blaze-layout
Define a Layout Template
<template name="App_body">
<nav>
{{> navigation}}
</nav>
<main>
{{> Template.dynamic template=main}}
</main>
<footer>
{{> footer}}
</footer>
</template>
Render Templates on Route Changes
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
FlowRouter.route('/lists/:_id', {
name: 'Lists.show',
action() {
BlazeLayout.render('App_body', { main: 'Lists_show_page' });
}
});
Page Components
Page components are top-level templates that:
- Collect route information
- Subscribe to data
- Fetch data from collections
- Pass data to child components
// lists-show-page.js
Template.Lists_show_page.onCreated(function() {
// Get route parameter
this.getListId = () => FlowRouter.getParam('_id');
// Subscribe to data
this.autorun(() => {
const listId = this.getListId();
this.subscribe('todos.inList', listId);
});
});
Template.Lists_show_page.helpers({
list() {
const listId = Template.instance().getListId();
return Lists.findOne(listId);
},
todos() {
const listId = Template.instance().getListId();
return Todos.find({ listId });
},
isReady() {
return Template.instance().subscriptionsReady();
}
});
<template name="Lists_show_page">
{{#if isReady}}
{{#if list}}
{{> Lists_show list=list todos=todos}}
{{else}}
{{> App_notFound}}
{{/if}}
{{else}}
{{> App_loading}}
{{/if}}
</template>
Current Route
// Get route name
const routeName = FlowRouter.getRouteName();
// Get URL parameters
const listId = FlowRouter.getParam('_id');
// Get query parameters
const sortOrder = FlowRouter.getQueryParam('sort');
// Get all route info
const current = FlowRouter.current();
console.log(current.route.name);
console.log(current.params);
console.log(current.queryParams);
console.log(current.path);
Reactive Route Helpers
Template.MyTemplate.helpers({
currentPath() {
return FlowRouter.current().path;
},
isActive(routeName) {
return FlowRouter.getRouteName() === routeName;
}
});
Navigation
Programmatic Navigation
// Navigate to a route
FlowRouter.go('Lists.show', { _id: 'abc123' });
// Navigate with query parameters
FlowRouter.go('Lists.show',
{ _id: 'abc123' },
{ sort: 'name', filter: 'active' }
);
// Navigate to a path
FlowRouter.go('/about');
// Go back
FlowRouter.go(FlowRouter.current().route.name);
Generate URLs
// Generate path for a route
const url = FlowRouter.path('Lists.show', { _id: 'abc123' });
// Result: '/lists/abc123'
// With query parameters
const url = FlowRouter.path(
'Lists.show',
{ _id: 'abc123' },
{ sort: 'name' }
);
// Result: '/lists/abc123?sort=name'
HTML Links
{{#each list in lists}}
<a href="{{pathFor 'Lists.show' _id=list._id}}">
{{list.name}}
</a>
{{/each}}
Active Route Highlighting
Template.Navigation.helpers({
activeClass(routeName) {
return FlowRouter.getRouteName() === routeName ? 'active' : '';
}
});
<template name="Navigation">
<nav>
<a href="{{pathFor 'home'}}" class="{{activeClass 'home'}}">
Home
</a>
<a href="{{pathFor 'about'}}" class="{{activeClass 'about'}}">
About
</a>
</nav>
</template>
Or using ActiveRoute (built into Flow Router Extra):
<a href="/lists" class="{{ActiveRoute.name 'Lists.index'}}">
Lists
</a>
Route Groups
Organize routes with common properties:
// Create a group with common properties
const authenticatedRoutes = FlowRouter.group({
name: 'authenticated',
triggersEnter: [function(context, redirect) {
if (!Meteor.userId()) {
redirect('/login');
}
}]
});
// Define routes in the group
authenticatedRoutes.route('/dashboard', {
name: 'dashboard',
action() {
BlazeLayout.render('App_body', { main: 'Dashboard' });
}
});
authenticatedRoutes.route('/profile', {
name: 'profile',
action() {
BlazeLayout.render('App_body', { main: 'Profile' });
}
});
Route Triggers
Execute code when entering or exiting routes:
FlowRouter.route('/admin', {
name: 'admin',
triggersEnter: [
function(context, redirect) {
if (!Meteor.userId()) {
redirect('/login');
}
},
function(context, redirect) {
if (!Roles.userIsInRole(Meteor.userId(), 'admin')) {
redirect('/unauthorized');
}
}
],
triggersExit: [
function() {
console.log('Leaving admin area');
}
],
action() {
BlazeLayout.render('App_body', { main: 'Admin' });
}
});
Not Found (404) Routes
FlowRouter.notFound = {
action() {
BlazeLayout.render('App_body', { main: 'App_notFound' });
}
};
<template name="App_notFound">
<div class="not-found">
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<a href="{{pathFor 'home'}}">Go Home</a>
</div>
</template>
Using with React
import React from 'react';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { mount } from 'react-mounter';
FlowRouter.route('/lists/:_id', {
name: 'Lists.show',
action(params) {
mount(ListPage, { listId: params._id });
}
});
import React, { useState, useEffect } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
function ListPage({ listId }) {
const { list, todos, isLoading } = useTracker(() => {
const handle = Meteor.subscribe('todos.inList', listId);
return {
list: Lists.findOne(listId),
todos: Todos.find({ listId }).fetch(),
isLoading: !handle.ready()
};
}, [listId]);
if (isLoading) return <div>Loading...</div>;
if (!list) return <div>List not found</div>;
return (
<div>
<h1>{list.name}</h1>
<ul>
{todos.map(todo => (
<li key={todo._id}>{todo.text}</li>
))}
</ul>
</div>
);
}
Query Parameters
Reading Query Params
FlowRouter.route('/search', {
name: 'search',
action(params, queryParams) {
// URL: /search?q=meteor&category=guides
console.log(queryParams.q); // 'meteor'
console.log(queryParams.category); // 'guides'
}
});
Setting Query Params
// Replace query params
FlowRouter.setQueryParams({ sort: 'name', filter: 'active' });
// Add/update specific params
FlowRouter.setQueryParams({ page: 2 });
Best Practices
Keep URLs Linkable
Only put state in the URL that users should be able to bookmark or share.
// ✅ Good: Linkable search state
FlowRouter.go('search', {}, { q: 'meteor', category: 'guides' });
// ❌ Bad: Temporary UI state
FlowRouter.go('search', {}, { modalOpen: true, tooltipVisible: false });
Subscribe in Page Components
// ✅ Good: Subscribe close to where data is used
Template.Lists_show_page.onCreated(function() {
this.autorun(() => {
this.subscribe('todos.inList', FlowRouter.getParam('_id'));
});
});
// ❌ Bad: Global subscriptions
Meteor.startup(() => {
Meteor.subscribe('allTodos'); // Loads too much data
});
Use Named Routes
// ✅ Good: Named routes are refactorable
FlowRouter.go('Lists.show', { _id: listId });
// ❌ Bad: Hardcoded paths break when routes change
FlowRouter.go(`/lists/${listId}`);