JSPM

@classytic/mongoose-slug-plugin

1.0.1
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 17
  • Score
    100M100P100Q47245F
  • License MIT

A robust Mongoose plugin for automatic slug generation with duplicate handling

Package Exports

  • @classytic/mongoose-slug-plugin

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.