¿Cómo genera Stack Overflow sus URLs amigables con SEO?

253

¿Cuál es una buena expresión regular completa o algún otro proceso que tomaría el título:

¿Cómo se cambia un título para que sea parte de la URL como Stack Overflow?

y convertirlo en

how-do-you-change-a-title-to-be-part-of-the-url-like-stack-overflow

que se usa en las URL compatibles con SEO en Stack Overflow?

El entorno de desarrollo que estoy usando es Ruby on Rails , pero si hay otras soluciones específicas de plataforma (.NET, PHP, Django ), me encantaría verlas también.

Estoy seguro de que yo (u otro lector) se encontrará con el mismo problema en una plataforma diferente en el futuro.

Estoy usando rutas personalizadas, y principalmente quiero saber cómo alterar la cadena para que se eliminen todos los caracteres especiales, todo está en minúsculas y se reemplaza todo el espacio en blanco.

wusher
fuente
¿Qué hay de los personajes divertidos? ¿Qué vas a hacer al respecto? Umlauts? ¿Puntuación? Estos deben ser considerados. Básicamente, usaría un enfoque de lista blanca, en oposición a los enfoques de lista negra anteriores: Describa qué caracteres permitirá, qué caracteres convertirá (¿a qué?) Y luego cambie el resto a algo significativo ("") . Dudo que puedas hacer esto en una expresión regular ... ¿Por qué no simplemente recorrer los personajes?
Daren Thomas el
1
Debe migrarse a meta ; ya que la pregunta y la respuesta se refieren específicamente a la implementación de SO, y la respuesta aceptada es de @JeffAtwood.
casperOne
19
@casperOne ¿Crees que a Jeff no se le permite cierta reputación no meta? La pregunta es sobre "cómo se puede hacer algo como esto", no específicamente "cómo se hace esto aquí".
Paŭlo Ebermann
@ PaŭloEbermann: No se trata de que Jeff obtenga una reputación no meta (cuánta reputación tiene realmente no es de mi incumbencia); el cuerpo de la pregunta hace referencia específica a la implementación de StackOverflow, por lo tanto, la razón para que esté en meta.
casperOne

Respuestas:

300

Así es como lo hacemos. Tenga en cuenta que probablemente haya más condiciones de borde de las que cree a primera vista.

Esta es la segunda versión, desenrollada para 5 veces más rendimiento (y sí, la comparé). Pensé que lo optimizaría porque esta función se puede llamar cientos de veces por página.

/// <summary>
/// Produces optional, URL-friendly version of a title, "like-this-one". 
/// hand-tuned for speed, reflects performance refactoring contributed
/// by John Gietzen (user otac0n) 
/// </summary>
public static string URLFriendly(string title)
{
    if (title == null) return "";

    const int maxlen = 80;
    int len = title.Length;
    bool prevdash = false;
    var sb = new StringBuilder(len);
    char c;

    for (int i = 0; i < len; i++)
    {
        c = title[i];
        if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))
        {
            sb.Append(c);
            prevdash = false;
        }
        else if (c >= 'A' && c <= 'Z')
        {
            // tricky way to convert to lowercase
            sb.Append((char)(c | 32));
            prevdash = false;
        }
        else if (c == ' ' || c == ',' || c == '.' || c == '/' || 
            c == '\\' || c == '-' || c == '_' || c == '=')
        {
            if (!prevdash && sb.Length > 0)
            {
                sb.Append('-');
                prevdash = true;
            }
        }
        else if ((int)c >= 128)
        {
            int prevlen = sb.Length;
            sb.Append(RemapInternationalCharToAscii(c));
            if (prevlen != sb.Length) prevdash = false;
        }
        if (i == maxlen) break;
    }

    if (prevdash)
        return sb.ToString().Substring(0, sb.Length - 1);
    else
        return sb.ToString();
}

Para ver la versión anterior del código que reemplazó (pero es funcionalmente equivalente y 5 veces más rápido), vea el historial de revisiones de esta publicación (haga clic en el enlace de la fecha).

Además, el RemapInternationalCharToAsciicódigo fuente del método se puede encontrar aquí .

Jeff Atwood
fuente
24
Sería bueno con una versión que no solo deje caer caracteres acentuados como åäö, sino que los desacentúe a aao ... ^^
Oskar Duveborn
22
@oskar el trozo de esa RemapInternationalCharToAscii()función está allí meta.stackexchange.com/questions/7435/…
Jeff Atwood
3
Esto es genial. El único cambio que he hecho hasta ahora es cambiar "if (i == maxlen) break;" para convertirse en "if (sb.Length == maxlen) break;" en caso de que haya muchos caracteres no válidos en la cadena que estoy pasando.
Tom Chantler
44
Una optimización menor: en if (prevdash) sb.Length -= 1; return sb.ToString();lugar de la última ifdeclaración.
Mark Hurd
8
@Dommer sb.Length == maxlen break;tiene errores si el signo en maxLenght-1 es "ß" se convierte en "ss" sb.Length == maxlenenunca será cierto, es mejor probarlo (sb.Length > = maxlen).
Henrik Stenbæk
32

Aquí está mi versión del código de Jeff. He realizado los siguientes cambios:

  • Los guiones se agregaron de tal manera que se pudieran agregar, y luego se deben eliminar, ya que era el último carácter de la cadena. Es decir, nunca queremos "my-slug-". Esto significa una asignación de cadena adicional para eliminarlo en este caso límite. He trabajado alrededor de esto mediante guiones de retraso. Si compara mi código con el de Jeff, la lógica para esto es fácil de seguir.
  • Su enfoque se basa únicamente en la búsqueda y perdió muchos personajes que encontré en ejemplos mientras investigaba sobre Stack Overflow. Para contrarrestar esto, primero realizo un pase de normalización (clasificación de AKA mencionada en la pregunta de desbordamiento de Meta Stack Los caracteres que no son ASCII de EE. UU. Se eliminan de la URL completa (perfil) ) y, a continuación, ignorar cualquier caracteres fuera de los intervalos aceptables. Esto funciona la mayor parte del tiempo...
  • ... Para cuando no es así, también tuve que agregar una tabla de búsqueda. Como se mencionó anteriormente, algunos caracteres no se asignan a un valor ASCII bajo cuando se normalizan. En lugar de descartarlos, tengo una lista manual de excepciones que sin duda está llena de agujeros, pero es mejor que nada. El código de normalización se inspiró en la gran publicación de Jon Hanna en la pregunta de desbordamiento de pila. ¿ Cómo puedo eliminar los acentos en una cadena? .
  • La conversión del caso ahora también es opcional.

    public static class Slug
    {
        public static string Create(bool toLower, params string[] values)
        {
            return Create(toLower, String.Join("-", values));
        }
    
        /// <summary>
        /// Creates a slug.
        /// References:
        /// http://www.unicode.org/reports/tr15/tr15-34.html
        /// /meta/7435/non-us-ascii-characters-dropped-from-full-profile-url/7696#7696
        /// /programming/25259/how-do-you-include-a-webpage-title-as-part-of-a-webpage-url/25486#25486
        /// /programming/3769457/how-can-i-remove-accents-on-a-string
        /// </summary>
        /// <param name="toLower"></param>
        /// <param name="normalised"></param>
        /// <returns></returns>
        public static string Create(bool toLower, string value)
        {
            if (value == null)
                return "";
    
            var normalised = value.Normalize(NormalizationForm.FormKD);
    
            const int maxlen = 80;
            int len = normalised.Length;
            bool prevDash = false;
            var sb = new StringBuilder(len);
            char c;
    
            for (int i = 0; i < len; i++)
            {
                c = normalised[i];
                if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))
                {
                    if (prevDash)
                    {
                        sb.Append('-');
                        prevDash = false;
                    }
                    sb.Append(c);
                }
                else if (c >= 'A' && c <= 'Z')
                {
                    if (prevDash)
                    {
                        sb.Append('-');
                        prevDash = false;
                    }
                    // Tricky way to convert to lowercase
                    if (toLower)
                        sb.Append((char)(c | 32));
                    else
                        sb.Append(c);
                }
                else if (c == ' ' || c == ',' || c == '.' || c == '/' || c == '\\' || c == '-' || c == '_' || c == '=')
                {
                    if (!prevDash && sb.Length > 0)
                    {
                        prevDash = true;
                    }
                }
                else
                {
                    string swap = ConvertEdgeCases(c, toLower);
    
                    if (swap != null)
                    {
                        if (prevDash)
                        {
                            sb.Append('-');
                            prevDash = false;
                        }
                        sb.Append(swap);
                    }
                }
    
                if (sb.Length == maxlen)
                    break;
            }
            return sb.ToString();
        }
    
        static string ConvertEdgeCases(char c, bool toLower)
        {
            string swap = null;
            switch (c)
            {
                case 'ı':
                    swap = "i";
                    break;
                case 'ł':
                    swap = "l";
                    break;
                case 'Ł':
                    swap = toLower ? "l" : "L";
                    break;
                case 'đ':
                    swap = "d";
                    break;
                case 'ß':
                    swap = "ss";
                    break;
                case 'ø':
                    swap = "o";
                    break;
                case 'Þ':
                    swap = "th";
                    break;
            }
            return swap;
        }
    }

Para obtener más detalles, las pruebas unitarias y una explicación de por qué el esquema de URL de Facebook es un poco más inteligente que Stack Overflows, tengo una versión ampliada de esto en mi blog .

DanH
fuente
44
+1 Esto es genial Dan. También agregué un comentario en su blog sobre el posible cambio if (i == maxlen) break;para que, if (sb.Length == maxlen) break;en su lugar, si pasa una cadena con muchos espacios en blanco / caracteres no válidos, aún pueda obtener una barra de la longitud deseada, mientras que el código tal como está puede terminar truncarlo masivamente (por ejemplo, considere el caso en el que comienza con 80 espacios ...). Y un punto de referencia aproximado de 10,000,000 iteraciones contra el código de Jeff mostró que era aproximadamente la misma velocidad.
Tom Chantler
1
Gracias, respondí en mi blog y arreglé el código allí y más arriba. También gracias por comparar el código. Para aquellos interesados, estaba a la par de Jeff's.
DanH
2
Parece que hay algunos problemas con Slug.Create (): las versiones en mayúsculas de ÆØÅ no se convierten correctamente ÆØ se ignora mientras Å se traduce a a. Normalmente convertirá “å” a “aa”, “ø” a “oe” y “æ” a “ae”. Segundo (sb.Length == maxlen) descanso; tiene errores si el signo en maxLenght-1 es "ß" (sb.Length == maxlen) nunca será cierto, es mejor probarlo (sb.Length> = maxlen). Supongo que corta en cualquier posición aleatoria y no corta en el último "-", esto le evitará terminar con una palabra no deseada al final: como si tuviera que cortar "para afirmar" después de la última "s "
Henrik Stenbæk
@DanH sería genial tener la versión de JavaScript del código.
Freshblood
16

Deberá configurar una ruta personalizada para apuntar la URL al controlador que lo manejará. Como está utilizando Ruby on Rails, aquí hay una introducción al uso de su motor de enrutamiento.

En Ruby, necesitará una expresión regular como ya sabe y aquí está la expresión regular para usar:

def permalink_for(str)
    str.gsub(/[^\w\/]|[!\(\)\.]+/, ' ').strip.downcase.gsub(/\ +/, '-')
end
Dale Ragan
fuente
11

También puede usar esta función de JavaScript para la generación en forma de las babosas (esta se basa en / copiada de Django ):

function makeSlug(urlString, filter) {
    // Changes, e.g., "Petty theft" to "petty_theft".
    // Remove all these words from the string before URLifying

    if(filter) {
        removelist = ["a", "an", "as", "at", "before", "but", "by", "for", "from",
        "is", "in", "into", "like", "of", "off", "on", "onto", "per",
        "since", "than", "the", "this", "that", "to", "up", "via", "het", "de", "een", "en",
        "with"];
    }
    else {
        removelist = [];
    }
    s = urlString;
    r = new RegExp('\\b(' + removelist.join('|') + ')\\b', 'gi');
    s = s.replace(r, '');
    s = s.replace(/[^-\w\s]/g, ''); // Remove unneeded characters
    s = s.replace(/^\s+|\s+$/g, ''); // Trim leading/trailing spaces
    s = s.replace(/[-\s]+/g, '-'); // Convert spaces to hyphens
    s = s.toLowerCase(); // Convert to lowercase
    return s; // Trim to first num_chars characters
}
fijter
fuente
Agregar algunos vamos o const sería genial ya que esto no es vanilla JS.
Aditya Anand
8

En buena medida, aquí está la función PHP en WordPress que lo hace ... Creo que WordPress es una de las plataformas más populares que utiliza enlaces sofisticados.

    función sanitize_title_with_dashes ($ title) {
            $ title = strip_tags ($ title);
            // Preservar octetos escapados.
            $ title = preg_replace ('|% ([a-fA-F0-9] [a-fA-F0-9]) |', '--- $ 1 ---', $ title);
            // Eliminar signos de porcentaje que no son parte de un octeto.
            $ title = str_replace ('%', '', $ title);
            // Restaurar octetos.
            $ title = preg_replace ('| --- ([a-fA-F0-9] [a-fA-F0-9]) --- |', '% $ 1', $ title);
            $ title = remove_accents ($ título);
            if (parece_utf8 ($ título)) {
                    if (function_exists ('mb_strtolower')) {
                            $ title = mb_strtolower ($ title, 'UTF-8');
                    }
                    $ title = utf8_uri_encode ($ title, 200);
            }
            $ title = strtolower ($ title);
            $ title = preg_replace ('/&.+?;/', '', $ title); // matar entidades
            $ title = preg_replace ('/ [^% a-z0-9 _-] /', '', $ title);
            $ title = preg_replace ('/ \ s + /', '-', $ title);
            $ title = preg_replace ('| - + |', '-', $ title);
            $ title = trim ($ title, '-');
            devolver $ título;
    }

Esta función, así como algunas de las funciones de soporte, se pueden encontrar en wp-includes / formatting.php.

El friki de cómo hacerlo
fuente
66
Esta no es la respuesta completa. Se echa en falta funciones como: remove_accents, seems_utf8...
Nikola Loncar
para completar la respuesta de @The How-To Geek que aún puede git clone git://core.git.wordpress.org/y encontrar el wp-includes/formatting.phparchivo en
mickro
5

Si está utilizando Rails edge, puede confiar en Inflector.parametrize ; este es el ejemplo de la documentación:

  class Person
    def to_param
      "#{id}-#{name.parameterize}"
    end
  end

  @person = Person.find(1)
  # => #<Person id: 1, name: "Donald E. Knuth">

  <%= link_to(@person.name, person_path(@person)) %>
  # => <a href="https://stackoverflow.com/person/1-donald-e-knuth">Donald E. Knuth</a>

Además, si necesita manejar caracteres más exóticos como acentos (éphémère) en versiones anteriores de Rails, puede usar una mezcla de PermalinkFu y DiacriticsFu :

DiacriticsFu::escape("éphémère")
=> "ephemere"

DiacriticsFu::escape("räksmörgås")
=> "raksmorgas"
Thibaut Barrère
fuente
5

No estoy familiarizado con Ruby on Rails, pero el siguiente es un código PHP (no probado). Probablemente pueda traducir esto muy rápidamente a Ruby on Rails si lo encuentra útil.

$sURL = "This is a title to convert to URL-format. It has 1 number in it!";
// To lower-case
$sURL = strtolower($sURL);

// Replace all non-word characters with spaces
$sURL = preg_replace("/\W+/", " ", $sURL);

// Remove trailing spaces (so we won't end with a separator)
$sURL = trim($sURL);

// Replace spaces with separators (hyphens)
$sURL = str_replace(" ", "-", $sURL);

echo $sURL;
// outputs: this-is-a-title-to-convert-to-url-format-it-has-1-number-in-it

Espero que esto ayude.

Vegard Larsen
fuente
4

No sé mucho sobre Ruby o Rails, pero en Perl, esto es lo que haría:

my $title = "How do you change a title to be part of the url like Stackoverflow?";

my $url = lc $title;   # Change to lower case and copy to URL.
$url =~ s/^\s+//g;     # Remove leading spaces.
$url =~ s/\s+$//g;     # Remove trailing spaces.
$url =~ s/\s+/\-/g;    # Change one or more spaces to single hyphen.
$url =~ s/[^\w\-]//g;  # Remove any non-word characters.

print "$title\n$url\n";

Acabo de hacer una prueba rápida y parece funcionar. Esperemos que esto sea relativamente fácil de traducir a Ruby.

Brian
fuente
4

Implementación de T-SQL, adaptada de dbo.UrlEncode :

CREATE FUNCTION dbo.Slug(@string varchar(1024))
RETURNS varchar(3072)
AS
BEGIN
    DECLARE @count int, @c char(1), @i int, @slug varchar(3072)

    SET @string = replace(lower(ltrim(rtrim(@string))),' ','-')

    SET @count = Len(@string)
    SET @i = 1
    SET @slug = ''

    WHILE (@i <= @count)
    BEGIN
        SET @c = substring(@string, @i, 1)

        IF @c LIKE '[a-z0-9--]'
            SET @slug = @slug + @c

        SET @i = @i +1
    END

    RETURN @slug
END
Sören Kuklau
fuente
4

Sé que es una pregunta muy antigua, pero dado que la mayoría de los navegadores ahora admiten URL unicode , encontré una gran solución en XRegex que convierte todo excepto las letras (en todos los idiomas a '-').

Eso se puede hacer en varios lenguajes de programación.

El patrón es \\p{^L}+y luego solo necesita usarlo para reemplazar todas las letras que no sean '-'.

Ejemplo de trabajo en node.js con módulo xregex .

var text = 'This ! can @ have # several $ letters % from different languages such as עברית or Español';

var slugRegEx = XRegExp('((?!\\d)\\p{^L})+', 'g');

var slug = XRegExp.replace(text, slugRegEx, '-').toLowerCase();

console.log(slug) ==> "this-can-have-several-letters-from-different-languages-such-as-עברית-or-español"
Rotem
fuente
3

Suponiendo que su clase de modelo tiene un atributo de título, simplemente puede anular el método to_param dentro del modelo, así:

def to_param
  title.downcase.gsub(/ /, '-')
end

Este episodio de Railscast tiene todos los detalles. También puede asegurarse de que el título solo contenga caracteres válidos usando esto:

validates_format_of :title, :with => /^[a-z0-9-]+$/,
                    :message => 'can only contain letters, numbers and hyphens'
John Topley
fuente
2

El código de Brian, en Ruby:

title.downcase.strip.gsub(/\ /, '-').gsub(/[^\w\-]/, '')

downcaseconvierte la cadena a minúsculas, stripelimina espacio inicial y final, la primera gsubllamada g lobally sub espacios stitutes con guiones, y el segundo elimina todo lo que no sea una letra o un guión.

Sören Kuklau
fuente
2

Hay un pequeño complemento de Ruby on Rails llamado PermalinkFu , que hace esto. El método de escape realiza la transformación en una cadena que es adecuada para una URL . Echa un vistazo al código; Ese método es bastante simple.

Para eliminar caracteres no ASCII , utiliza iconv lib para traducir a 'ascii // ignore // translit' de 'utf-8'. Los espacios se convierten en guiones, todo se descarta, etc.

Lau
fuente
Si bien esto funciona perfectamente, de alguna manera siento que no es muy eficiente.
WhyNotHugo
2

Puede usar el siguiente método auxiliar. Puede convertir los caracteres Unicode.

public static string ConvertTextToSlug(string s)
{
    StringBuilder sb = new StringBuilder();

    bool wasHyphen = true;

    foreach (char c in s)
    {
        if (char.IsLetterOrDigit(c))
        {
            sb.Append(char.ToLower(c));
            wasHyphen = false;
        }
        else
            if (char.IsWhiteSpace(c) && !wasHyphen)
            {
                sb.Append('-');
                wasHyphen = true;
            }
    }

    // Avoid trailing hyphens
    if (wasHyphen && sb.Length > 0)
        sb.Length--;

    return sb.ToString().Replace("--","-");
}
Peyman Mehrabani
fuente
2

Aquí está mi versión (más lenta pero divertida de escribir) del código de Jeff:

public static string URLFriendly(string title)
{
    char? prevRead = null,
        prevWritten = null;

    var seq = 
        from c in title
        let norm = RemapInternationalCharToAscii(char.ToLowerInvariant(c).ToString())[0]
        let keep = char.IsLetterOrDigit(norm)
        where prevRead.HasValue || keep
        let replaced = keep ? norm
            :  prevWritten != '-' ? '-'
            :  (char?)null
        where replaced != null
        let s = replaced + (prevRead == null ? ""
            : norm == '#' && "cf".Contains(prevRead.Value) ? "sharp"
            : norm == '+' ? "plus"
            : "")
        let _ = prevRead = norm
        from written in s
        let __ = prevWritten = written
        select written;

    const int maxlen = 80;  
    return string.Concat(seq.Take(maxlen)).TrimEnd('-');
}

public static string RemapInternationalCharToAscii(string text)
{
    var seq = text.Normalize(NormalizationForm.FormD)
        .Where(c => CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark);

    return string.Concat(seq).Normalize(NormalizationForm.FormC);
}

Mi cadena de prueba:

" I love C#, F#, C++, and... Crème brûlée!!! They see me codin'... they hatin'... tryin' to catch me codin' dirty... "

Ronnie Overby
fuente
2

La solución stackoverflow es excelente, pero el navegador moderno (excluyendo IE, como siempre) ahora maneja muy bien la codificación utf8:

ingrese la descripción de la imagen aquí

Así que actualicé la solución propuesta:

public static string ToFriendlyUrl(string title, bool useUTF8Encoding = false)
{
    ...

        else if (c >= 128)
        {
            int prevlen = sb.Length;
            if (useUTF8Encoding )
            {
                sb.Append(HttpUtility.UrlEncode(c.ToString(CultureInfo.InvariantCulture),Encoding.UTF8));
            }
            else
            {
                sb.Append(RemapInternationalCharToAscii(c));
            }
    ...
}

Código completo en Pastebin

Editar: Aquí está el código para el RemapInternationalCharToAsciimétodo (que falta en el pastebin).

giammin
fuente
Según Wikipedia , Mozilla 1.4, Netscape 7.1, Opera 7.11 estuvieron entre las primeras aplicaciones que admitieron IDNA. Un complemento del navegador está disponible para Internet Explorer 6 para proporcionar compatibilidad con IDN. Las API de URL de Internet Explorer 7.0 y Windows Vista proporcionan soporte nativo para IDN. Parece que eliminar caracteres UTF-8 es una pérdida de tiempo. ¡Viva UTF-8!
Muhammad Rehan Saeed
1

Me gustó la forma en que esto se hace sin usar expresiones regulares , así que lo porté a PHP. Acabo de agregar una función llamada is_betweenpara verificar los caracteres:

function is_between($val, $min, $max)
{
    $val = (int) $val; $min = (int) $min; $max = (int) $max;

    return ($val >= $min && $val <= $max);
}

function international_char_to_ascii($char)
{
    if (mb_strpos('àåáâäãåa', $char) !== false)
    {
        return 'a';
    }

    if (mb_strpos('èéêëe', $char) !== false)
    {
        return 'e';
    }

    if (mb_strpos('ìíîïi', $char) !== false)
    {
        return 'i';
    }

    if (mb_strpos('òóôõö', $char) !== false)
    {
        return 'o';
    }

    if (mb_strpos('ùúûüuu', $char) !== false)
    {
        return 'u';
    }

    if (mb_strpos('çccc', $char) !== false)
    {
        return 'c';
    }

    if (mb_strpos('zzž', $char) !== false)
    {
        return 'z';
    }

    if (mb_strpos('ssšs', $char) !== false)
    {
        return 's';
    }

    if (mb_strpos('ñn', $char) !== false)
    {
        return 'n';
    }

    if (mb_strpos('ýÿ', $char) !== false)
    {
        return 'y';
    }

    if (mb_strpos('gg', $char) !== false)
    {
        return 'g';
    }

    if (mb_strpos('r', $char) !== false)
    {
        return 'r';
    }

    if (mb_strpos('l', $char) !== false)
    {
        return 'l';
    }

    if (mb_strpos('d', $char) !== false)
    {
        return 'd';
    }

    if (mb_strpos('ß', $char) !== false)
    {
        return 'ss';
    }

    if (mb_strpos('Þ', $char) !== false)
    {
        return 'th';
    }

    if (mb_strpos('h', $char) !== false)
    {
        return 'h';
    }

    if (mb_strpos('j', $char) !== false)
    {
        return 'j';
    }
    return '';
}

function url_friendly_title($url_title)
{
    if (empty($url_title))
    {
        return '';
    }

    $url_title = mb_strtolower($url_title);

    $url_title_max_length   = 80;
    $url_title_length       = mb_strlen($url_title);
    $url_title_friendly     = '';
    $url_title_dash_added   = false;
    $url_title_char = '';

    for ($i = 0; $i < $url_title_length; $i++)
    {
        $url_title_char     = mb_substr($url_title, $i, 1);

        if (strlen($url_title_char) == 2)
        {
            $url_title_ascii    = ord($url_title_char[0]) * 256 + ord($url_title_char[1]) . "\r\n";
        }
        else
        {
            $url_title_ascii    = ord($url_title_char);
        }

        if (is_between($url_title_ascii, 97, 122) || is_between($url_title_ascii, 48, 57))
        {
            $url_title_friendly .= $url_title_char;

            $url_title_dash_added = false;
        }
        elseif(is_between($url_title_ascii, 65, 90))
        {
            $url_title_friendly .= chr(($url_title_ascii | 32));

            $url_title_dash_added = false;
        }
        elseif($url_title_ascii == 32 || $url_title_ascii == 44 || $url_title_ascii == 46 || $url_title_ascii == 47 || $url_title_ascii == 92 || $url_title_ascii == 45 || $url_title_ascii == 47 || $url_title_ascii == 95 || $url_title_ascii == 61)
        {
            if (!$url_title_dash_added && mb_strlen($url_title_friendly) > 0)
            {
                $url_title_friendly .= chr(45);

                $url_title_dash_added = true;
            }
        }
        else if ($url_title_ascii >= 128)
        {
            $url_title_previous_length = mb_strlen($url_title_friendly);

            $url_title_friendly .= international_char_to_ascii($url_title_char);

            if ($url_title_previous_length != mb_strlen($url_title_friendly))
            {
                $url_title_dash_added = false;
            }
        }

        if ($i == $url_title_max_length)
        {
            break;
        }
    }

    if ($url_title_dash_added)
    {
        return mb_substr($url_title_friendly, 0, -1);
    }
    else
    {
        return $url_title_friendly;
    }
}
Peter Mortensen
fuente
1

Ahora todos los navegadores manejan muy bien la codificación utf8, por lo que puede usar el método WebUtility.UrlEncode , es como HttpUtility.UrlEncode usado por @giamin pero funciona fuera de una aplicación web.

ikourfaln
fuente
1

Porté el código a TypeScript. Se puede adaptar fácilmente a JavaScript.

Estoy agregando un .containsmétodo al Stringprototipo, si está apuntando a los últimos navegadores o ES6, puede usar .includesen su lugar.

if (!String.prototype.contains) {
    String.prototype.contains = function (check) {
        return this.indexOf(check, 0) !== -1;
    };
}

declare interface String {
    contains(check: string): boolean;
}

export function MakeUrlFriendly(title: string) {
            if (title == null || title == '')
                return '';

            const maxlen = 80;
            let len = title.length;
            let prevdash = false;
            let result = '';
            let c: string;
            let cc: number;
            let remapInternationalCharToAscii = function (c: string) {
                let s = c.toLowerCase();
                if ("àåáâäãåą".contains(s)) {
                    return "a";
                }
                else if ("èéêëę".contains(s)) {
                    return "e";
                }
                else if ("ìíîïı".contains(s)) {
                    return "i";
                }
                else if ("òóôõöøőð".contains(s)) {
                    return "o";
                }
                else if ("ùúûüŭů".contains(s)) {
                    return "u";
                }
                else if ("çćčĉ".contains(s)) {
                    return "c";
                }
                else if ("żźž".contains(s)) {
                    return "z";
                }
                else if ("śşšŝ".contains(s)) {
                    return "s";
                }
                else if ("ñń".contains(s)) {
                    return "n";
                }
                else if ("ýÿ".contains(s)) {
                    return "y";
                }
                else if ("ğĝ".contains(s)) {
                    return "g";
                }
                else if (c == 'ř') {
                    return "r";
                }
                else if (c == 'ł') {
                    return "l";
                }
                else if (c == 'đ') {
                    return "d";
                }
                else if (c == 'ß') {
                    return "ss";
                }
                else if (c == 'Þ') {
                    return "th";
                }
                else if (c == 'ĥ') {
                    return "h";
                }
                else if (c == 'ĵ') {
                    return "j";
                }
                else {
                    return "";
                }
            };

            for (let i = 0; i < len; i++) {
                c = title[i];
                cc = c.charCodeAt(0);

                if ((cc >= 97 /* a */ && cc <= 122 /* z */) || (cc >= 48 /* 0 */ && cc <= 57 /* 9 */)) {
                    result += c;
                    prevdash = false;
                }
                else if ((cc >= 65 && cc <= 90 /* A - Z */)) {
                    result += c.toLowerCase();
                    prevdash = false;
                }
                else if (c == ' ' || c == ',' || c == '.' || c == '/' || c == '\\' || c == '-' || c == '_' || c == '=') {
                    if (!prevdash && result.length > 0) {
                        result += '-';
                        prevdash = true;
                    }
                }
                else if (cc >= 128) {
                    let prevlen = result.length;
                    result += remapInternationalCharToAscii(c);
                    if (prevlen != result.length) prevdash = false;
                }
                if (i == maxlen) break;
            }

            if (prevdash)
                return result.substring(0, result.length - 1);
            else
                return result;
        }
Sam
fuente
0

No no no. Todos ustedes están muy equivocados. Excepto por las cosas de diacríticos-fu, estás llegando allí, pero ¿qué pasa con los personajes asiáticos (lástima de los desarrolladores de Ruby por no considerar a sus hermanos nihonjin ).

Firefox y Safari muestran caracteres que no son ASCII en la URL y, francamente, se ven geniales. Es bueno admitir enlaces como ' http://somewhere.com/news/read/ お 前 た ち は ア ホ じ ゃ な い か い '.

Así que aquí hay un código PHP que lo hará, pero lo acabo de escribir y no lo he probado en estrés.

<?php
    function slug($str)
    {
        $args = func_get_args();
        array_filter($args);  //remove blanks
        $slug = mb_strtolower(implode('-', $args));

        $real_slug = '';
        $hyphen = '';
        foreach(SU::mb_str_split($slug) as $c)
        {
            if (strlen($c) > 1 && mb_strlen($c)===1)
            {
                $real_slug .= $hyphen . $c;
                $hyphen = '';
            }
            else
            {
                switch($c)
                {
                    case '&':
                        $hyphen = $real_slug ? '-and-' : '';
                        break;
                    case 'a':
                    case 'b':
                    case 'c':
                    case 'd':
                    case 'e':
                    case 'f':
                    case 'g':
                    case 'h':
                    case 'i':
                    case 'j':
                    case 'k':
                    case 'l':
                    case 'm':
                    case 'n':
                    case 'o':
                    case 'p':
                    case 'q':
                    case 'r':
                    case 's':
                    case 't':
                    case 'u':
                    case 'v':
                    case 'w':
                    case 'x':
                    case 'y':
                    case 'z':

                    case 'A':
                    case 'B':
                    case 'C':
                    case 'D':
                    case 'E':
                    case 'F':
                    case 'G':
                    case 'H':
                    case 'I':
                    case 'J':
                    case 'K':
                    case 'L':
                    case 'M':
                    case 'N':
                    case 'O':
                    case 'P':
                    case 'Q':
                    case 'R':
                    case 'S':
                    case 'T':
                    case 'U':
                    case 'V':
                    case 'W':
                    case 'X':
                    case 'Y':
                    case 'Z':

                    case '0':
                    case '1':
                    case '2':
                    case '3':
                    case '4':
                    case '5':
                    case '6':
                    case '7':
                    case '8':
                    case '9':
                        $real_slug .= $hyphen . $c;
                        $hyphen = '';
                        break;

                    default:
                       $hyphen = $hyphen ? $hyphen : ($real_slug ? '-' : '');
                }
            }
        }
        return $real_slug;
    }

Ejemplo:

$str = "~!@#$%^&*()_+-=[]\{}|;':\",./<>?\n\r\t\x07\x00\x04 コリン ~!@#$%^&*()_+-=[]\{}|;':\",./<>?\n\r\t\x07\x00\x04 トーマス ~!@#$%^&*()_+-=[]\{}|;':\",./<>?\n\r\t\x07\x00\x04 アーノルド ~!@#$%^&*()_+-=[]\{}|;':\",./<>?\n\r\t\x07\x00\x04";
echo slug($str);

Salidas: コ リ ン -and- ト ー マ ス -and- ア ー ノ ル ド

El '-y-' se debe a que & 'se cambia a' -y- '.

Peter Mortensen
fuente
44
Realmente no sé qué decir sobre esta información.
sjas
3
Ese es un muy buen ejemplo de cuándo NO usar una declaración de cambio de caso.
NickG