StackPractices
beginner Por Mathias Paulenko

Cómo Generar Slugs para URLs (Python, JS, Java)

Cómo generar slugs limpios y amigables para URLs desde strings en Python, JavaScript, Java y Go, con transliteración y manejo de unicidad.

Temas: data

Visión General

Los títulos de artículos, nombres de productos y texto generado por usuarios contienen espacios, acentos y puntuación que rompen las URLs o las vuelven ilegibles (/posts/Café%20&%20Crème!). Un slug convierte esa entrada en un identificador seguro y legible como cafe-creme-brulee. El trabajo consiste en transliterar caracteres no ASCII, eliminar símbolos especiales, colapsar espacios en blanco y —la parte que la mayoría de ejemplos omite— garantizar unicidad cuando dos títulos producen el mismo slug.

Cuándo Usar

Usa este recurso cuando:

  • Conviertas títulos de artículos, nombres de productos o contenido generado por usuarios en URLs permanentes
  • Construyas CMSs, blogs, plataformas de e-commerce o cualquier app con URLs visibles para usuarios
  • Normalices nombres de archivos para assets subidos y evites problemas de codificación
  • Crees rutas amigables para SEO en contenido multi-idioma
  • Generes slugs a partir de input del usuario — valida y sanitiza primero, como cubre la receta de validación de entrada

No uses slugs para identificadores que nunca aparecen en una URL. Un UUID o un ID numérico es más simple y evita por completo el problema de unicidad.

Solución

Los snippets están listos para copiar y pegar; las versiones ejecutables de los cuatro lenguajes están en el repositorio complementario.

Python

import re
import unicodedata

def generate_slug(text):
    # Normalizar unicode y remover acentos
    text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore').decode('ascii')
    # Minúsculas y reemplazar no alfanuméricos con guiones
    text = re.sub(r'[^\w\s-]', '', text.lower())
    # Colapsar múltiples guiones/espacios y recortar extremos
    text = re.sub(r'[-\s]+', '-', text).strip('-_')
    return text

print(generate_slug("Hello, World! 2024"))    # hello-world-2024
print(generate_slug("Café & Crème Brûlée"))   # cafe-creme-brulee

JavaScript

function generateSlug(text) {
    return text
        .normalize('NFKD')
        .replace(/[\u0300-\u036f]/g, '')  // Remover diacríticos
        .toLowerCase()
        .trim()
        .replace(/[^\w\s-]/g, '')
        .replace(/[-\s]+/g, '-')
        .replace(/^-+|-+$/g, '');         // Recortar guiones de los extremos
}

console.log(generateSlug("Hello, World! 2024"));    // hello-world-2024
console.log(generateSlug("Café & Crème Brûlée"));   // cafe-creme-brulee
// Usando la popular librería slugify
// npm install slugify
import slugify from 'slugify';

slugify('Hello, World! 2024');    // hello-world-2024
slugify('Café & Crème Brûlée');   // cafe-creme-brulee

Java

import java.text.Normalizer;
import java.util.Locale;

public class SlugGenerator {
    public static String generateSlug(String input) {
        String normalized = Normalizer.normalize(input, Normalizer.Form.NFKD)
            .replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
        return normalized.toLowerCase(Locale.ROOT)
            .replaceAll("[^\\w\\s-]", "")
            .replaceAll("[-\\s]+", "-")
            .replaceAll("^-+|-+$", "");   // Recortar guiones de los extremos
    }

    public static void main(String[] args) {
        System.out.println(generateSlug("Hello, World! 2024"));    // hello-world-2024
        System.out.println(generateSlug("Café & Crème Brûlée"));   // cafe-creme-brulee
    }
}

Explicación

El algoritmo central es consistente entre lenguajes:

  1. Normalización Unicode (NFKD) descompone caracteres acentuados en base + marca combinada, así é se convierte en e más un diacrítico eliminable
  2. Eliminar diacríticos deja solo letras ASCII
  3. Minúsculas produce URLs insensibles a mayúsculas
  4. Eliminar caracteres especiales excepto alfanuméricos, guiones y espacios
  5. Colapsar separadores en guiones simples y recortar los extremos
Pipeline de generación de slug: un título original pasa por normalización NFKD, eliminación de diacríticos, minúsculas, quitar caracteres especiales, colapsar separadores y recorte de extremos hasta producir un slug limpio para URL

El trabajo pesado lo hacen expresiones regulares — mira la receta de expresiones regulares si los patrones no te resultan familiares. El resultado es un string solo ASCII, en minúsculas y delimitado por guiones, seguro para URLs, nombres de archivo e IDs.

Manejo de Scripts No Latinos

Para lenguajes como chino, japonés, árabe o ruso, la normalización NFKD sola no basta: esos caracteres no se descomponen en ASCII. Necesitas transliteración:

from slugify import slugify

print(slugify('你好世界'))      # ni-hao-shi-jie
print(slugify('Привет мир'))  # privet-mir
import slugify from '@sindresorhus/slugify';

console.log(slugify('你好世界'));  // ni-hao-shi-jie

Si la transliteración no está disponible, genera un ID aleatorio o usa un sufijo numérico como fallback.

Python con Manejo de Unicidad

import re
import unicodedata

def generate_slug(text, existing_slugs=None, max_length=100):
    text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore').decode('ascii')
    text = re.sub(r'[^\w\s-]', '', text.lower())
    text = re.sub(r'[-\s]+', '-', text).strip('-_')
    text = text[:max_length].rstrip('-')

    if existing_slugs is None:
        return text

    base = text
    counter = 2
    while text in existing_slugs:
        suffix = f'-{counter}'
        text = base[:max_length - len(suffix)] + suffix
        counter += 1
    return text

existing = {'hello-world', 'hello-world-2'}
print(generate_slug('Hello World', existing))  # hello-world-3

JavaScript con Opciones Personalizadas

function generateSlug(text, options = {}) {
  const {
    replacement = '-',
    remove = /[^\w\s-]/g,
    lower = true,
    strict = true,
  } = options;

  let result = text
    .normalize('NFKD')
    .replace(/[\u0300-\u036f]/g, '')
    .replace(remove, '')
    .trim()
    .replace(/[-\s]+/g, replacement);

  if (lower) result = result.toLowerCase();
  if (strict) result = result.replace(/[^a-z0-9-]/g, '');

  return result;
}

Go (Usando golang.org/x/text)

package main

import (
    "fmt"
    "regexp"
    "strings"
    "unicode"

    "golang.org/x/text/unicode/norm"
)

func generateSlug(text string) string {
    t := norm.NFKD.String(text)

    var b strings.Builder
    for _, r := range t {
        if unicode.Is(unicode.Mn, r) {
            continue
        }
        b.WriteRune(r)
    }
    text = strings.ToLower(b.String())

    reg := regexp.MustCompile(`[^a-z0-9]+`)
    text = reg.ReplaceAllString(text, "-")

    return strings.Trim(text, "-")
}

func main() {
    fmt.Println(generateSlug("Hello, World! 2024"))   // hello-world-2024
    fmt.Println(generateSlug("Café & Crème Brûlée"))  // cafe-creme-brulee
}

Variantes

TecnologíaLibreríaEnfoqueNotas
Pythonpython-slugifyslugify()Maneja unicode, soporta reglas específicas por idioma
Pythonunicodedata + reManualSin dependencias, control total
JavaScriptslugifyslugify()Ligera, soporta reemplazos personalizados
JavaScriptManualnormalize() + regexCero dependencias
Javaslugify (Maven)SlugifySoporta reemplazos personalizados y reglas por idioma
JavaNormalizer + regexManualIncluido en el JDK, sin dependencias externas
Gogolang.org/x/textnorm.NFKD + regexStdlib extendido, sin dependencias de terceros

Lo que funciona

  • Siempre normaliza Unicode primero: si quitas diacríticos antes de NFKD, é, ñ y el input CJK producen resultados incorrectos
  • Impón una longitud máxima (~100 caracteres) para prevenir URLs excesivamente largas y columnas de base de datos sobredimensionadas; la receta de truncar texto cubre cómo cortar en límites de palabra
  • Verifica unicidad contra los slugs existentes en tu base de datos; añade -2, -3 si hace falta
  • Evita guiones al inicio y al final: se ven poco profesionales y pueden romper la resolución de URLs relativas
  • Usa minúsculas exclusivamente: las URLs distinguen mayúsculas en la mayoría de servidores Unix
  • Prueba con casos borde: strings vacíos, input de solo caracteres especiales, strings muy largos y scripts mezclados
  • Aplica reglas por locale donde importen: alemán ä → ae y ß → ss, turco İ → i
  • Cachea los slugs generados si los calculas en cada carga de página: la normalización NFKD sobre un catálogo grande se acumula

Errores Comunes

  • Quitar acentos sin normalización NFKD: los caracteres acentuados sobreviven o se eliminan por completo en lugar de convertirse en su forma base
  • Dejar pasar caracteres reservados de URL: #, %, ?, &, / y espacios sin codificar rompen el routing y el parseo de queries
  • No limitar la longitud del slug: los slugs extremadamente largos dañan la legibilidad y pueden exceder los límites de columnas de base de datos
  • Ignorar slugs duplicados: dos artículos con el mismo título colisionarán sin una estrategia de unicidad
  • Traducir en lugar de transliterar: traducir “hello” al español no es lo mismo que hacerlo seguro para URLs
  • No manejar slugs vacíos: un input de solo caracteres especiales produce un string vacío — decide un fallback (ID aleatorio, ID del registro o un error)
  • Usar guiones bajos en lugar de guiones: los guiones bajos son válidos en URLs pero Google trata los guiones como separadores de palabras, lo cual es mejor para SEO
  • No eliminar separadores consecutivos: hello---world se ve roto

Solución de Problemas

  • El slug sale vacío: el input no tenía caracteres alfanuméricos ASCII tras la normalización. Comprueba la salida vacía y usa un sufijo aleatorio o el ID del registro como fallback.
  • Títulos distintos producen el mismo slug: probablemente estás eliminando caracteres antes de normalizar, así que el input acentuado colapsa en la misma salida. Ejecuta NFKD primero y elimina después.
  • Colisiones con escrituras concurrentes: dos requests pueden pasar la verificación de unicidad a la vez. Impón un índice único y reintenta con un sufijo, o envuelve la comprobación e inserción en una transacción.
  • Cambiar un título cambia la URL silenciosamente: regenerar el slug en cada render hace que los enlaces viejos se rompan. Guarda el slug una sola vez y cámbialo solo de forma deliberada, con un redirect 301.

Lectura Adicional

Preguntas frecuentes

¿Cómo manejo scripts completamente no latinos como chino o árabe?

Para chino, usa librerías de romanización Pinyin (pypinyin en Python, pinyin en JS). Para árabe, persa o cirílico, usa librerías de transliteración (unidecode, transliteration). Como fallback, genera un ID aleatorio o usa un sufijo numérico.

¿Los slugs deben ser únicos globalmente o por usuario?

Depende de la estructura de tus URLs. Si las URLs son /posts/:slug, los slugs deben ser globalmente únicos. Si son /:username/:slug, la unicidad solo necesita garantizarse por usuario. Siempre indexa la columna slug para búsquedas rápidas.

¿Puedo cambiar un slug después de publicar?

Cambiar slugs rompe enlaces existentes y marcadores. Si debes cambiar un slug, implementa un redirect 301 del slug antiguo al nuevo. Guarda el slug antiguo en una tabla de redirects para preservar el valor SEO.

¿Debo almacenar slugs en la base de datos o generarlos al vuelo?

Almacénalos. Generar slugs al vuelo significa que un cambio de título rompe URLs silenciosamente. Guarda el slug como una columna con índice único. Cuando el título cambie, decide si actualizar el slug (con un redirect) o mantener el antiguo.

¿Cómo manejo colisiones de slugs a escala?

En sitios de alto tráfico, añadir -2, -3 puede provocar race conditions. Usa una transacción de base de datos con SELECT ... FOR UPDATE para comprobar e insertar atómicamente. Alternativamente, añade un sufijo aleatorio corto (ej. 4 caracteres) para garantizar unicidad sin una consulta previa.

¿Cuál es la longitud ideal de un slug?

Mantén los slugs entre 3 y 75 caracteres. Los motores de búsqueda truncan URLs alrededor de 60-75 caracteres en las páginas de resultados. Los slugs cortos (menos de 3 caracteres) son ambiguos y pueden chocar con rutas reservadas. Trunca en el último límite de palabra completo para no partir palabras.

¿Cómo slugifico contenido con emojis?

Elimina los emojis por completo. La mayoría de parsers de URLs y navegadores no manejan URLs con emoji de forma confiable. En Python, usa re.sub(r'[\U0001F600-\U0001F64F]', '', text) o la librería emoji para eliminarlos. En JavaScript, usa text.replace(/\p{Extended_Pictographic}/gu, '').

¿Los slugs distinguen mayúsculas de minúsculas?

Técnicamente, las URLs distinguen mayúsculas según RFC 3986. En la práctica, la mayoría de servidores las tratan como insensibles, pero no está garantizado. Usa siempre minúsculas para evitar ambigüedad. Algunos servidores (nginx, Apache) pueden redirigir URLs en mayúsculas a minúsculas por consistencia.

¿Qué librerías manejan transliteración para scripts no latinos?

En Python, python-slugify incluye transliteración para la mayoría de scripts. unidecode maneja muchos scripts pero produce salida solo ASCII. En JavaScript, @sindresorhus/slugify y el paquete transliteration cubren chino, cirílico y árabe. En Java, junidecode te da funcionalidad similar. Prueba con tu contenido real — la calidad de transliteración varía entre librerías.

¿Debo usar guiones o guiones bajos en los slugs?

Usa guiones. Google trata los guiones como separadores de palabras, lo que mejora el SEO. Los guiones bajos son válidos en URLs pero Google no los reconoce como separadores, así que hello_world se interpreta como una sola palabra.