The perfect PHP clean url generator

In my hunt for the perfect clean url (smart url, slug, permalink, whatever) generator I’ve always slipped in some exception or bug that made the function a piece of junk. But I recently found an easy solution I hope I could call “definitive”.

Clean url generators are crucial for search engine optimization or just to tidy up the site navigation. They are even more important if you work with international characters, accented vowels /à, è, ì, .../, cedilla /ç/, dieresis /ë/, tilde /ñ/ and so on.

First of all we need to strip all special characters and punctuation away. This is easily accomplished with something like:

function toAscii($str) {
	$clean = preg_replace("/[^a-zA-Z0-9\/_|+ -]/", '', $str);
	$clean = strtolower(trim($clean, '-'));
	$clean = preg_replace("/[\/_|+ -]+/", '-', $clean);

	return $clean;
}

With our toAscii function we can convert a string like “Hi! I’m the title of your page!” to hi-im-the-title-of-your-page. This is nice, but what happens with a title like “A piñata is a paper container filled with candy”?

The result will be a-piata-is-a-paper-container-filled-with-candy, which is not cool. We need to convert all special characters to the closest ascii character equivalent.

There are many ways to do this, maybe the easiest is by using iconv.

setlocale(LC_ALL, 'en_US.UTF8');
function toAscii($str) {
	$clean = iconv('UTF-8', 'ASCII//TRANSLIT', $str);
	$clean = preg_replace("/[^a-zA-Z0-9\/_| -]/", '', $clean);
	$clean = strtolower(trim($clean, '-'));
	$clean = preg_replace("/[\/_| -]+/", '-', $clean);

	return $clean;
}

I always work with UTF-8 but you can obviously use any character encoding recognized by your system. The piñata text is now transliterated into a-pinata-is-a-paper-container-filled-with-candy. Lovable.

If they are not Spanish, users will hardly search your site for the word piñata, they will most likely search for pinata. So you may want to store both versions in your database. You may have a title field with the actual displayed text and a slug field containing its ascii version counterpart.

We can add a delimiter parameter to our function so we can use it to generate both clean urls and slugs (in newspaper editing, a slug is a short name given to an article that is in production, source).

setlocale(LC_ALL, 'en_US.UTF8');
function toAscii($str, $delimiter='-') {
	$clean = iconv('UTF-8', 'ASCII//TRANSLIT', $str);
	$clean = preg_replace("/[^a-zA-Z0-9\/_|+ -]/", '', $clean);
	$clean = strtolower(trim($clean, '-'));
	$clean = preg_replace("/[\/_|+ -]+/", $delimiter, $clean);

	return $clean;
}

// echo toAscii("A piñata is a paper container filled with candy.", ' ');
// returns: a pinata is a paper container filled with candy

There’s one more thing. The string “I’ll be back!” is converted to ill-be-back. This may or may not be an issue depending on your application. If you use the function to generate a searchable slug for example, looking for “ill” would return the famous Terminator quote that probably isn’t what you wanted.

setlocale(LC_ALL, 'en_US.UTF8');
function toAscii($str, $replace=array(), $delimiter='-') {
	if( !empty($replace) ) {
		$str = str_replace((array)$replace, ' ', $str);
	}

	$clean = iconv('UTF-8', 'ASCII//TRANSLIT', $str);
	$clean = preg_replace("/[^a-zA-Z0-9\/_|+ -]/", '', $clean);
	$clean = strtolower(trim($clean, '-'));
	$clean = preg_replace("/[\/_|+ -]+/", $delimiter, $clean);

	return $clean;
}

You can now pass custom delimiters to the function. Calling toAscii("I'll be back!", "'") you’ll get i-ll-be-back. Also note that the apostrophe is replaced before the string is converted to ascii as character encoding conversion may lead to weird results, for example é is converted to 'e, so the apostrophe needs to be parsed before the string is mangled by iconv.

The function seems now complete. Lets stress test it.

echo toAscii("Mess'd up --text-- just (to) stress /test/ ?our! `little` \\clean\\ url fun.ction!?-->");
returns: messd-up-text-just-to-stress-test-our-little-clean-url-function

echo toAscii("Perché l'erba è verde?", "'"); // Italian
returns: perche-l-erba-e-verde

echo toAscii("Peux-tu m'aider s'il te plaît?", "'"); // French
returns: peux-tu-m-aider-s-il-te-plait

echo toAscii("Tänk efter nu – förr'n vi föser dig bort"); // Swedish
returns: tank-efter-nu-forrn-vi-foser-dig-bort

echo toAscii("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖÙÚÛÜÝßàáâãäåæçèéêëìíîïðñòóôõöùúûüýÿ");
returns: aaaaaaaeceeeeiiiidnooooouuuuyssaaaaaaaeceeeeiiiidnooooouuuuyy

echo toAscii("Custom`delimiter*example", array('*', '`'));
returns: custom-delimiter-example

echo toAscii("My+Last_Crazy|delimiter/example", '', ' ');
returns: my last crazy delimiter example

I’m sure we are far from perfection and probably some php/regex guru will soon bury me under my ignorance suggesting an über-simple alternative to my function. What do you thing?

Reactions

  1. Cool. I’ve seen some articles about this topic, but they’re not complete as this. If the language is English, everything seems to be OK, but with other languages, it’s not simple. I’ve tried many methods to take n first characters of a string, but I was not successful. This post helps me a lot. Thank you very much.

  2. I’m liking it. A few minor tweaks and I think it’s something i’ll be implementing in a few of my applications!

    Thanks for the post - good work! :)

    Rgds,
    JB

  3. [...] The perfect PHP Clean URL Generator - The perfect slug generator for PHP developers! [...]

    • Permalink: #4
    • Date: 2009/04/28 @ 14:32
    • By: lelebart

    very very lovable!! thanks!

    • Permalink: #5
    • Date: 2009/04/30 @ 00:17
    • By: lelebart

    BUT, there were a BUT.. i’ve had a little problem:

    echo toAscii(”Perché l’erba è verde?”, “‘”); // Italian
    didn’t return: perche-l-erba-e-verde
    but it returned:
    Notice: iconv() [function.iconv]: Detected an illegal character in input string in {file} on line {line}
    perch

    so i’ve add a little line.. and it solved (or it seems to solve).
    so here you’re the new function:

    function toAscii($str, $replace=array(), $delimiter='-', $charset='ISO-8859-1') {
    $str = iconv($charset, 'UTF-8', $str); // by lelebart

    if( !empty($replace) ) {
    $str = str_replace((array)$replace, ' ', $str);
    }

    $clean = iconv('UTF-8', 'ASCII//TRANSLIT', $str);
    $clean = preg_replace("/[^a-zA-Z0-9\/_|+ -]/”, ”, $clean);
    $clean = strtolower(trim($clean, ‘-’));
    $clean = preg_replace(”/[\/_|+ -]+/”, $delimiter, $clean);

    return $clean;
    }

    hope to be helpul/useful

  4. @lelebart, that depends on the fact that you are probably working in latin-1 instead of utf-8. If you work in iso-8859-1 you can simply replace all references to UTF-8 into ISO-8859-1. You don’t need to convert latin-1 to uft-8 first.

    • Permalink: #7
    • Date: 2009/04/30 @ 16:47
    • By: lelebart

    @Matteo Spinelli: thanks. i didn’t know that. ^^
    so i need a simple line like that?
    $clean = iconv(’ISO-8859-1′, ‘ASCII//TRANSLIT’, $str);

  5. @lelebart, yep. Also remember to set the right setlocale.

    • Permalink: #9
    • Date: 2009/05/01 @ 15:35
    • By: Jazz

    I can’t thank you enough for this wonderful script.

    Before I found this I used to clean urls using str_replace for each kind of special character…this script has saved me tons of time.

    !! 5 STARS !!

    • Permalink: #10
    • Date: 2009/05/06 @ 00:56
    • By: Megamind

    Very nice indeed!!!

  6. Wow,

    This is the shortest solution I’ve ever seen. Nice, clean code, congrats and thanks.

    • Permalink: #12
    • Date: 2009/06/16 @ 11:09
    • By: Francesco

    Thank you very much, it’s just what I was looking for this morning.

    Just one thing: I don’t understand what you intended to do with:

    $str = str_replace((array)$replace, ‘ ‘, $str);

    and how do you expect to pass the $replace parameter. The default is array() but in the example below you pass “‘” that is a string and in the function i’ts converted to array(”‘”). But what if I pass “‘,;.”, it will be converted into array(”‘,;.”) by (array)$replace and it won’t work as expected in str_replace(). I should pass the $replace parameter directly as an array(”‘”,”,”,”;”,”.”) instead.

    Hope it helps if I got the point, otherwise I didn’t get that part of the code.

    If the above is valid, then if you want to specify the $delimiter using the default $replace empty array(), you may want to modify the function to check for $replace type with is_string or !is_array:

    if(is_string($replace)) { // gonna use default $replace
    $delimiter = $replace;
    $replace = array();
    }

    Bye.

  7. @francesco: yes the $replace parameter must be an array so that you can specify multiple character delimiters (have a look at the custom delimiters example). It shouldn’t be needed to check if the parameter is a string or an array. I convert it to array anyway and it should work.

    • Permalink: #14
    • Date: 2009/08/26 @ 17:56
    • By: andrew

    If you don’t want to worry about setlocale() causing you problems, I’d recommend merging this example with the example on this page - http://www.randomsequence.com/articles/removing-accented-utf-8-characters-with-php/

    • Permalink: #15
    • Date: 2009/09/12 @ 07:01
    • By: ege madra

    Possibly the best article on the subject. Thank you.

    • Permalink: #16
    • Date: 2009/09/19 @ 19:26
    • By: Chris

    It’s very nice… but how about website that are not in a US locale?

    setlocale(LC_ALL, ‘en_US.UTF8′);

    It will mess up the settings, is there anyway around this?

  8. @Chris,
    you can use any locale you have installed on your system, I’d suggest to stick with UTF8 though (it_IT.UTF8, fr_FR.UTF8, etc…). If that is not possible remember to change the iconv parameter as well.

    • Permalink: #18
    • Date: 2009/10/13 @ 21:52
    • By: Aled

    In your third and fourth examples, you trim the cleaned string from dashes. Is there a reason for this, or should it be trimmed by the delimiter?

    $clean = strtolower(trim($clean, ‘-’));

    to

    $clean = strtolower(trim($clean, $delimiter));

  9. Aled, thanks for pointing that out. Actually you’d need to trim for all the following characters: /_|+ -
    It’s just to be sure that the string doesn’t end with a delimiter.

    • Permalink: #20
    • Date: 2009/10/18 @ 22:05
    • By: b4r7

    The perfect PHP clean url generator…

    via: http://cubiq.org/the-perfect-php-clean-url-generator/12
    In my hunt for the perfect clean url (smart url, slug, permalink, whatever) generator I’ve always slipped in some exception or bug that made the function a piece of junk. But I recently foun…

    • Permalink: #21
    • Date: 2009/11/17 @ 08:07
    • By: Mike

    This is an odd request, but if I wanted to keep UTF-8 characters but continue to strip miscellaneous characters such as ASCII punctuation, how would I modify this function?

    I know it’s not SEO preferred but our niche apps need their native encoding (UTF-8) and not ASCII substitutions. ;-)

  10. @Mike: I’m afraid you have to create an array of chatacters you wish to substitute.

    • Permalink: #23
    • Date: 2009/11/17 @ 08:21
    • By: Mike

    Sorry for double comment, I tried the original function and it’s simply deleting all our Russian/Chinese/etc. encoded strings. If you could help me with a modification that keeps UTF-8 characters but cleans out punctuation I would be much obliged. =)

    • Permalink: #24
    • Date: 2009/11/17 @ 08:30
    • By: Mike

    @Matteo: Sorry I didn’t see your comment before my last post.

    I’m not sure I need an array, I’m not trying to keep any specific UTF-8 characters. I’m just trying to keep all UTF-8 characters but do the rest of your function (strip punctuation, replace whitespace with dashes, etc. slug preparing). I’m been scouring the net for a function that just strips punctuation/prepares slugs but leaves UTF-8 untouched.

    I found a Python version of what I want but not sure how to convert it to PHP:
    http://semicolons.org/archives/friendly-urls.html


Speak your mind



Creative Commons License cubiq.org by Matteo Spinelli is licensed under a Creative Commons License.