Skip to content

adonisjs/lucid-slugify

Repository files navigation

Lucid Slugify


gh-workflow-image npm-image license-image typescript-image

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.

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.

{
"fields":

An array of source fields to use for generating the slug. The value of multiple fields is concatenated using the config.separator property.

"strategy":

Reference to pre-existing strategy or an object with the makeSlug and makeSlugUnique methods.

"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 maxLength in following scenarios.

No max length is applied by default.

  • When completeWords is set to true.
  • When using the dbIncrement strategy. The counter value is appended after trimming the value for the maxLength.
"completeWords":

A boolean that forces to complete the words when applying the maxLength property. Completing words will generate a slug larger than the maxLength. So make sure to keep some buffer between the maxLength property and the database storage size.

Complete words is disabled by default.

"separator":

The separator to use for creating the slug. A dash - is used by default.

"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 "1" or "0".

By defining the transformer property you can decide how different data types can be converted to a string.

}

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
  }
}