@nestbolt/sluggable
Update Behavior
Control what happens to slugs when entity source fields are updated.
By default, slugs are preserved on update. You can configure this globally or per entity to regenerate the slug when source fields change.
Two Modes
Keep (Default)
The slug is preserved when the entity is updated, regardless of changes to source fields:
@Sluggable({ from: "title" }) // onUpdate defaults to "keep"
@Entity("posts")
export class Post { /* ... */ }
const post = await repo.save(repo.create({ title: "Original Title" }));
// slug: "original-title"
post.title = "Updated Title";
await repo.save(post);
// slug: "original-title" — unchangedThis is the recommended default for most use cases. Changing slugs can break existing links and SEO.
Exception: if the slug field is empty on update, a new slug is generated even in "keep" mode. This handles the case where an entity was created with an empty source field and later updated.
Regenerate
The slug is regenerated from source fields whenever they change:
@Sluggable({ from: "title", onUpdate: "regenerate" })
@Entity("pages")
export class Page { /* ... */ }
const page = await repo.save(repo.create({ title: "Original Title" }));
// slug: "original-title"
page.title = "Updated Title";
await repo.save(page);
// slug: "updated-title" — regeneratedChange Detection
In "regenerate" mode, the subscriber checks whether source fields have actually changed before regenerating:
const page = await repo.save(repo.create({ title: "Same Title" }));
// slug: "same-title"
// Save again without changing the title
await repo.save(page);
// slug: "same-title" — no regeneration because fields didn't changeChange detection works by comparing event.entity with event.databaseEntity provided by TypeORM.
Global vs Per-Entity
Set the default globally:
SluggableModule.forRoot({ onUpdate: "regenerate" })Override per entity:
@Sluggable({ from: "title", onUpdate: "keep" })
// This entity keeps slugs even though the global default is "regenerate"The priority order is:
@Sluggable()decoratoronUpdatevalueSluggableModule.forRoot()onUpdatevalue"keep"(built-in default)
Unique Slugs on Regeneration
When a slug is regenerated, the uniqueness check excludes the current entity so it doesn't collide with itself:
@Sluggable({ from: "title", onUpdate: "regenerate" })
@Entity("pages")
export class Page { /* ... */ }
const page1 = await repo.save(repo.create({ title: "Target Title" }));
// slug: "target-title"
const page2 = await repo.save(repo.create({ title: "Other Title" }));
// slug: "other-title"
page2.title = "Target Title";
await repo.save(page2);
// slug: "target-title-1" — unique slug because "target-title" is takenNon-Unique Regeneration
If unique: false is set, regeneration produces the base slug without collision checking:
@Sluggable({ from: "title", onUpdate: "regenerate", unique: false })
@Entity("notes")
export class Note { /* ... */ }
const note1 = await repo.save(repo.create({ title: "Same Title" }));
const note2 = await repo.save(repo.create({ title: "Different" }));
note2.title = "Same Title";
await repo.save(note2);
// note2.slug: "same-title" — no collision suffixEvents on Regeneration
When a slug changes during update, a sluggable.slug-regenerated event is emitted (if @nestjs/event-emitter is installed). If the regenerated slug is the same as the old slug, no event is emitted.
See Events for details.