Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/adonisjs/lucid-slugify
Generate unique slugs using your Lucid models
https://github.com/adonisjs/lucid-slugify
first-party-package lucid
Last synced: 4 days ago
JSON representation
Generate unique slugs using your Lucid models
- Host: GitHub
- URL: https://github.com/adonisjs/lucid-slugify
- Owner: adonisjs
- License: mit
- Created: 2018-02-24T07:35:11.000Z (almost 7 years ago)
- Default Branch: develop
- Last Pushed: 2024-08-12T08:47:35.000Z (4 months ago)
- Last Synced: 2024-12-11T01:05:58.969Z (11 days ago)
- Topics: first-party-package, lucid
- Language: TypeScript
- Homepage:
- Size: 1.08 MB
- Stars: 107
- Watchers: 8
- Forks: 8
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- Contributing: .github/CONTRIBUTING.md
- License: LICENSE.md
Awesome Lists containing this project
- awesome-adonisjs - Adonis Lucid slugify - Generate unique slugs from your Lucid models (Packages)
README
# Lucid Slugify
---
[![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] [![license-image]][license-url] [![typescript-image]][typescript-url]
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:```sh
npm i @adonisjs/lucid-slugify
```And then configure the package as follows:
```sh
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**.
```ts
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.
```ts
@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.
```ts
@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 theconfig.separator
property.
"strategy":
Reference to pre-existing strategy or an object with themakeSlug
andmakeSlugUnique
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 definedmaxLength
in following scenarios.
No max length is applied by default.
-
WhencompleteWords
is set to true.
-
When using thedbIncrement
strategy. The counter value is appended after trimming the value for themaxLength
.
"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
```ts
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
```ts
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.
```ts
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.
```ts
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.
```ts
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
```ts
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.
```ts
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](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces) to add the property to the `StrategiesList` interface.
```ts
declare module '@ioc:Adonis/Addons/LucidSlugify' {
interface StrategiesList {
strategyName: SlugifyStrategyContract
}
}
```
[gh-workflow-image]: https://img.shields.io/github/workflow/status/adonisjs/lucid-slugify/test?style=for-the-badge
[gh-workflow-url]: https://github.com/adonisjs/lucid-slugify/actions/workflows/test.yml "Github action"
[npm-image]: https://img.shields.io/npm/v/@adonisjs/lucid-slugify.svg?style=for-the-badge&logo=npm
[npm-url]: https://npmjs.org/package/@adonisjs/lucid-slugify "npm"
[license-image]: https://img.shields.io/npm/l/@adonisjs/lucid-slugify?color=blueviolet&style=for-the-badge
[license-url]: LICENSE.md "license"
[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
[typescript-url]: "typescript"