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.
- Define a maximum length for the slug
- Complete words when truncating the slug
- Generate unique slugs using different strategies
- Add your custom strategies
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.
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
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
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.
Following is the list of available options accepted by the @slugify
decorator.
{ |
|
"fields": |
An array of source fields to use for generating the slug. The value of multiple fields is concatenated using the |
"strategy": |
Reference to pre-existing strategy or an object with the |
"allowUpdates": |
A boolean to enable updates. Updates are disabled by default. |
"maxLength": |
The maximum length for the generated slug. The final slug value can be slightly over the defined No max length is applied by default.
|
"completeWords": |
A boolean that forces to complete the words when applying the Complete words is disabled by default. |
"separator": |
The separator to use for creating the slug. A dash |
"transformer": |
A custom function to convert non-string data types to a string value. For example, if the source field from which slug is generated is a boolean, then we will convert it to
By defining the |
} |
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.
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.
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
}
const post = new Post()
post.title = 'Hello world'
await post.save()
+----+-------------+---------------+
| id | title | slug |
+----+-------------+---------------+
| 1 | Hello world | hello-world |
| 2 | Hello world | hello-world-1 |
+----+-------------+---------------+
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.
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
}
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 |
+----+-------------+------------------------+
You can add custom strategies using two different ways.
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']
})
This is the recommended approach when you are distributing your strategy as an npm package. Every strategy must implement the SlugifyStrategyContract
interface.
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 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)
})
}
}
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
}
}