22

Definition

From Wikipedia:

A slug is the part of a URL which identifies a page using human-readable keywords.

To make the URL easier for users to type, special characters are often removed or replaced as well. For instance, accented characters are usually replaced by letters from the English alphabet; punctuation marks are generally removed; and spaces (which have to be encoded as %20 or +) are replaced by dashes (-) or underscores (_), which are more aesthetically pleasing.

Context

I developed a photo-sharing website on which users can upload, share and view photos.

All pages are generated automatically without my grip on the title. Because the title of a photo or the name of a user may contain accented characters or spaces, I needed a function to automatically create slugs and keep readable URLs.

I created the following function which replaces accented characters (âèêëçî), removes punctuation and bad characters (#@&~^!) and transforms spaces in dashes.

Questions

  • What do you think about this function?
  • Do you know any other functions to create slugs?

Code

:

function sluggable($str) {

    $before = array(
        'àáâãäåòóôõöøèéêëðçìíîïùúûüñšž',
        '/[^a-z0-9\s]/',
        array('/\s/', '/--+/', '/---+/')
    );
 
    $after = array(
        'aaaaaaooooooeeeeeciiiiuuuunsz',
        '',
        '-'
    );

    $str = strtolower($str);
    $str = strtr($str, $before[0], $after[0]);
    $str = preg_replace($before[1], $after[1], $str);
    $str = trim($str);
    $str = preg_replace($before[2], $after[2], $str);
 
    return $str;
}
Community
  • 1
  • 1
GG.
  • 21,083
  • 14
  • 84
  • 130

8 Answers8

36

I like the php-slugs code at google code solution. But if you want a simpler one that works with UTF-8:

function format_uri( $string, $separator = '-' )
{
    $accents_regex = '~&([a-z]{1,2})(?:acute|cedil|circ|grave|lig|orn|ring|slash|th|tilde|uml);~i';
    $special_cases = array( '&' => 'and', "'" => '');
    $string = mb_strtolower( trim( $string ), 'UTF-8' );
    $string = str_replace( array_keys($special_cases), array_values( $special_cases), $string );
    $string = preg_replace( $accents_regex, '$1', htmlentities( $string, ENT_QUOTES, 'UTF-8' ) );
    $string = preg_replace("/[^a-z0-9]/u", "$separator", $string);
    $string = preg_replace("/[$separator]+/u", "$separator", $string);
    return $string;
}

So

echo format_uri("#@&~^!âèêëçî");

outputs

-and-aeeeci
desertnaut
  • 57,590
  • 26
  • 140
  • 166
Natxet
  • 1,334
  • 3
  • 12
  • 18
  • `Here's` is converted to `here-039-s`. A better alternative is to simply remove the apostrophe. – rybo111 Jul 03 '15 at 13:23
8

A few people have linked to "php-slugs" on google.com, but it looks like their page is a little screwy now, so here it is if anyone needs it:

// source: https://code.google.com/archive/p/php-slugs/

function my_str_split($string)
{
    $slen=strlen($string);
    for($i=0; $i<$slen; $i++)
    {
        $sArray[$i]=$string{$i};
    }
    return $sArray;
}

function noDiacritics($string)
{
    //cyrylic transcription
    $cyrylicFrom = array('А', 'Б', 'В', 'Г', 'Д', 'Е', 'Ё', 'Ж', 'З', 'И', 'Й', 'К', 'Л', 'М', 'Н', 'О', 'П', 'Р', 'С', 'Т', 'У', 'Ф', 'Х', 'Ц', 'Ч', 'Ш', 'Щ', 'Ъ', 'Ы', 'Ь', 'Э', 'Ю', 'Я', 'а', 'б', 'в', 'г', 'д', 'е', 'ё', 'ж', 'з', 'и', 'й', 'к', 'л', 'м', 'н', 'о', 'п', 'р', 'с', 'т', 'у', 'ф', 'х', 'ц', 'ч', 'ш', 'щ', 'ъ', 'ы', 'ь', 'э', 'ю', 'я');
    $cyrylicTo   = array('A', 'B', 'W', 'G', 'D', 'Ie', 'Io', 'Z', 'Z', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'R', 'S', 'T', 'U', 'F', 'Ch', 'C', 'Tch', 'Sh', 'Shtch', '', 'Y', '', 'E', 'Iu', 'Ia', 'a', 'b', 'w', 'g', 'd', 'ie', 'io', 'z', 'z', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'r', 's', 't', 'u', 'f', 'ch', 'c', 'tch', 'sh', 'shtch', '', 'y', '', 'e', 'iu', 'ia'); 


    $from = array("Á", "À", "Â", "Ä", "Ă", "Ā", "Ã", "Å", "Ą", "Æ", "Ć", "Ċ", "Ĉ", "Č", "Ç", "Ď", "Đ", "Ð", "É", "È", "Ė", "Ê", "Ë", "Ě", "Ē", "Ę", "Ə", "Ġ", "Ĝ", "Ğ", "Ģ", "á", "à", "â", "ä", "ă", "ā", "ã", "å", "ą", "æ", "ć", "ċ", "ĉ", "č", "ç", "ď", "đ", "ð", "é", "è", "ė", "ê", "ë", "ě", "ē", "ę", "ə", "ġ", "ĝ", "ğ", "ģ", "Ĥ", "Ħ", "I", "Í", "Ì", "İ", "Î", "Ï", "Ī", "Į", "IJ", "Ĵ", "Ķ", "Ļ", "Ł", "Ń", "Ň", "Ñ", "Ņ", "Ó", "Ò", "Ô", "Ö", "Õ", "Ő", "Ø", "Ơ", "Œ", "ĥ", "ħ", "ı", "í", "ì", "i", "î", "ï", "ī", "į", "ij", "ĵ", "ķ", "ļ", "ł", "ń", "ň", "ñ", "ņ", "ó", "ò", "ô", "ö", "õ", "ő", "ø", "ơ", "œ", "Ŕ", "Ř", "Ś", "Ŝ", "Š", "Ş", "Ť", "Ţ", "Þ", "Ú", "Ù", "Û", "Ü", "Ŭ", "Ū", "Ů", "Ų", "Ű", "Ư", "Ŵ", "Ý", "Ŷ", "Ÿ", "Ź", "Ż", "Ž", "ŕ", "ř", "ś", "ŝ", "š", "ş", "ß", "ť", "ţ", "þ", "ú", "ù", "û", "ü", "ŭ", "ū", "ů", "ų", "ű", "ư", "ŵ", "ý", "ŷ", "ÿ", "ź", "ż", "ž");
    $to   = array("A", "A", "A", "AE", "A", "A", "A", "A", "A", "AE", "C", "C", "C", "C", "C", "D", "D", "D", "E", "E", "E", "E", "E", "E", "E", "E", "G", "G", "G", "G", "G", "a", "a", "a", "ae", "ae", "a", "a", "a", "a", "ae", "c", "c", "c", "c", "c", "d", "d", "d", "e", "e", "e", "e", "e", "e", "e", "e", "g", "g", "g", "g", "g", "H", "H", "I", "I", "I", "I", "I", "I", "I", "I", "IJ", "J", "K", "L", "L", "N", "N", "N", "N", "O", "O", "O", "OE", "O", "O", "O", "O", "CE", "h", "h", "i", "i", "i", "i", "i", "i", "i", "i", "ij", "j", "k", "l", "l", "n", "n", "n", "n", "o", "o", "o", "oe", "o", "o", "o", "o", "o", "R", "R", "S", "S", "S", "S", "T", "T", "T", "U", "U", "U", "UE", "U", "U", "U", "U", "U", "U", "W", "Y", "Y", "Y", "Z", "Z", "Z", "r", "r", "s", "s", "s", "s", "ss", "t", "t", "b", "u", "u", "u", "ue", "u", "u", "u", "u", "u", "u", "w", "y", "y", "y", "z", "z", "z");


    $from = array_merge($from, $cyrylicFrom);
    $to   = array_merge($to, $cyrylicTo);

    $newstring=str_replace($from, $to, $string);
    return $newstring;
}

function makeSlugs($string, $maxlen=0)
{
    $newStringTab=array();
    $string=strtolower(noDiacritics($string));
    if(function_exists('str_split'))
    {
        $stringTab=str_split($string);
    }
    else
    {
        $stringTab=my_str_split($string);
    }

    $numbers=array("0","1","2","3","4","5","6","7","8","9","-");
    //$numbers=array("0","1","2","3","4","5","6","7","8","9");

    foreach($stringTab as $letter)
    {
        if(in_array($letter, range("a", "z")) || in_array($letter, $numbers))
        {
            $newStringTab[]=$letter;
        }
        elseif($letter==" ")
        {
            $newStringTab[]="-";
        }
    }

    if(count($newStringTab))
    {
        $newString=implode($newStringTab);
        if($maxlen>0)
        {
            $newString=substr($newString, 0, $maxlen);
        }

        $newString = removeDuplicates('--', '-', $newString);
    }
    else
    {
        $newString='';
    }

    return $newString;
}


function checkSlug($sSlug)
{
    if(preg_match("/^[a-zA-Z0-9]+[a-zA-Z0-9\-]*$/", $sSlug) == 1)
    {
        return true;
    }

    return false;
}

function removeDuplicates($sSearch, $sReplace, $sSubject)
{
    $i=0;
    do{

        $sSubject=str_replace($sSearch, $sReplace, $sSubject);
        $pos=strpos($sSubject, $sSearch);

        $i++;
        if($i>100)
        {
            die('removeDuplicates() loop error');
        }

    }while($pos!==false);

    return $sSubject;
}
SirDerpington
  • 11,260
  • 4
  • 49
  • 55
rybo111
  • 12,240
  • 4
  • 61
  • 70
  • 1
    Rather than a huge horrible and incomplete list of replacements, you'd be better off normalizing the string and then removing non-ascii characters – BlueRaja - Danny Pflughoeft Oct 30 '17 at 12:03
  • 1
    @BlueRaja-DannyPflughoeft Since this is Google's original code I'm not going to edit it. I'd encourage you to add another answer with improvements to this code. – rybo111 Oct 30 '17 at 12:52
  • I edited the matches for german umlauts. I think Ä should be AE, Ü UE and so on. – SirDerpington Oct 11 '18 at 10:23
  • @SirDerpington I am wondering if this answer should be editable, since it is effectively a copy-paste of https://code.google.com/archive/p/php-slugs/ – rybo111 Oct 12 '18 at 09:40
  • @rybo111 Yeah I know what you mean. I think it should be since - I don't know why- some characters in the $to and $from arrays were missing. It just said "?" instead of the actual char. – SirDerpington Oct 12 '18 at 09:47
7
    setlocale(LC_ALL, 'en_US.UTF8');

        function slugify($text)
        {
          // replace non letter or digits by -
          $text = preg_replace('~[^\\pL\d]+~u', '-', $text);

          // trim
          $text = trim($text, '-');

          // transliterate
          $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);

          // lowercase
          $text = strtolower($text);

          // remove unwanted characters
          $text = preg_replace('~[^-\w]+~', '', $text);

          if (empty($text))
          {
            return 'n-a';
          }

          return $text;
        }


$slug = slugify($var);
3

This really works fine. Returns correct clean url slug.

$string = '(1234) S*m@#ith S)&+*t `E}{xam)ple?>land   - - 1!_2)#3)(*4""5';

// remove all non alphanumeric characters except spaces
$clean =  preg_replace('/[^a-zA-Z0-9\s]/', '', strtolower($string)); 

// replace one or multiple spaces into single dash (-)
$clean =  preg_replace('!\s+!', '-', $clean); 

echo $clean; // 1234-smith-st-exampleland-12345
dotnetom
  • 24,551
  • 9
  • 51
  • 54
  • This code will cause the elimination of all the characters that are not in the regex, it's like a white list solution. But be careful because most of the international programmers will need a solution that transforms "café" into "cafe" and not into "caf" like this code does. – Natxet Jul 03 '15 at 21:18
3

I found this on the net, does exactly as you want, but keeps the case.

function sluggable($p) {
    $ts = array("/[À-Å]/","/Æ/","/Ç/","/[È-Ë]/","/[Ì-Ï]/","/Ð/","/Ñ/","/[Ò-ÖØ]/","/×/","/[Ù-Ü]/","/[Ý-ß]/","/[à-å]/","/æ/","/ç/","/[è-ë]/","/[ì-ï]/","/ð/","/ñ/","/[ò-öø]/","/÷/","/[ù-ü]/","/[ý-ÿ]/");
    $tn = array("A","AE","C","E","I","D","N","O","X","U","Y","a","ae","c","e","i","d","n","o","x","u","y");
    return preg_replace($ts,$tn, $p);
}

source

  • This isn't very robust as it's only able to handle the characters listed. What about Cyrillic? Hebrew? Other obscure non-ASCII symbols like `²`, `º`, `‘`, etc.? – Will Vousden Mar 14 '11 at 23:42
  • But preg_replace() is slower than strtr(). – GG. Mar 14 '11 at 23:56
1

This is the class we use and while it can perform individual operations, it also has the ability to turn strings (or paths) into slug versions (only a-z, 0-9, and - are in the final output). It also does a couple extra things such as convert ampersands (&) to the word and.

Usage:

echo (new Str('My Cover Letter & Résumé'))->slugify()->__toString();

my-cover-letter-and-resume

Str class:

<?php

use RuntimeException;
use Transliterator;

class Str
{
    /**
     * Will hold an instance of Transliterator
     * for removing accents from characters.
     * Same instance for all instances of this class is fine.
     */
    private static $accent_transliterator;
    private $string;

    public function __construct(string $string)
    {
        $this->string = $string;
    }

    public function __toString()
    {
        return $this->string;
    }

    public function cleanForUrlPath(): self
    {
        $path = '';

        // Loop through path sections (separated by `/`)
        // and slugify each section.
        foreach (explode('/', $this->string) as $section) {
            $section = (new static($section))->slugify()->__toString();
            if ($section !== '') {
                $path .= "/$section";
            }
        }

        // Save the cleaned path
        $this->string = "$path/";

        return $this;
    }

    public function cleanUpSlugDashes(): self
    {
        // Remove extra dashes
        $this->string = preg_replace('/--+/', '-', $this->string);

        // Remove leading and trailing dashes
        $this->string = trim($this->string, '-');

        return $this;
    }

    /**
     * Replace symbols with word replacements.
     * Eg, `&` becomes ` and `.
     */
    public function convertSymbolsToWords(): self
    {
        $this->string = strtr($this->string, [
            '@' => ' at ',
            '%' => ' percent ',
            '&' => ' and ',
        ]);

        return $this;
    }

    public static function getSpacerCharacters(
        array $with = [],
        array $without = []
    ): array {
        return array_unique(array_diff(array_merge([
            ' ', // space
            '…', // ellipsis
            '–', // en dash
            '—', // em dash
            '/', // slash
            '\\', // backslash
            ':', // colon
            ';', // semi-colon
            '.', // period
            '+', // plus sign
            '#', // pound sign
            '~', // tilde
            '_', // underscore
            '|', // pipe
        ], array_values($with)), array_values($without)));
    }

    public function lower(): self
    {
        $this->string = strtolower($this->string);

        return $this;
    }

    /**
     * Replaces all accented characters
     * with similar ASCII characters.
     */
    public function removeAccents(): self
    {
        // If no accented characters are found,
        // return the given string as-is.
        if (!preg_match('/[\x80-\xff]/', $this->string)) {
            return $this;
        }

        // Instantiate Transliterator if we haven't already
        if (!isset(self::$accent_transliterator)) {
            self::$accent_transliterator = Transliterator::create(
                'Any-Latin; Latin-ASCII;'
            );

            if (self::$accent_transliterator === null) {
                // @codeCoverageIgnoreStart
                throw new RuntimeException(
                    'Could not create a transliterator'
                );
                // @codeCoverageIgnoreEnd
            }
        }

        // Save transliterated string
        $this->string = (self::$accent_transliterator)->transliterate(
            $this->string
        );

        return $this;
    }

    public function replace($search, $replace)
    {
        $this->string = str_replace($search, $replace, $this->string);

        return $this;
    }

    public function replaceRegex($pattern, $replacement): self
    {
        $this->string = preg_replace($pattern, $replacement, $this->string);

        return $this;
    }

    /**
     * @param int $length number of bytes to shorten the string to
     */
    public function shorten(int $length): self
    {
        // If the string is already `$length` or shorter,
        // return it as-is.
        if (strlen($this->string) <= $length) {
            return $this;
        }

        // Shorten by 2 additional characters
        // to account for the three periods that are appended.
        // Only need to shorten by 2
        // as there's always at least one character (space) removed
        // when the last word is popped off of the array.
        $length -= 2;

        // Shorten the string to `$length` and split into words
        $words = explode(' ', substr($this->string, 0, $length));

        // Discard the last word as it's a partial word,
        // or empty if the last character happened to be a space.
        // If there's only one word,
        // then it was longer than `$length`
        // and the truncated version should be returned.
        if (count($words) > 1) {
            array_pop($words);
        }

        // Save the shortened string with "..." appended
        $this->string = rtrim(implode(' ', $words), ':').'...';

        return $this;
    }

    public function slugify(): self
    {
        // If the string is already a slug
        if (preg_match('/^[a-z0-9\\-]+$/', $this->string)) {
            return $this;
        }

        // - Normalize accents
        // - Normalize symbols
        // - Lowercase
        // - Replace space characters with dashes
        // - Remove non-slug characters
        // - Clean up leading, trailing, and consecutive dashes
        return $this
            ->removeAccents()
            ->convertSymbolsToWords()
            ->lower()
            ->spacersToDashes()
            ->replaceRegex('/([^a-z0-9\\-]+)/', '')
            ->cleanUpSlugDashes();
    }

    public function spacersToDashes(): self
    {
        return $this->replace(static::getSpacerCharacters(), '-');
    }
}
0b10011
  • 18,397
  • 4
  • 65
  • 86
  • @NorbertBoros It's been a little over 7 years since I posted this, and while most of it has remained the same (some cleanup and putting it into a self-contained class), the one big change is `remove_accents()` has been completely rewritten to take advantage of [PHP's `Transliterator` class](https://www.php.net/manual/en/class.transliterator.php). The first `if` statement is kept, and then the rest of the function can be replaced by `$transliterator = Transliterator::create('Any-Latin; Latin-ASCII;'); return $transliterator->transliterate($string);`. I'll try to get the answer updated as well. – 0b10011 Oct 16 '19 at 15:37
  • I actually save `$transliterator` to the class to avoid having to reconstruct it every time. – 0b10011 Oct 16 '19 at 15:38
  • @NorbertBoros answer updated if you want the cleaned up version. At a quick glance, I think it works in PHP 7.0+. – 0b10011 Oct 16 '19 at 15:51
  • Thank you! I will test it shortly. – Mecanik Oct 16 '19 at 16:30
1
function seourl($phrase, $maxLength = 100000000000000) {
        $result = strtolower($phrase);

        $result = preg_replace("~[^A-Za-z0-9-\s]~", "", $result);
        $result = trim(preg_replace("~[\s-]+~", " ", $result));
        $result = trim(substr($result, 0, $maxLength));
        $result = preg_replace("~\s~", "-", $result);

        return $result;
    }
XpertSpot
  • 21
  • 6
1
function remove_accents($string)
{
    $a = 'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûýýþÿŔŕ';
    $b = 'aaaaaaaceeeeiiiidnoooooouuuuybsaaaaaaaceeeeiiiidnoooooouuuyybyRr';
    $string = strtr(utf8_decode($string), utf8_decode($a), $b);
    return utf8_encode($string);
}

function format_slug($title)
{
    $title = remove_accents($title);
    $title = trim(strtolower($title));
    $title = preg_replace('#[^a-z0-9\\-/]#i', '_', $title);
    return trim(preg_replace('/-+/', '-', $title), '-/');
}

use : echo format_slug($var);