@nestbolt/sluggable
v0.1.0
Published
Auto-generate URL slugs for NestJS with TypeORM — unique slugs, collision handling, transliteration, and translatable integration.
Maintainers
Readme
This package provides automatic URL slug generation for NestJS with TypeORM that generates unique, URL-friendly slugs from entity fields with collision handling and built-in transliteration for Arabic, Cyrillic, and accented characters.
Once installed, using it is as simple as:
@Sluggable({ from: "title" })
@Entity()
class Post {
@Column() title: string;
@Column() slug: string; // Auto-generated: "my-awesome-post"
}Table of Contents
- Installation
- Quick Start
- Module Configuration
- Using the Decorator
- Using the Mixin
- Using the Service Directly
- Collision Handling
- Transliteration
- Update Behavior
- Multiple Source Fields
- Events
- Configuration Options
- Standalone Usage
- Testing
- Changelog
- Contributing
- Security
- Credits
- License
Installation
Install the package via npm:
npm install @nestbolt/sluggableOr via yarn:
yarn add @nestbolt/sluggableOr via pnpm:
pnpm add @nestbolt/sluggablePeer Dependencies
This package requires the following peer dependencies:
@nestjs/common ^10.0.0 || ^11.0.0
@nestjs/core ^10.0.0 || ^11.0.0
typeorm ^0.3.0
reflect-metadata ^0.1.13 || ^0.2.0Optional:
@nestjs/event-emitter ^2.0.0 || ^3.0.0Quick Start
- Register the module in your
AppModule:
import { SluggableModule } from "@nestbolt/sluggable";
@Module({
imports: [
TypeOrmModule.forRoot({
/* ... */
}),
SluggableModule.forRoot(),
],
})
export class AppModule {}- Add the decorator to your entity:
import { Sluggable } from "@nestbolt/sluggable";
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
@Sluggable({ from: "title" })
@Entity("posts")
export class Post {
@PrimaryGeneratedColumn("uuid")
id: string;
@Column()
title: string;
@Column()
slug: string; // Auto-generated on insert
}- Save an entity and the slug is generated automatically:
const post = postRepo.create({ title: "My Awesome Post" });
await postRepo.save(post);
console.log(post.slug); // "my-awesome-post"Module Configuration
Static Configuration (forRoot)
SluggableModule.forRoot({
separator: "-", // Word separator (default: '-')
maxLength: 255, // Max slug length (default: 255)
lowercase: true, // Lowercase slugs (default: true)
transliterate: true, // Enable transliteration (default: true)
onUpdate: "keep", // 'keep' or 'regenerate' (default: 'keep')
suffixSeparator: "-", // Collision suffix separator (default: '-')
});Async Configuration (forRootAsync)
SluggableModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
maxLength: config.get("SLUG_MAX_LENGTH", 100),
onUpdate: config.get("SLUG_ON_UPDATE", "keep"),
}),
});The module is registered as global — SluggableService is available everywhere without re-importing.
Using the Decorator
The @Sluggable() class decorator configures slug generation for an entity:
@Sluggable({
from: 'title', // Source field(s) — required
slugField: 'slug', // Target field (default: 'slug')
separator: '-', // Word separator override
maxLength: 100, // Max length override
onUpdate: 'keep', // 'keep' or 'regenerate'
unique: true, // Collision handling (default: true)
})Using the Mixin
The SluggableMixin() adds instance methods to your entity:
import { Sluggable, SluggableMixin } from "@nestbolt/sluggable";
import { BaseEntity } from "typeorm";
@Sluggable({ from: "title" })
@Entity()
class Post extends SluggableMixin(BaseEntity) {
/* ... */
}| Method | Returns | Description |
| ------------------ | ---------------------- | ------------------------------ |
| getSlug() | string | Get current slug value |
| getSlugField() | string | Get slug column name |
| findBySlug(slug) | Promise<any \| null> | Find entity by slug |
| regenerateSlug() | Promise<string> | Regenerate and return new slug |
Using the Service Directly
Inject SluggableService for programmatic control:
| Method | Returns | Description |
| ------------------------------------------------------- | -------------------- | ---------------------------------- |
| generateSlug(input, overrides?) | string | Generate slug from text |
| generateUniqueSlug(Entity, field, base, excludeId?) | Promise<string> | Generate unique slug with DB check |
| findBySlug<T>(Entity, field, slug) | Promise<T \| null> | Find entity by slug |
| regenerateSlug(entity, fields, slugField, overrides?) | Promise<string> | Regenerate for existing entity |
Collision Handling
When unique: true (default), the package queries the database for existing slugs and appends a numeric suffix:
my-post (first)
my-post-1 (second with same title)
my-post-2 (third with same title)The suffix separator can be customized via suffixSeparator in module options.
Transliteration
Built-in transliteration converts non-Latin characters to ASCII:
// Arabic
sluggableService.generateSlug("مرحبا بالعالم"); // "mrhba-balalm"
// Cyrillic
sluggableService.generateSlug("Привет мир"); // "privet-mir"
// Accented Latin
sluggableService.generateSlug("Cafe Resume"); // "cafe-resume"
// German
sluggableService.generateSlug("Uber Munchen"); // "ueber-muenchen"Custom Transliterator
Provide your own transliteration function:
SluggableModule.forRoot({
transliterator: (input) => myCustomTransliterate(input),
});Using the Utilities Standalone
The slugify and transliterate functions are exported for standalone use:
import { slugify, transliterate } from "@nestbolt/sluggable";
const slug = slugify(transliterate("Cafe Resume")); // "cafe-resume"Update Behavior
Control what happens to the slug when an entity is updated:
'keep'(default) — Keeps the original slug, even if the source field changes'regenerate'— Generates a new slug when source fields change
@Sluggable({ from: 'title', onUpdate: 'regenerate' })You can set the default behavior at the module level and override per entity.
Multiple Source Fields
Generate slugs from multiple fields:
@Sluggable({ from: ["firstName", "lastName"] })
@Entity()
class User {
@Column() firstName: string;
@Column() lastName: string;
@Column() slug: string; // "john-doe"
}Events
When @nestjs/event-emitter is installed, the following events are emitted:
| Event | Payload |
| ---------------------------- | -------------------------------------------- |
| sluggable.slug-generated | { entity, slug, sourceFields, sourceText } |
| sluggable.slug-regenerated | { entity, oldSlug, newSlug, sourceFields } |
Configuration Options
Module Options
| Option | Type | Default | Description |
| ----------------- | ------------------------ | -------- | ------------------------------- |
| separator | string | '-' | Word separator |
| maxLength | number | 255 | Maximum slug length |
| lowercase | boolean | true | Lowercase slugs |
| transliterate | boolean | true | Enable transliteration |
| transliterator | Function | built-in | Custom transliteration function |
| onUpdate | 'keep' \| 'regenerate' | 'keep' | Slug update behavior |
| suffixSeparator | string | '-' | Collision suffix separator |
Decorator Options
| Option | Type | Default | Description |
| ----------- | ------------------------ | -------------- | ------------------------- |
| from | string \| string[] | required | Source field(s) |
| slugField | string | 'slug' | Target field |
| separator | string | module default | Word separator override |
| maxLength | number | module default | Max length override |
| onUpdate | 'keep' \| 'regenerate' | module default | Update behavior override |
| unique | boolean | true | Enable collision handling |
Standalone Usage
Use slugify and transliterate without the NestJS module:
import { slugify, transliterate } from "@nestbolt/sluggable";
const slug = slugify("Hello World!"); // "hello-world"
const slug2 = slugify("Hello", { separator: "_" }); // "hello"
const latin = transliterate("Привет"); // "Privet"Testing
npm testRun tests in watch mode:
npm run test:watchGenerate coverage report:
npm run test:covChangelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security
If you discover any security-related issues, please report them via GitHub Issues with the security label instead of using the public issue tracker.
License
The MIT License (MIT). Please see License File for more information.
