Awesome
Lucid Slugify
<div align="center"> <img src="./assets/banner.jpg" /> </div>Generating slugs is easy, but keeping them unique and within a maximum length range is hard. This package abstracts the hard parts and gives you a simple API to generate unique slugs.
<!-- START doctoc generated TOC please keep comment here to allow auto update --> <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> <!-- END doctoc generated TOC please keep comment here to allow auto update -->Features
- Define a maximum length for the slug
- Complete words when truncating the slug
- Generate unique slugs using different strategies
- Add your custom strategies
Usage
Install the package from the npm registry as follows:
npm i @adonisjs/lucid-slugify
And then configure the package as follows:
node ace configure @adonisjs/lucid-slugify
Once done, you need to use the following decorator on the field for which you want to generate the slug. Following is an example with the Post
model generating slug from the post title.
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'
import { slugify } from '@ioc:Adonis/Addons/LucidSlugify'
class Post extends BaseModel {
@column({ isPrimary: true })
public id: number
@column()
@slugify({
strategy: 'dbIncrement',
fields: ['title']
})
public slug: string
@column()
public title: string
}
In the above example, the slug
property will be set based upon the value of the title
property.
Updating slugs
By default, slugs are not updated when you update a model instance, and this is how it should be when slugs are used to look up a record, as changing a slug will result in a broken URL.
However, if slugs are not primarily used to look up records, you may want to update them.
You can enable updates by using the allowUpdates
flag.
@slugify({
strategy: 'dbIncrement',
fields: ['title'],
allowUpdates: true,
})
public slug: string
Generate slug from multiple properties
The fields
array can accept multiple model properties and generate a slug by concatenating the values of all the fields.
@slugify({
strategy: 'dbIncrement',
fields: ['country', 'state', 'city'],
allowUpdates: true,
})
public location: string
Null values and slug generation
The slugify
decorator does not generate slugs when the source field(s) value is not defined or null.
In other words, all of the source fields should have a value for the slug to be generated. It is an opinionated choice and not likely to change.
Available options
Following is the list of available options accepted by the @slugify
decorator.
Strategies
Strategies decide how to generate a slug and then make it unique. This package ships with three different strategies.
- simple: Just the slug is generated. No uniqueness guaranteed.
- dbIncrement: Generates unique slugs by adding a counter to the existing similar slug.
- shortId: Appends a short id to the initial slug value to ensure uniqueness.
Db Increment
The Db Increment strategy uses a counter to generate unique slugs. Given the following table structure and data.
+----+-------------+-------------+
| id | title | slug |
+----+-------------+-------------+
| 1 | Hello world | hello-world |
+----+-------------+-------------+
If you generate another slug for the Hello world title, the dbIncrement
strategy will append -1
to ensure slug uniqueness.
Model definition
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'
import { slugify } from '@ioc:Adonis/Addons/LucidSlugify'
class Post extends BaseModel {
@column({ isPrimary: true })
public id: number
@column()
@slugify({
strategy: 'dbIncrement',
fields: ['title']
})
public slug: string
@column()
public title: string
}
Create a new record
const post = new Post()
post.title = 'Hello world'
await post.save()
Database state
+----+-------------+---------------+
| id | title | slug |
+----+-------------+---------------+
| 1 | Hello world | hello-world |
| 2 | Hello world | hello-world-1 |
+----+-------------+---------------+
Implementation details
The implementation details vary a lot across different database drivers.
- PostgreSQL, MsSQL 8.0, and Redshift performs optimized queries to fetch only matching record with the largest counter.
- For SQLite, MySQL < 8.0, and MSSQL, we have to fetch all the matching rows and then find the largest counter in JavaScript.
- The OracleDB implementation is untested (feel free to contribute the tests). However, it also performs an optimized query to fetch only matching records with the largest counter.
Simple
The simple
strategy just generates a slug respecting the maxLength
and completeWords
config options. No uniqueness is guaranteed when using this strategy.
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'
import { slugify } from '@ioc:Adonis/Addons/LucidSlugify'
class Post extends BaseModel {
@column({ isPrimary: true })
public id: number
@column()
@slugify({
strategy: 'simple',
fields: ['title']
})
public slug: string
@column()
public title: string
}
Short Id
The shortId
strategy appends a ten-digit long random short id to the initial slug value for uniqueness. Following is an example of using the shortId
strategy.
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'
import { slugify } from '@ioc:Adonis/Addons/LucidSlugify'
class Post extends BaseModel {
@column({ isPrimary: true })
public id: number
@column()
@slugify({
strategy: 'shortId',
fields: ['title']
})
public slug: string
@column()
public title: string
}
+----+-------------+------------------------+
| id | title | slug |
+----+-------------+------------------------+
| 1 | Hello world | hello-world-yRPZZIWGgC |
+----+-------------+------------------------+
Adding a custom strategy
You can add custom strategies using two different ways.
Inline within the slugify
decorator
The simplest way is to define the strategy inline in the decorator options. A strategy must implement the following two methods.
import { SlugifyStrategyContract } from '@ioc:Adonis/Addons/LucidSlugify'
const myCustomStrategy: SlugifyStrategyContract = {
makeSlug (model, field, value) {
return // slug for the value
},
makeSlugUnique(model, field, slug) {
return // make slug unique
},
}
@slugify({
strategy: myCustomStrategy,
fields: ['title']
})
Extending the slugify
package
This is the recommended approach when you are distributing your strategy as an npm package. Every strategy must implement the SlugifyStrategyContract
interface.
Define strategy
import {
SlugifyConfig,
SlugifyStrategyContract
} from '@ioc:Adonis/Addons/LucidSlugify'
class MyStrategy implements SlugifyStrategyContract {
constructor (private config: SlugifyConfig) {}
makeSlug (
model: LucidModel,
field: string,
value: string
) {}
makeSlugUnique (
model: LucidModel,
field: string,
slug: string
) {}
}
Register the strategy
Register the strategy using the Slugify.extend
method. You must write the following code inside the provider boot
method.
import { ApplicationContract } from '@ioc:Adonis/Core/Application'
export default class AppProvider {
constructor(protected app: ApplicationContract) {}
public async boot() {
const { Slugify } = this.app.container.use('Adonis/Addons/LucidSlugify')
Slugify.extend('strategyName', (slugify, config) => {
return new MyStrategy(config)
})
}
}
Inform typescript about the strategy
Finally, you will also have to inform typescript about the new strategy you added using the Slugify.extend
method. We will use declaration merging to add the property to the StrategiesList
interface.
declare module '@ioc:Adonis/Addons/LucidSlugify' {
interface StrategiesList {
strategyName: SlugifyStrategyContract
}
}