Skip to content

Slugs for Mongoose with history and i18n support (uses speakingurl by default, but you can use any slug library such as limax, slugify, mollusc, or slugme)

License

Notifications You must be signed in to change notification settings

ladjs/mongoose-slug-plugin

Repository files navigation

mongoose-slug-plugin

build status code coverage code style styled with prettier made with lass license

Slugs forMongoosewith history andi18nsupport (usesspeakingurlby default, but you can use any slug library such aslimax,slugify,mollusc,orslugme)

Table of Contents

Install

npm:

npm install mongoose-slug-plugin

yarn:

yarn add mongoose-slug-plugin

Usage

Add the plugin to your project (it will automatically generate a slug when the document is validated based off the template string passed)

constmongooseSlugPlugin=require('mongoose-slug-plugin');
constmongoose=require('mongoose');

constBlogPost=newmongoose.Schema({
title:String
});

BlogPost.plugin(mongooseSlugPlugin,{tmpl:'<%=title%>'});

module.exports=mongoose.model('BlogPost',BlogPost);

If you need to render some custom function in the template string for display purposes, such as outputting a formatted date withdayjs:

constdayjs=require('dayjs');

constmongooseSlugPlugin=require('mongoose-slug-plugin');
constmongoose=require('mongoose');

constBlogPost=newmongoose.Schema({
title:{type:String,required:true,unique:true},
posted_at:{type:Date,required:true}
});

BlogPost.plugin(mongooseSlugPlugin,{
tmpl:"<%=title%>-<%=dayjs(posted_at).format('YYYY-MM-DD')%>",
locals:{dayjs}
});

module.exports=mongoose.model('BlogPost',BlogPost);

If you're usingKoa,here's an example showing how to lookup a slug or an archived slug and properly 301 redirect:

constKoa=require('koa');
constRouter=require('koa-router');
constBoom=require('boom');

constBlogPosts=require('./blog-post');

constapp=newKoa();
constrouter=newRouter();

router.get('/blog/:slug',async(ctx,next)=>{
try{
// lookup the blog post by the slug parameter
constblogPost=awaitBlogPosts.findOne({slug:ctx.params.slug});

// if we found it then return early and render the blog post
if(blogPost)returnctx.render('blog-post',{title:blogPost.title,blogPost});

// check if the slug changed for the post we're trying to lookup
blogPost=awaitBlogPosts.findOne({slug_history:ctx.params.slug});

// 301 permanent redirect to new blog post slug if it was found
if(blogPost)returnctx.redirect(301,`/blog/${blogPost.slug}`);

// if no blog post found then throw a nice 404 error
// this assumes that you're using `koa-better-error-handler`
// and also using `koa-404-handler`, but you don't necessarily need to
// since koa automatically sets 404 status code if nothing found
// <https://github /ladjs/koa-better-error-handler>
// <https://github /ladjs/koa-404-handler>
returnnext();

}catch(err){
ctx.throw(err);
}
});

app.use(router.routes());
app.listen(3000);

If you're usingExpress,here's an example showing how to lookup a slug or an archived slug and properly 301 redirect:

TODO

Note that you also have access to a static function on the model calledgetUniqueSlug.

This function accepts an_idandstrargument. The_idbeing the ObjectID of the document andstrbeing the slug you're searching for to ensure uniqueness.

This function is used internally by the plugin to recursively ensure uniqueness.

Static Methods

If you have to write a script to automatically set slugs across a collection, you can use thegetUniqueSlugstatic method this package exposes on models.

For example, if you want to programmatically set all blog posts to have slugs, run this script (note that you should run the updates serially as the example shows to prevent slug conflicts):

constPromise=require('bluebird');// exposes `Promise.each`

constBlogPost=require('../app/models/blog-post.js');

(async()=>{
constblogPosts=awaitBlogPost.find({}).exec();
awaitPromise.each(blogPosts,asyncblogPost=>{
blogPost.slug=null;
blogPost.slug=awaitBlogPost.getUniqueSlug(blogPost._id,blogPost.title);
returnblogPost.save();
}));
})();

Options

Here are the default options passed to the plugin:

  • tmpl(String) - Required, this should be alodash template string(e.g.<%=title%>to use the blog post title as the slug)
  • locals(Object) - Defaults to an empty object, but you can pass a custom object that will be inherited for use in the lodash template string (see above example for how you could usedayjsto render a document's date formatted in the slug)
  • alwaysUpdateSlug(Boolean) - Defaults totrue(basically this will re-set the slug to the value it should be based off the template string every time the document is validated (or saved for instance due to pre-save hook in turn calling pre-validate in Mongoose)
  • errorMessage(String) - Defaults toSlug was missing or blank,this is a String that is returned for failed validation (note that it gets translated based off thethis.localefield if it is set on the document (seeLadfor more insight into how this works))
  • logger(Object) - defaults toconsole,but you might want to useLad's logger
  • slugField(String) - defaults toslug,this is the field used for storing the slug for the document
  • historyField(String) - defaults toslug_history,this is the field used for storing a document's slug history
  • i18n(Object|Boolean) - defaults tofalse,but accepts ai18nobject fromLad's i18n
  • slug(Function) - Defaults tospeakingurl,but it is a function that converts a string into a slug (see belowCustom Slug Libaryexamples)
  • slugOptions(Object) - An object of options to pass to the slug function when invoked as specified inoptions.slug

Slug Tips

If you're using the default slug libraryspeakingurl,then you might want to pass the optionslugOptions: { "'": '' }in order to fix contractions.

For example, if your title is "Jason's Blog Post", you probably want the slug to be "jasons-blog-post" as opposed to "jason-s-blog-post". This option will fix that.

Seepid/speakingurl#105for more information.

Slug Uniqueness

If a slug of "foo-bar" already exists, and if we are inserting a new document that also has a slug of "foo-bar", then this new slug will automatically become "foo-bar-1".

Custom Slug Library

If you don't want to use the libraryspeakingurlfor generating slugs (which this package uses by default), then you can pass a customslugfunction:

limaxexample:

constlimax=require('limax');

BlogPost.plugin(mongooseSlugPlugin,{tmpl:'<%=title%>',slug:limax});

slugifyexample:

constslugify=require('slugify');

BlogPost.plugin(mongooseSlugPlugin,{tmpl:'<%=title%>',slug:slugify});

molluscexample:

constslug=require('mollusc');

BlogPost.plugin(mongooseSlugPlugin,{tmpl:'<%=title%>',slug});

slugmeexample:

constslugme=require('slugme');

BlogPost.plugin(mongooseSlugPlugin,{tmpl:'<%=title%>',slug:slugme});

Background

I created this package despite knowing that other alternatives like it exist for these reasons:

  • No alternative supported i18n localization/translation out of the box
  • No alternative used the well-tested and SEO-friendlyspeakingurlpackage
  • No alternative allowed users to pass their own slug library
  • No alternative documented how to clearly do a 301 permanent redirect for archived slugs
  • No alternative allowed the field names to be customized
  • No alternative had decent tests written

Contributors

Name Website
Nick Baugh http://niftylettuce /
shadowgate15 https://github /shadowgate15

License

MIT©Nick Baugh