ostrio:flow-router-meta

v2.4.1Published last week

support support

Reactive meta, link, and script tags

Change meta tags on the fly in Meteor.js apps via flow-router-extra API. This package manages meta tags, script and link via simple router object definitions.

Features:

  • 👷‍♂️ Tinytest suite covers globals, routes, groups, not-found, and application/ld+json;
  • 🎛 Per route, per group, and default (all routes) meta tags;
  • 🎛 Per route, per group, and default (all routes) scripts;
  • 🎛 Per route, per group, and default (all routes) link, like CSS files.

Various ways to set meta, script and link tags, ordered by priority:

  • FlowRouter.route() [overrides all below]
  • FlowRouter.group()
  • FlowRouter.globals
  • Head template <meta/>, <link/>, <script/> tags [superseded by any above]

ToC

Install

meteor add ostrio:flow-router-meta

Compatibility

  • Meteor >=1.4, including latest Meteor 3.4;
  • Requires ostrio:flow-router-extra@3.14.0+;
  • Bundles with ostrio:flow-router-title@3.5.0+.

[!NOTE] This package implies ostrio:flow-router-title package.

Demos

ES6 / TypeScript import

1import { FlowRouterMeta } from 'meteor/ostrio:flow-router-meta';
2// This package implies `ostrio:flow-router-title`; both can be imported in one line:
3import { FlowRouterMeta, FlowRouterTitle } from 'meteor/ostrio:flow-router-meta';

TypeScript (with app dependency on ostrio:flow-router-extra so Router types resolve):

1import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
2import { FlowRouterMeta, FlowRouterTitle } from 'meteor/ostrio:flow-router-meta';
3
4FlowRouter.route('/', {
5  name: 'home',
6  title: 'Home',
7  meta: { description: 'Welcome' },
8  action() {},
9});
10
11new FlowRouterMeta(FlowRouter);
12new FlowRouterTitle(FlowRouter);

Published typings ship as package assets (index.d.ts); with zodern:types in the app, meteor/ostrio:flow-router-meta imports are fully typed. FlowRouterMeta is client-only — construct it from client code (or a client entry) after routes are defined.

AGENTS.md

The repo ships AGENTS.md: a compact implementation map for ostrio:flow-router-title. It complements API in this document.

SKILLS.md

  • The main ostrio:flow-router-extra package ships a bundled skill at .agents/skills/meteor-flow-router/SKILL.md (covers ostrio:flow-router-extra, ostrio:flow-router-meta, ostrio:flow-router-title). Install into your project with the Skills CLI (npx skills):
# From a Meteor app repo (install into that app’s .agents/skills for Cursor, etc.)
npx skills add veliovgroup/flow-router --skill meteor-flow-router

# Only list skills discovered in the Flow Router repo (no install)
npx skills add veliovgroup/flow-router --list

# User-global Cursor skills dir (~/.cursor/skills)
npx skills add veliovgroup/flow-router --skill meteor-flow-router --agent cursor --global --yes

flow-router-meta performs the best when used with the next packages:

API

  • new FlowRouterMeta(FlowRouter) — Registers a triggers.enter handler on the router instance; pass the same FlowRouter object you use for routes.

Together with ostrio:flow-router-extra, you may set the following on FlowRouter.route(), FlowRouter.group(), and FlowRouter.globals (merged in that order — route wins over group over globals):

  • meta: Object — Object with meta-tags
  • meta: function(params, qs, data) => object — Method returning object with meta-tags
  • link: Object — Object with link-tags
  • link: function(params, qs, data) => object — Method returning object with link-tags
  • script: Object — Object with script-tags
  • script: function(params, qs, data) => object — Method returning object with script-tags

Usage

You need to initialize FlowRouterMeta and FlowRouterTitle classes by passing FlowRouter object. Right after creating all your routes:

1import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
2import { FlowRouterMeta, FlowRouterTitle } from 'meteor/ostrio:flow-router-meta';
3
4FlowRouter.route('/', {
5  action() { /* ... */ },
6  title: 'Title'
7  /* ... */
8});
9
10new FlowRouterMeta(FlowRouter);
11new FlowRouterTitle(FlowRouter);

404 / notFound compatibility

ostrio:flow-router-meta works with both ostrio:flow-router-extra 404 definitions:

  • Recommended: catch-all route via FlowRouter.route('*', { title, meta, link, script, action })
  • Legacy/deprecated API: FlowRouter.notFound = { title, meta, link, script, action }

Route options from notFound are supported, including dynamic values/functions.

Basic examples

Set only name and content attributes on meta tag:

1FlowRouter.route('/routePath', {
2  name: 'routeName',
3  meta: {
4    name: 'content'
5  }
6});
7// Will generate
8// <meta name="name" content="content">
9
10FlowRouter.route('/routePath', {
11  name: 'routeName',
12  meta: {
13    'og:title': 'Page title'
14  }
15});
16// Will generate
17// <meta name="og:title" content="Page title">

Set only rel and href attributes on link tag:

1FlowRouter.route('/routePath', {
2  name: 'routeName',
3  link: {
4    canonical: 'http://example.com'
5  }
6});
7// Will generate
8// <link rel="canonical" href="http://example.com">
9
10FlowRouter.route('/routePath', {
11  name: 'routeName',
12  link: {
13    rel: 'canonical',
14    href: 'http://example.com'
15  }
16});
17// Will generate
18// <link rel="canonical" href="http://example.com">

Set multiple attributes on meta tag:

1FlowRouter.route('/routePath', {
2  name: 'routeName',
3  meta: {
4    uniqueName: {
5      name: 'name',
6      content: 'value',
7      property: 'og:name',
8      itemprop: 'name'
9    }
10  }
11});
12// Will generate
13// <meta name="name" content="value" property="og:name" itemprop="name">

Set multiple attributes on link tag:

1FlowRouter.route('/routePath', {
2  name: 'routeName',
3  link: {
4    uniqueName: {
5      rel: 'name',
6      sizes: 'value',
7      href: 'http://value',
8      type: 'value-type'
9    }
10  }
11});
12// Will generate
13// <link rel="name" sizes="value" href="http://value" type="value-type">

ldjson

This method uses special property named innerHTML which set script's content instead of attribute. This method and property can be used in the any other case when you need to set script's contents.

1import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
2
3FlowRouter.route('/fourthPage', {
4  name: 'fourthPage',
5  title: 'Fourth Page title',
6  script: {
7    ldjson: {
8      type: 'application/ld+json',
9      innerHTML: JSON.stringify({
10        '@context': 'http://schema.org/',
11        '@type': 'Recipe',
12        name: 'Grandma\'s Holiday Apple Pie',
13        author: 'Elaine Smith',
14        image: 'http://images.edge-generalmills.com/56459281-6fe6-4d9d-984f-385c9488d824.jpg',
15        description: 'A classic apple pie.',
16        aggregateRating: {
17          '@type': 'AggregateRating',
18          ratingValue: '4',
19          reviewCount: '276',
20          bestRating: '5',
21          worstRating: '1'
22        }
23      })
24    }
25  },
26  action() { /*...*/ }
27});

Use function as value

Properties of meta, link, and script tags can be a function that will execute upon navigation

1import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
2
3FlowRouter.route('/routePath', {
4  name: 'routeName',
5  meta: {
6    url: {
7      property: 'og:url',
8      itemprop: 'url',
9      content() {
10        return document.location.href;
11      }
12    }
13  },
14  link: {
15    canonical() {
16      return document.location.href;
17    }
18  }
19});

Use function context

data can get passed from data() hook. Read about data hook.

1import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
2
3FlowRouter.route('/post/:_id', {
4  name: 'post',
5  waitOn(params) {
6    return [Meteor.subscribe('post', params._id)];
7  },
8  async data(params) {
9    return await Collection.Posts.findOneAsync(params._id);
10  },
11  meta: {
12    keywords: {
13      name: 'keywords',
14      itemprop: 'keywords',
15      content(params, query, data) {
16        return data?.keywords || 'default, key, words';
17      }
18    }
19  },
20  title(params, query, data) {
21    if (data) {
22      return data.title;
23    }
24    return '404: Page not found';
25  }
26});

Set CSS and JS per route

Load CSS and JS files on per route and per group basis.

[!IMPORTANT] Once CSS or JS is loaded there's no way to "unload" its code. This package will remove tags from head when navigated to other routes, but contents of loaded JS and CSS files will remain in browser's memory

1import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
2
3// Set default JS and CSS for all routes
4FlowRouter.globals.push({
5  link: {
6    twbs: {
7      rel: 'stylesheet',
8      href: 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css'
9    }
10  },
11  script: {
12    twbs: 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js'
13  }
14});
15
16// Rewrite default JS and CSS, for second route, via controller:
17FlowRouter.route('/secondPage', {
18  name: 'secondPage',
19  action() {
20    return this.render('layout', 'secondPage');
21  },
22  link: {
23    twbs: {
24      rel: 'stylesheet',
25      href: 'https://maxcdn.bootstrapcdn.com/bootstrap/2.2.0/css/bootstrap.min.css'
26    }
27  },
28  script: {
29    twbs: 'https://maxcdn.bootstrapcdn.com/bootstrap/2.2.0/js/bootstrap.min.js'
30  }
31});
32
33// Unset defaults, via controller:
34FlowRouter.route('/secondPage', {
35  name: 'secondPage',
36  action() {
37    return this.render('layout', 'secondPage');
38  },
39  link: {
40    twbs: null
41  },
42  script: {
43    twbs: null
44  }
45});
46
47// Rewrite default JS and CSS, for route group:
48const group = FlowRouter.group({
49  link: {
50    twbs: {
51      rel: 'stylesheet',
52      href: 'https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha/css/bootstrap.min.css'
53    }
54  },
55  script: {
56    twbs: 'https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha/js/bootstrap.min.js'
57  }
58});
59
60group.route('/groupPage1', {
61  name: 'groupPage1',
62  action() {
63    return this.render('layout', 'groupPage1');
64  }
65});

Bootstrap configuration

Push default meta, link, or script tags to FlowRouter.globals

1import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
2
3FlowRouter.globals.push({
4  meta: {
5    // <meta charset="UTF-8">
6    charset: {
7      charset: 'UTF-8'
8    },
9
10    // <meta name="keywords" content="Awes..">
11    keywords: {
12      name: 'keywords',
13      itemprop: 'keywords',
14      content: 'Awesome, Meteor, based, app'
15    },
16
17    // <meta name="description" itemprop="description" property="og:description" content="Default desc..">
18    description: {
19      name: 'description',
20      itemprop: 'description',
21      property: 'og:description',
22      content: 'Default description'
23    },
24    image: {
25      name: 'twitter:image',
26      itemprop: 'image',
27      property: 'og:image',
28      content: 'http://example.com'
29    },
30    'og:type': 'website',
31    'og:title'() {
32      return document.title;
33    },
34    'og:site_name': 'My Awesome Site',
35    url: {
36      property: 'og:url',
37      itemprop: 'url',
38      content() {
39        return window.location.href;
40      }
41    },
42    'twitter:card': 'summary',
43    'twitter:title'() {
44      return document.title;
45    },
46    'twitter:description': 'Default description',
47    'twitter:site': {
48      name: 'twitter:site',
49      value: '@twitterAccountName'
50    },
51    'twitter:creator': {
52      name: 'twitter:creator',
53      value: '@twitterAccountName'
54    },
55    'http-equiv': {
56      'http-equiv': 'X-UA-Compatible',
57      content: 'IE=edge,chrome=1'
58    },
59    robots: 'index, follow',
60    google: 'notranslate'
61  },
62  link: {
63    // <link href="https://maxcdn.bootstrapcdn.com/..." rel="stylesheet">
64    stylesheet: 'https://maxcdn.bootstrapcdn.com/bootstrap/2.3.2/css/bootstrap.min.css',
65
66    // <link rel="canonical" href="http://example.com">
67    canonical() {
68      return document.location.href;
69    },
70
71    // <link rel="image" sizes="500x500" href="http://example.com">
72    image: {
73      rel: 'image',
74      sizes: '500x500',
75      href: 'http://example.com'
76    },
77    publisher: 'http://plus.google...',
78    'shortcut icon': {
79      rel: 'shortcut icon',
80      type: 'image/x-icon',
81      href: 'http://example.com'
82    },
83    'icon': {
84      rel: 'icon',
85      type: 'image/png',
86      href: 'http://example.com'
87    },
88    'apple-touch-icon-144': {
89      rel: 'apple-touch-icon',
90      sizes: '144x144',
91      href: 'http://example.com'
92    },
93    'apple-touch-icon-114': {
94      rel: 'apple-touch-icon',
95      sizes: '114x114',
96      href: 'http://example.com'
97    },
98    'apple-touch-icon-72': {
99      rel: 'apple-touch-icon',
100      sizes: '72x72',
101      href: 'http://example.com'
102    },
103    'apple-touch-icon-57': {
104      rel: 'apple-touch-icon',
105      sizes: '57x57',
106      href: 'http://example.com'
107    }
108  },
109  script: {
110    twbs: 'https://maxcdn.bootstrapcdn.com/bootstrap/2.3.2/js/bootstrap.min.js',
111    d3: {
112      src: 'https://d3js.org/d3.v3.min.js',
113      charset: 'utf-8'
114    }
115  }
116});

Running Tests

  1. Clone this package
  2. In Terminal (Console) go to directory where package is cloned
  3. Then run:

Meteor/Tinytest

# Default
meteor test-packages ./

# With custom port
meteor test-packages ./ --port 8888

# With local MongoDB and custom port
MONGO_URL="mongodb://127.0.0.1:27017/flow-router-meta-tests" meteor test-packages ./ --port 8888

Support this project: