Skip to main content

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:
  1. Collect route information
  2. Subscribe to data
  3. Fetch data from collections
  4. 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>

Accessing Route Information

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;
  }
});

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'
{{#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}`);