Skip to content

Commit fc8a751

Browse files
committed
Initial commit
0 parents  commit fc8a751

11 files changed

+6391
-0
lines changed

.editorconfig

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
root = true
2+
3+
[*]
4+
indent_style = space
5+
indent_size = 2
6+
end_of_line = lf
7+
charset = utf-8
8+
trim_trailing_whitespace = true
9+
insert_final_newline = true

.gitattributes

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* text=auto

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.DS_Store
2+
*.log
3+
.idea
4+
node_modules
5+
coverage
6+
.nyc_output

.remarkignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test/snapshots/**/*.md

.travis.yml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
language: node_js
2+
node_js:
3+
- '8'
4+
after_success:
5+
npm run coverage

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2017 Nick Baugh <[email protected]> (http://niftylettuce.com/)
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+245
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
# mongoose-slug-plugin
2+
3+
[![build status](https://img.shields.io/travis/ladjs/mongoose-slug-plugin.svg)](https://travis-ci.org/ladjs/mongoose-slug-plugin)
4+
[![code coverage](https://img.shields.io/codecov/c/github/ladjs/mongoose-slug-plugin.svg)](https://codecov.io/gh/ladjs/mongoose-slug-plugin)
5+
[![code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo)
6+
[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
7+
[![made with lass](https://img.shields.io/badge/made_with-lass-95CC28.svg)](https://lass.js.org)
8+
[![license](https://img.shields.io/github/license/ladjs/mongoose-slug-plugin.svg)](LICENSE)
9+
10+
> 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][])
11+
12+
13+
## Table of Contents
14+
15+
* [Install](#install)
16+
* [Usage](#usage)
17+
* [Options](#options)
18+
* [Slug Uniqueness](#slug-uniqueness)
19+
* [Custom Slug Library](#custom-slug-library)
20+
* [Background](#background)
21+
* [Contributors](#contributors)
22+
* [License](#license)
23+
24+
25+
## Install
26+
27+
[npm][]:
28+
29+
```sh
30+
npm install mongoose-slug-plugin
31+
```
32+
33+
[yarn][]:
34+
35+
```sh
36+
yarn add mongoose-slug-plugin
37+
```
38+
39+
40+
## Usage
41+
42+
> Add the plugin to your project (it will automatically generate a slug when the document is validated based off the template string passed)
43+
44+
```js
45+
const mongooseSlugPlugin = require('mongoose-slug-plugin');
46+
const mongoose = require('mongoose');
47+
48+
const BlogPost = new mongoose.Schema({
49+
title: String
50+
});
51+
52+
BlogPost.plugin(mongooseSlugPlugin, { tmpl: '<%=title%>' });
53+
54+
module.exports = mongoose.model('BlogPost', BlogPost);
55+
```
56+
57+
> If you need to render some custom function in the template string for display purposes, such as outputting a formatted date with [moment][]:
58+
59+
```js
60+
const moment = require('moment');
61+
62+
const mongooseSlugPlugin = require('mongoose-slug-plugin');
63+
const mongoose = require('mongoose');
64+
65+
const BlogPost = new mongoose.Schema({
66+
title: { type: String, required: true, unique: true },
67+
posted_at: { type: Date, required: true }
68+
});
69+
70+
BlogPost.plugin(mongooseSlugPlugin, {
71+
tmpl: "<%=title%>-<%=moment(posted_at).format('YYYY-MM-DD')%>",
72+
locals: { moment }
73+
});
74+
75+
module.exports = mongoose.model('BlogPost', BlogPost);
76+
```
77+
78+
> If you're using [Koa][], here's an example showing how to lookup a slug or an archived slug and properly 301 redirect:
79+
80+
```js
81+
const Koa = require('koa');
82+
const Router = require('koa-router');
83+
const Boom = require('boom');
84+
85+
const BlogPosts = require('./blog-post');
86+
87+
const app = new Koa();
88+
const router = new Router();
89+
90+
router.get('/blog/:slug', async (ctx, next) => {
91+
try {
92+
// lookup the blog post by the slug parameter
93+
const blogPost = await BlogPosts.findOne({ slug: ctx.params.slug });
94+
95+
// if we found it then return early and render the blog post
96+
if (blogPost) return ctx.render('blog-post', { title: blogPost.title, blogPost });
97+
98+
// check if the slug changed for the post we're trying to lookup
99+
blogPost = await BlogPosts.findOne({ slug_history: ctx.params.slug });
100+
101+
// 301 permanent redirect to new blog post slug if it was found
102+
if (blogPost) return ctx.redirect(301, `/blog/${blogPost.slug}`);
103+
104+
// if no blog post found then throw a nice 404 error
105+
// this assumes that you're using `koa-better-error-handler`
106+
// and also using `koa-404-handler`, but you don't necessarily need to
107+
// since koa automatically sets 404 status code if nothing found
108+
// <https://github.com/ladjs/koa-better-error-handler>
109+
// <https://github.com/ladjs/koa-404-handler>
110+
return next();
111+
112+
} catch (err) {
113+
ctx.throw(err);
114+
}
115+
});
116+
117+
app.use(router.routes());
118+
app.listen(3000);
119+
```
120+
121+
> If you're using [Express][], here's an example showing how to lookup a slug or an archived slug and properly 301 redirect:
122+
123+
```js
124+
125+
```
126+
127+
> Note that you also have access to a static function on the model called `getUniqueSlug`.
128+
129+
This function accepts an `_id` and `str` argument. The `_id` being the ObjectID of the document and `str` being the slug you're searching for to ensure uniqueness.
130+
131+
This function is used internally by the plugin to recursively ensure uniqueness.
132+
133+
134+
## Options
135+
136+
Here are the default options passed to the plugin:
137+
138+
* `tmpl` (String) - Required, this should be a [lodash template string][lodash-template-string] (e.g. `<%=title%>` to use the blog post title as the slug)
139+
* `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 use [moment][] to render a document's date formatted in the slug)
140+
* `alwaysUpdateSlug` (Boolean) - Defaults to `true` (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)
141+
* `slug` (Function) - Defaults to `speakingurl`, but it is a function that converts a string into a slug (see below [Custom Slug Libary](#custom-slug-library) examples)
142+
* `errorMessage` (String) - Defaults to `Slug was missing or blank`, this is a String that is returned for failed validation (note that it gets translated based off the `this.locale` field if it is set on the document (see [Lad][] for more insight into how this works))
143+
* `logger` (Object) - defaults to `console`, but you might want to use [Lad's logger][lad-logger]
144+
* `slugField` (String) - defaults to `slug`, this is the field used for storing the slug for the document
145+
* `historyField` (String) - defaults to `slug_history`, this is the field used for storing a document's slug history
146+
* `i18n` (Object|Boolean) - defaults to `false`, but accepts a `i18n` object from [Lad's i18n][i18n]
147+
148+
149+
## Slug Uniqueness
150+
151+
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".
152+
153+
154+
## Custom Slug Library
155+
156+
If you don't want to use the library `speakingurl` for generating slugs (which this package uses by default), then you can pass a custom `slug` function:
157+
158+
> [limax][] example:
159+
160+
```js
161+
const limax = require('limax');
162+
163+
BlogPost.plugin(mongooseSlugPlugin, { tmpl: '<%=title%>', slug: limax });
164+
```
165+
166+
> [slugify][] example:
167+
168+
```js
169+
const slugify = require('slugify');
170+
171+
BlogPost.plugin(mongooseSlugPlugin, { tmpl: '<%=title%>', slug: slugify });
172+
```
173+
174+
> [mollusc][] example:
175+
176+
```js
177+
const slug = require('mollusc');
178+
179+
BlogPost.plugin(mongooseSlugPlugin, { tmpl: '<%=title%>', slug });
180+
```
181+
182+
> [slugme][] example:
183+
184+
```js
185+
const slugme = require('slugme');
186+
187+
BlogPost.plugin(mongooseSlugPlugin, { tmpl: '<%=title%>', slug: slugme });
188+
```
189+
190+
191+
## Background
192+
193+
I created this package despite knowing that other alternatives like it exist for these reasons:
194+
195+
* No alternative supported i18n localization/translation out of the box
196+
* No alternative used the well-tested and SEO-friendly `speakingurl` package
197+
* No alternative allowed users to pass their own slug library
198+
* No alternative documented how to clearly do a 301 permanent redirect for archived slugs
199+
* No alternative allowed the field names to be customized
200+
* No alternative had decent tests written
201+
202+
203+
## Contributors
204+
205+
| Name | Website |
206+
| -------------- | -------------------------- |
207+
| **Nick Baugh** | <http://niftylettuce.com/> |
208+
209+
210+
## License
211+
212+
[MIT](LICENSE) © [Nick Baugh](http://niftylettuce.com/)
213+
214+
215+
##
216+
217+
[npm]: https://www.npmjs.com/
218+
219+
[yarn]: https://yarnpkg.com/
220+
221+
[limax]: https://github.com/lovell/limax
222+
223+
[slugify]: https://github.com/simov/slugify
224+
225+
[mollusc]: https://github.com/Zertz/mollusc
226+
227+
[slugme]: https://github.com/arthurlacoste/js-slug-me
228+
229+
[i18n]: https://github.com/ladjs/i18n
230+
231+
[mongoose]: http://mongoosejs.com/
232+
233+
[speakingurl]: https://github.com/pid/speakingurl
234+
235+
[koa]: http://koajs.com/
236+
237+
[express]: https://expressjs.com/
238+
239+
[lodash-template-string]: https://lodash.com/docs/4.17.4#template
240+
241+
[lad-logger]: https://github.com/ladjs/logger
242+
243+
[moment]: http://momentjs.com/
244+
245+
[lad]: https://lad.js.org

0 commit comments

Comments
 (0)