npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@classytic/mongoose-slug-plugin

v1.0.1

Published

A robust Mongoose plugin for automatic slug generation with duplicate handling

Downloads

43

Readme

mongoose-slug-plugin

A robust Mongoose plugin for automatic slug generation with intelligent duplicate handling.

Features

  • Mongoose 9 Compatible (with backward compatibility for v6/v7/v8)
  • Automatic slug generation from any source field (e.g., title, name)
  • Duplicate handling with incremental suffixes (Amazon-style: "product", "product-1", "product-2")
  • Global uniqueness across your entire platform (like Amazon, eBay)
  • Works on create and update operations
  • Preserves manual slugs if explicitly provided
  • Customizable slugify options
  • Production-ready with comprehensive error handling

Installation

The package is already included in your project. The required peer dependency slugify should be installed:

npm install slugify

Quick Start

Basic Usage

import mongoose from 'mongoose';
import slugPlugin from '#lib/mongoose-slug-plugin/index.js';

const productSchema = new mongoose.Schema({
  title: { type: String, required: true },
  slug: { type: String, required: true, lowercase: true },
});

// Add the plugin
productSchema.plugin(slugPlugin, {
  sourceField: 'title'
});

const Product = mongoose.model('Product', productSchema);

// Create a product
const product = await Product.create({
  title: 'Awesome Product'
});

console.log(product.slug); // "awesome-product"

E-commerce Usage (Global Uniqueness)

For e-commerce platforms like Amazon, eBay, etc., slugs must be globally unique across all organizations:

const productSchema = new mongoose.Schema({
  organizationId: { type: Schema.Types.ObjectId, required: true },
  title: { type: String, required: true },
  slug: { type: String, lowercase: true },
});

// Global unique index on slug
productSchema.index({ slug: 1 }, { unique: true });

productSchema.plugin(slugPlugin, {
  sourceField: 'title'
  // No scopeFields - globally unique!
});

// Organization A creates "wireless-headphones"
await Product.create({
  organizationId: orgA,
  title: 'Wireless Headphones'
  // slug: "wireless-headphones"
});

// Organization B tries same title → gets incremented slug
await Product.create({
  organizationId: orgB,
  title: 'Wireless Headphones'
  // slug: "wireless-headphones-1"
});

// URLs are globally unique across platform:
// → yourplatform.com/products/wireless-headphones
// → yourplatform.com/products/wireless-headphones-1

API

Plugin Options

schema.plugin(slugPlugin, options);

| Option | Type | Default | Description | |--------|------|---------|-------------| | sourceField | string | required | Field to generate slug from (e.g., 'title', 'name') | | slugField | string | 'slug' | Field to store the generated slug | | scopeFields | string[] | [] | Optional: Fields to scope uniqueness (e.g., ['category']). Omit for global uniqueness (recommended for e-commerce). | | slugifyOptions | object | See below | Custom slugify options | | updateOnChange | boolean | false | Regenerate slug when source field changes on updates |

Default Slugify Options

{
  lower: true,                  // Convert to lowercase
  strict: true,                 // Strip special characters
  trim: true,                   // Trim leading/trailing chars
  remove: /[*+~.()'"!:@]/g     // Remove these characters
}

Examples

Example 1: Basic Product

const productSchema = new Schema({
  title: String,
  slug: String,
});

productSchema.plugin(slugPlugin, {
  sourceField: 'title'
});

await Product.create({ title: 'Coffee Maker' });
// slug: "coffee-maker"

await Product.create({ title: 'Coffee Maker' });
// slug: "coffee-maker-1"

await Product.create({ title: 'Coffee Maker' });
// slug: "coffee-maker-2"

Example 2: Multi-tenant Course Platform

const courseSchema = new Schema({
  organizationId: { type: Schema.Types.ObjectId, required: true },
  title: String,
  slug: String,
});

courseSchema.plugin(slugPlugin, {
  sourceField: 'title',
  scopeFields: ['organizationId']
});

// Each organization can have its own "intro-to-programming" course
await Course.create({
  organizationId: org1,
  title: 'Intro to Programming'
  // slug: "intro-to-programming"
});

await Course.create({
  organizationId: org2,
  title: 'Intro to Programming'
  // slug: "intro-to-programming" (different org)
});

Example 3: Custom Slug Field

const blogSchema = new Schema({
  title: String,
  urlSlug: String,
});

blogSchema.plugin(slugPlugin, {
  sourceField: 'title',
  slugField: 'urlSlug'
});

await Blog.create({ title: 'My First Post' });
// urlSlug: "my-first-post"

Example 4: Manual Slug Override

// If you provide a slug manually, the plugin won't generate one
await Product.create({
  title: 'Special Product',
  slug: 'custom-slug-123'
});
// slug: "custom-slug-123" (manual slug preserved)

Example 5: Update with Slug Regeneration

const productSchema = new Schema({
  title: String,
  slug: String,
});

productSchema.plugin(slugPlugin, {
  sourceField: 'title',
  updateOnChange: true  // Regenerate slug on title change
});

const product = await Product.create({ title: 'Old Name' });
// slug: "old-name"

await product.updateOne({ title: 'New Name' });
// slug: "new-name" (regenerated)

Example 6: Multiple Scope Fields

const productSchema = new Schema({
  organizationId: Schema.Types.ObjectId,
  category: String,
  name: String,
  slug: String,
});

productSchema.plugin(slugPlugin, {
  sourceField: 'name',
  scopeFields: ['organizationId', 'category']
});

// Unique slug per organization AND category
await Product.create({
  organizationId: org1,
  category: 'electronics',
  name: 'Laptop'
  // slug: "laptop"
});

await Product.create({
  organizationId: org1,
  category: 'furniture',
  name: 'Laptop'
  // slug: "laptop" (different category)
});

await Product.create({
  organizationId: org1,
  category: 'electronics',
  name: 'Laptop'
  // slug: "laptop-1" (same org + category)
});

How It Works

  1. On Create: When a new document is created, the plugin:

    • Checks if a slug is manually provided → uses it as-is
    • Otherwise, generates a base slug from the sourceField
    • Checks for duplicates within the specified scope
    • Appends -1, -2, etc. if duplicates exist
  2. On Update: When updating via save() or findOneAndUpdate():

    • By default, doesn't regenerate slug
    • If updateOnChange: true, regenerates when source field changes
    • Checks for duplicates and handles them appropriately
  3. Duplicate Resolution:

    • Uses regex pattern matching to find existing slugs
    • Extracts numbers from slugs like "product-5"
    • Finds the highest number and increments by 1
    • Example sequence: "product""product-1""product-2"

Best Practices

1. Add Unique Index

Always add a unique index on the slug field (and scope fields):

// Single tenant
productSchema.index({ slug: 1 }, { unique: true });

// Multi-tenant
productSchema.index({ organizationId: 1, slug: 1 }, { unique: true });

2. Use Scoped Uniqueness

For multi-tenant apps, always use scopeFields:

schema.plugin(slugPlugin, {
  sourceField: 'title',
  scopeFields: ['organizationId']  // ✅ Correct
});

// ❌ Without scope, slugs would be globally unique across all organizations

3. Don't Auto-Update Slugs

Unless you have a specific reason, keep updateOnChange: false (default):

// ✅ Recommended: Slugs are stable (good for SEO)
schema.plugin(slugPlugin, {
  sourceField: 'title',
  updateOnChange: false
});

// ❌ Avoid: Changing title breaks existing URLs
schema.plugin(slugPlugin, {
  sourceField: 'title',
  updateOnChange: true
});

Error Handling

The plugin throws errors in these cases:

// Missing sourceField
schema.plugin(slugPlugin, {});
// Error: slugPlugin requires a sourceField option

// Non-existent sourceField
schema.plugin(slugPlugin, { sourceField: 'nonExistent' });
// Error: sourceField 'nonExistent' does not exist in schema

// Empty source value
await Product.create({ title: '' });
// Error: Cannot generate slug: title is required

// Empty generated slug
await Product.create({ title: '!@#$%' });
// Error: Cannot generate slug from title: "!@#$%"

Integration with Existing Models

Product Model

// modules/product/product.model.js
import slugPlugin from '#lib/mongoose-slug-plugin/index.js';

productSchema.plugin(slugPlugin, {
  sourceField: 'title',
  scopeFields: ['organizationId']
});

Course Model

// modules/course/models/course.model.js
import slugPlugin from '#lib/mongoose-slug-plugin/index.js';

courseSchema.plugin(slugPlugin, {
  sourceField: 'title',
  scopeFields: ['organizationId']
});

Landing Page Model

// modules/landing/landing.model.js
import slugPlugin from '#lib/mongoose-slug-plugin/index.js';

landingSchema.plugin(slugPlugin, {
  sourceField: 'name',
  scopeFields: ['organizationId']
});

Testing

Test the plugin with various scenarios:

// Test 1: Basic slug generation
const p1 = await Product.create({ title: 'Test Product' });
expect(p1.slug).toBe('test-product');

// Test 2: Duplicate handling
const p2 = await Product.create({ title: 'Test Product' });
expect(p2.slug).toBe('test-product-1');

// Test 3: Scoped uniqueness
const p3 = await Product.create({
  organizationId: org2,
  title: 'Test Product'
});
expect(p3.slug).toBe('test-product'); // Different org

// Test 4: Manual slug
const p4 = await Product.create({
  title: 'Test',
  slug: 'custom'
});
expect(p4.slug).toBe('custom');

// Test 5: Special characters
const p5 = await Product.create({
  title: 'Product w/ Special Chars! @2024'
});
expect(p5.slug).toBe('product-w-special-chars-2024');

License

MIT

Contributing

This package is designed to be extracted as a standalone npm package in the future. Contributions welcome!

Support

For issues or questions, please contact the classytic development team.