Simple SEO-Ready Meteor Boilerplate with Iron Router

August 3, 2019 6:50 pm Published by

Setting up a brand new Meteor app is as simple as running meteor create but setting it up to have routing and indexable pages is more time-consuming. So I created a simple boilerplate I can use to get started quickly.

Assumptions & technology stack choices:

  • You’re familiar with Meteor basics
  • You’re ok running the production environment on Meteor’s Galaxy hosting (starting at ~$30/mo)
  • Using Blaze for templating
  • Using Iron Router for routing

If you want to clone the boilerplate repo, just run these 2 commands:

git clone https://github.com/meteorhubdotnet/meteor-iron-router-seo-boilerplate.git
meteor npm install --save @babel/runtime

If you want to manually code it, here are the steps. First, run:

meteor create --bare MY_PROJECT_NAME_GOES_HERE
cd MY_PROJECT_NAME_GOES_HERE
meteor

And let’s update the package list for our project – first let’s replace static HTML with Blaze templating:

meteor remove static-html
meteor add blaze-html-templates

Then let’s add a few packages:

meteor add iron:router mdg:seo meteorhubdotnet:seo gadicohen:sitemaps underscore

Then create the following folder structure:

My Project
|-- client
    |-- head.html
    |-- layout.html
    |-- page-home.html
    |-- page-home.js
    |-- page-home.css
    |-- page-2.html
    |-- page-2.js
    |-- page-2.css
|-- server
    |-- sitemap.js
|-- public
|-- private
|-- routes
    |-- route-for-page-home.js
    |-- route-for-page-2.js
    |-- router-options.js
    |-- router-seo.js

Now let’s start working on our homepage. In the client folder, add head.html:

<!-- /client/head.html -->
<head>
  <title>MY_PROJECT_NAME_GOES_HERE</title>
  <meta name="viewport" content="initial-scale=1, width=device-width, height=device-height, viewport-fit=cover">
</head>

And in page-home.html and page-2.html let’s add some (very) basic code:

<!-- /client/page-home.html -->
<template name="pageHome">
    <h1>Home</h1>
    <a href="{{pathFor 'page2'}}">Go to page 2</a>
</template>
<!-- /client/page-2.html -->
<template name="page2">
    <h1>Page 2</h1>
    <a href="{{pathFor 'pageHome'}}">Go to home</a>
</template>

In layout.html we must add the {{>yield}} template helper for Iron Router:

<!-- /client/layout.html -->
<template name="layout">
  {{>yield}}
</template>

Now that we have some HTML for our layout and our pages, let’s configure our routes:

// /routes/router-options.js
Router.configure({
    layoutTemplate: 'layout',
});
// /routes/route-for-page-home.js
import { Router } from 'meteor/iron:router';
Router.route(
    '/',
    {
        // Route name
        name: 'pageHome',
        // Include in sitemap?
        sitemap: true,
        // Crawl request frequency
        changefreq: 'daily',
        // Crawl priority
        priority: '1.0',
        // Activate pre-rendering of meta info
        ironMeta: true,
        // Meta info
        meta() {
            return {
                title: 'META_TITLE_GOES_HERE',
                description: 'META_DESCRIPTION_GOES_HERE',
                keywords: 'META_KEYWORDS_GO_HERE',
                canonical: 'CANONICAL_URL_GOES_HERE',

            };
        },
    },
);
// /routes/route-for-page-2.js
import { Router } from 'meteor/iron:router';
Router.route(
    '/page-2',
    {
        // Route name
        name: 'page2',
        // Include in sitemap?
        sitemap: true,
        // Crawl request frequency
        changefreq: 'monthly',
        // Crawl priority
        priority: '0.1',
        // Activate pre-rendering of meta info
        ironMeta: true,
        // Meta info
        meta() {
            return {
                title: 'META_TITLE_FOR_PAGE_2_GOES_HERE',
                description: 'META_DESCRIPTION_FOR_PAGE_2_GOES_HERE',
                keywords: 'META_KEYWORDS_FOR_PAGE_2_GO_HERE',
                canonical: 'CANONICAL_URL_FOR_PAGE_2_GOES_HERE',

            };
        },
    },
);

At this point, the app works and renders the page metadata, but only when the page first loads. We want our metadata to get updated whenever the route changes, so let’s add some code to router-seo.js in our routes folder:

// /routes/router-seo.js
import { Router } from 'meteor/iron:router';

Router.onStop(function() {

    if (Meteor.isClient) {

        // Remove twitter stuff
        // Remove Facebook stuff
        $('[seo="meteorhubdotnet"]').remove();

    }

});

Router.onAfterAction(function() {

    if (Meteor.isClient) {

        const meta = this.lookupOption('meta');

        if (typeof meta === 'function') {

            this.meta = _.bind(meta, this);

        } else if (typeof meta !== 'undefined') {

            this.meta = function() {

                return meta;

            };

        }
        if (meta) {

            const metaData = this.meta();

            if (metaData) {

                $('[seo="meteorhubdotnet"]').remove();
                _.each(metaData, function (val, key) {

                    if (key === 'title') {

                        document.title = val;

                    } else if (key === 'canonical') {

                        $('head').append(`<link rel="canonical" href="${val}" seo="meteorhubdotnet">`);

                    } else {

                        // Inject.meta(key, val, res);
                        let idType = 'name';

                        // Twitter like the name attribute whilst standard dictates property
                        if (key.slice(0, 2) === 'og' || key.slice(0, 2) === 'fb') {

                            idType = 'property';

                        }
                        $('head').append(`<meta ${idType}="${key}" content="${val}" seo="meteorhubdotnet"></meta>`);

                    }

                });

            }

        }

    }

});

Now let’s add a sitemap to our website. In the sitemap.js files, let’s add:

// /routes/router-sitemap.js

Meteor.startup(function() {

    sitemaps.add('/sitemap.xml', function () {

        const out = [];
        Router.routes.forEach(function (route) {

            if (route.options && route.options.sitemap) {

                if (route.path()) {

                    out.push({
                        page: route.path(),
                        lastmod: new Date(),
                        changefreq: route.options.changefreq ? route.options.changefreq : 'monthly',
                        priority: route.options.priority ? route.options.priority : '0.5',
                    });

                }

            }

        });

        return out;

    });

});

Now if you go to http://localhost:3000/sitemap.xml you will see a sitemap with 1 entry for each route in your app.

And finally, we can deploy to Galaxy and our app will get picked up by search engines!

Categorised in: ,

This post was written by Yacine