NestboltNestbolt

@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" — unchanged

This 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" — regenerated

Change 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 change

Change 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:

  1. @Sluggable() decorator onUpdate value
  2. SluggableModule.forRoot() onUpdate value
  3. "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 taken

Non-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 suffix

Events 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.