JavaScript
Article

How to Add Permalinks to a Meteor Project

By David Turnbull

There’s no reasons for modern web applications to not have permalinks (also known as “pretty URLs”). They’re easier for users to remember, important for SEO, and when working with Meteor, extremely easy to implement.

To demonstrate how to create a basic permalinks system, we’re going to build a barebone blogging application. By default, each post will have a URL that contains the ID of the post like the following: http://localhost:3000/post/pCZLhbwqtGXPdTDMM.

This is the simplest approach, but the resulting URL isn’t very nice, so we’ll then learn how to replace that ID with a custom value. Our change will enable us to have a URL like this: http://localhost:3000/post/hello-world.

Along the way, we’ll also learn a couple of other tricks that’ll come in handy when building any kind of web application with Meteor. In case you want to play with the code created in this article I’ve created a repository for you.

Let’s start!

Getting Started

Inside a new Meteor project, install Iron Router by running the command:

meteor add iron:router

For the uninitiated, Iron Router is the preferred routing package among Meteor developers, and it allows us to associate templates with URL paths.

For example, we can create a “home” template like the following:

<template name="home">
    <form>
        <input type="text" name="title" placeholder="Title goes here...">
        <textarea name="content" placeholder="Entry goes here..."></textarea>
        <input type="submit" value="Add Post">
    </form>
</template>

And then a route for this template as shown below:

Router.route('/', function(){
    this.render('home'); 
});

Once done, whenever we visit the root path of our application, the content of the “home” template will be shown.

Writing Posts

To store the blog posts for our application, we need to create a “posts” collection:

var Posts = new Meteor.Collection('posts');

Inside an isClient conditional, we’ll write statements that will add data to this collection once the form inside the “home” template is submitted:

if(Meteor.isClient) {
    Template.home.events({
        'submit form': function(event){
            event.preventDefault();
            var title = event.target.title.value;
            var content = event.target.content.value;
            Posts.insert({
                title: title,
                content: content
            });
        }
    });
}

We also need each post to have a “permalink” field to define the value that will appear in the URL. I like to set this field to the value of the _id field as a default.

To achieve this, store the value returned by the insert() function inside a variable and then call the update() function as shown below:

var post = Posts.insert({
    title: title,
    content: content
});

Posts.update(
   { _id: post },
   { $set: {permalink: post} }
);

With this code, we’re:

  1. Creating a new post.
  2. Storing the ID of the post in a variable named post.
  3. Updating the specified post.
  4. Setting the “permalink” field to the value of the post’s ID.

If you created a post, for instance, and it had an ID of “pCZLhbwqtGXPdTDMM”, then the structure of the post would resemble to the following:

{
    _id: 'pCZLhbwqtGXPdTDMM',
    title: 'Hello World',
    content: 'This is the content...',
    permalink: 'pCZLhbwqtGXPdTDMM'
}

You could also create a permalink based on the title of the post — so if the post title was “Hello World”, the permalink would be “hello-world” — but this is a little beyond the scope of this tutorial.

Post Pages

Every blog post needs its own, individual page. For starters though, these pages will rely on the ID of the post, rather than the permalink value (even though these are currently the same values, as we just covered).

First, create a “post” template for these post pages:

<template name="post">
    <h1>{{title}}</h1>
    <div class="entry">
        {{content}}
    </div>
</template>

Then create a route:

Router.route('/post/:_id', function(){
    var postId = this.params._id;
    this.render('post', {
        data: function(){
            return Posts.findOne(postId);
        }
    });
});

In this code we are:

  1. Extracting the _id field from the URL.
  2. Storing this value in the “postId” variable.
  3. Retrieving a document based on this ID.

As a result, if a post has the ID of “pCZLhbwqtGXPdTDMM”, that post will become accessible through the URL http://localhost:3000/post/pCZLhbwqtGXPdTDMM.

You don’t need the “/post” part of the URL, and you could change the route code with the following to remove this part of the URL:

Router.route('/:_id', function(){
    var postId = this.params._id;
    this.render('post', {
        data: function(){
            return Posts.findOne(postId);
        }
    });
});

With this change in place, the URL for the same post page would become http://localhost:3000/pCZLhbwqtGXPdTDMM.

Editing the “Permalink” Field

We’re close to adding permalink support to the application but, first, let’s write the code to edit the “permalink” field. To do that, we need to update the “post” template by adding the “permalink” field. We’ll also add a contentEditable attribute to the surrounding div element. Using this attribute, we’ll be able to directly edit the permalink field from within the page, although we’ll need to build the logic for this to work. The resulting code of the template is shown below:

<template name="post">
    <h1>{{title}}</h1>
    <div class="permalink" contenteditable="true">{{permalink}}</div>
    <div class="entry">
        {{content}}
    </div>
</template>

To create the business logic mentioned early, create an event that allows users to edit the value of the “permalink” element and press the “Return” key to save that value to the database. The code to create such event is shown below:

Template.post.events({
    'keypress .permalink': function(event, template){
        var permalink = template.find('.permalink').innerHTML;
        if(event.which === 13){
            event.preventDefault();
            Posts.update({_id: this._id}, {$set: {permalink: permalink}});
            event.target.blur();
        }
    }
});

Here’s how it works:

  1. The keypress event is triggered when the user presses a key while focused on the element having class of permalink.
  2. The content of the “permalink” field is retrieved with the template.find function and stored in a “permalink” variable.
  3. If the “Return” key is pressed, the value in the “permalink” variable is saved to the database, overwriting the old value.

Once you’ll complete this last step, this is what the user interface will look like:

meteor permalinks

Adding Permalink Support

Up to this point, we have set up almost all the code needed. However, to make permalinks work as planned, you have to change the code that handles the route as follows:

Router.route('/post/:permalink', function(){
    var permalinkValue = this.params.permalink;
    this.render('post', {
        data: function(){
            return Posts.findOne({permalink: permalinkValue});
        }
    });
});

Here, there’s a few things going on:

  1. The path of the route contains a “permalink” parameter instead of an “_id” parameter.
  2. Instead of using the “postId” variable, we have a “permalinkValue” variable.
  3. The findOne function has been modified to perform its search based on the value of the permalink field, rather than the _id field.

As a result, the URL http://localhost:3000/post/pCZLhbwqtGXPdTDMM will no longer work, but http://localhost:3000/post/hello-world will do.

Neat, isn’t it?

Changing the Permalink

At the moment, if the user changes the permalink of a page, that page will break. We can fix this so that, right after a permalink is modified, the user is redirected to the new route.

To do this, add a “name” parameter to our post’s route:

Router.route('/post/:permalink', function(){
    var permalinkValue = this.params.permalink;
    this.render('post', {
        data: function(){
            return Posts.findOne({permalink: permalinkValue});
        }
    });
}, {
    name: 'post'
});

Then add a Router.go function to the keypress event:

Template.post.events({
    'keypress .permalink': function(event, template){
        var permalink = template.find('.permalink').innerHTML;
        if(event.which == 13){
            event.preventDefault();
            Posts.update({_id: this._id}, {$set: {permalink: permalink}});
            event.target.blur();
            Router.go('post', {permalink: permalink});
        }
    }
});

Here, we’re using this Router.go function to redirect to the “post” route using the edited value of the permalink field.

Conclusions

Setting up the basics of a permalink system is simple enough. The tricky part is all the extra details that need to be considered. Here are a few questions to think about:

  • What happens if the user tries to define a permalink that’s already been defined for a separate page?
  • What kind of limits should be enforced when defining a permalink? (Length, types of characters allowed, etc.)
  • How can you auto-create a permalink based on the title of the blog post, instead of using the ID of a post in the permalink by default?

These problems are a good exercise to test your knowledge. So you might like to tackle them in your own time. In case you want to play with the code created in this article I’ve created a repository for you.


Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in JavaScript, once a week, for free.