Overview
Generate URL-safe slugs with uniqueness helpers and collision avoidance.
Features
- Slugify integration
- Unique slugs
- Timestamp slugs
- Collision check factory
Installation
npx hanma add slug --framework nest
Dependencies
slugify
Code
import slugify from "slugify";
import { randomBytes } from "crypto";
export interface SlugOptions {
lowercase?: boolean;
strict?: boolean;
locale?: string;
trim?: boolean;
replacement?: string;
}
const DEFAULT_OPTIONS: SlugOptions = {
lowercase: true,
strict: true,
locale: "en",
trim: true,
replacement: "-",
};
/**
* Generate a URL-safe slug from a string.
*/
export function generateSlug(text: string, options?: SlugOptions): string {
const opts = { ...DEFAULT_OPTIONS, ...options };
return slugify(text, opts);
}
/**
* Generate a unique slug with random suffix.
*/
export function generateUniqueSlug(text: string, length = 6): string {
const slug = generateSlug(text);
const suffix = randomBytes(Math.ceil(length / 2))
.toString("hex")
.slice(0, length);
return `${slug}-${suffix}`;
}
/**
* Generate a slug with timestamp for time-based uniqueness.
*/
export function generateTimestampSlug(text: string): string {
const slug = generateSlug(text);
const timestamp = Date.now().toString(36);
return `${slug}-${timestamp}`;
}
/**
* Validate if a string is a valid slug.
*/
export function isValidSlug(slug: string): boolean {
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug);
}
/**
* Sanitize a string to be used as a slug (more lenient than generateSlug).
*/
export function sanitizeSlug(slug: string): string {
return slug
.toLowerCase()
.trim()
.replace(/[^a-z0-9-]/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "");
}
/**
* Create a slug checker factory for database collision avoidance.
*
* Usage:
* const checkSlug = createSlugChecker(async (slug) => {
* const existing = await prisma.post.findUnique({ where: { slug } });
* return existing === null;
* });
*
* const uniqueSlug = await checkSlug("my-post-title");
*/
export function createSlugChecker(
isAvailable: (slug: string) => Promise<boolean>,
maxAttempts = 10
) {
return async function ensureUniqueSlug(
text: string,
existingSlug?: string
): Promise<string> {
const baseSlug = generateSlug(text);
// If updating and slug hasn't changed, return the existing one
if (existingSlug && sanitizeSlug(existingSlug) === baseSlug) {
return existingSlug;
}
// Check if base slug is available
if (await isAvailable(baseSlug)) {
return baseSlug;
}
// Try with numeric suffixes
for (let i = 2; i <= maxAttempts; i++) {
const slug = `${baseSlug}-${i}`;
if (await isAvailable(slug)) {
return slug;
}
}
// Fallback to random suffix
return generateUniqueSlug(text);
};
}
/**
* Extract slug from URL path.
*/
export function extractSlugFromUrl(url: string): string | null {
const match = url.match(/\/([a-z0-9-]+)\/?(?:\?|#|$)/i);
return match ? match[1] : null;
}
/**
* Generate slug with category prefix.
*/
export function generateCategorizedSlug(
category: string,
text: string
): string {
const categorySlug = generateSlug(category);
const textSlug = generateSlug(text);
return `${categorySlug}/${textSlug}`;
}
// Usage:
/*
import { generateSlug, createSlugChecker } from "./utils/slug";
@Injectable()
export class PostService {
private checkSlugAvailability = createSlugChecker(async (slug) => {
const post = await this.prisma.post.findUnique({ where: { slug } });
return post === null;
});
async create(dto: CreatePostDto) {
const slug = await this.checkSlugAvailability(dto.title);
return this.prisma.post.create({
data: {
title: dto.title,
slug,
content: dto.content,
},
});
}
async update(id: string, dto: UpdatePostDto) {
const post = await this.prisma.post.findUnique({ where: { id } });
const slug = dto.title
? await this.checkSlugAvailability(dto.title, post.slug)
: post.slug;
return this.prisma.post.update({
where: { id },
data: { ...dto, slug },
});
}
}
*/
Usage
const slug = await this.checkSlugAvailability(dto.title);
Dependencies
slugify
Frequently Asked Questions
How do I install Slug Generator?
Run the following command:
npx hanma add slug --framework nestWhat dependencies are required?
This snippet requires: slugify. Make sure to install them before using the snippet.
How do I use this snippet?
Copy the code and paste it into your project. The snippet will be saved to
src/utils/slug.ts.Can I customize this snippet?
Yes! All snippets are copy-paste code that you own. Feel free to modify them to fit your needs.
Does this work with other frameworks?
This snippet is designed for the specific framework. Check the other framework pages for equivalent implementations.