Descriptive blog urls for Express Js

Ideally a blog url would contain the title of the article it points to. There are a few considerations to be made however, scoping is necessary in order to avoid conflicts, and if the title of your article changes the old url should still work.

My solution involves including the published date, and maintaining an array of old urls in the database. In the end a blog url should look like the url of this article. The first thing we need to do is convert the title into a address bar friendly format.

function slugify (text) {
    if (!text)
        return "";
    return text.toString().toLowerCase()
        .replace(/\s+/g, '-')           // Replace spaces with -
        .replace(/[^\w\-]+/g, '')       // Remove all non-word chars
        .replace(/\-\-+/g, '-')         // Replace multiple - with single -
        .replace(/^-+/, '')             // Trim - from start of text
        .replace(/-+$/, '');            // Trim - from end of text
}

That will trim down our titles into something relatively simple, readable, and unique. As long as we aren't posting more than one article a day with the same title.

Next we are going to want to ensure that we are saving our title slugs.

let body = _.pick(req.body, 'title', 'content');
let slug = slugify(body['title'] || article.body['title']);
// up to date slug

if (article.body['path']) {
    // updating published article
    if (body['title'] && body['title'] != article.body['title']) {
        // title is different
        let slugs = article.body['slugs'] || [];
        if (slugs.indexOf(slug) <= -1) {
            // add slug
            slugs.push(slug);
            body['slugs'] = slugs;
        }
        body['path'] = '/' + [article.body['yyyy'], article.body['mm'], article.body['dd'], slug].join('/');
    }
}
else {
    // updating unpublished article
    if (req.body['publish']) {
        // publish it
        let date = moment.utc();
        body['slugs'] = [slug];
        body['created'] = date.valueOf();
        body['yyyy'] = date.format("YYYY");
        body['mm'] = date.format("MM");
        body['dd'] = date.format("DD");
        // how we know it's published
        body['path'] = '/' + [body['yyyy'], body['mm'], body['dd'], slug].join('/');
    }
}

Here we are constructing a body object which we will use to update our article in the database. We are comparing the submitted title against the old title to see if it changed. If so we add the new slug to our slugs array.

If this action represents a publication event, update our date parameters and a path attribute which suggests our preferred url.

Now we have a few important attributes on our article.

{
    "title": "Title of our article",
    "content": "Content of our article",
    "slugs": ["old-title-of-our-article", "title-of-our-article"],
    "created": 1464710617072,
    "yyyy": "2016",
    "mm": "05",
    "dd": "31",
    "path": "/2016/05/31/title-of-our-article"
}

We need to be able to lookup articles based on the yyyy, mm, dd, and slugs attributes. This is my CouchDB view.

function (doc) {
    for (var i = 0; i < (doc.slugs || []).length; i++) {
        emit([doc.yyyy, doc.mm, doc.dd, doc.slugs[i]], null);
    }
}

You can arrange this a number of ways. This will return one row per working article url. We may now lookup articles based on their date and slug.

router.get('/:yyyy/:mm/:dd/:slug', (req, res, next) => {

    let params = {
        key: [req.params['yyyy'], req.params['mm'], req.params['dd'], req.params['slug']],
        include_docs: true,
        limit: 1
    };

    db.view.catalog("base", "slugs", params, (err, list) => {
        let article = (list ? list.doc(0) : undefined);
        if (err || !article)
            next(err);
        else
            res.render('articles/show', { title: article.body['title'], article: article });
    });

});

You can tweak the outline of this to do something similar using other nosql databases or even a relational one. But use my implementation as a guide.

I think I got it to be relatively simple.