Generating permalinks
We’ve all been there - SEO can be quite a pain in the ass when you’re competing against others for exposure. Turns out, much of the tedious content optimization can be resolved in the development phase, using nice and descriptive URLs.
For example, if you’re reading this part of the post, you’ve most probably clicked some link that takes you to the view page for this article. If you’ll look at the URL, you’ll notice a very consistent structure between the view pages for the different posts, which is (in Symfony routing terms):
/:year/:month/:day/:post_slug
So far so good. It’s a classic routing rule, described in nice detail in the Symfony book. We define a module and action that receives the above mentioned GET parameters and define a Propel Criteria object:
// get the date for Propel
$year = $this->getRequestParameter("year");
$month = $this->getRequestParameter("month");
$day = $this->getRequestParameter("day");
$c = new Criteria();
// we're using a DATETIME field, so we need to check posts
// that are between 00:00:00 and 23:59:59 on the same date:
$c->add(
BlogPostPeer::CREATED_AT,
"{$year}-{$month}-{$day} 00:00:00",
Criteria::GREATER_EQUAL);
$c->addAnd(
BlogPostPeer::CREATED_AT,
"{$year}-{$month}-{$day} 23:59-59",
Criteria::LESS_EQUAL);
$c->addAnd(BlogPostPeer::POST_SLUG, $this->getRequestParameter("post_slug");
$this->post = BlogPostPeer::doSelectOne($c);
The action’s code is a bit messy, since we’ve assembled the query right in it (in a real setting, I’d go for refactoring the whole thing to a static function within BlogPostPeer that returns a Propel object given the 4 parameters specified. Oh well, but it suffices for the example given.
This solution, however, proves to have shortcomings - one, the criteria needed to fetch the object is pretty complex, since we need to search by date and post_slug. Two, if multiple posts were added on the same day with the same title, only one of them would come through. To improve on this, let’s explore the magical world of URL slug generation (which forms the permalink, if you’ve missed the connection between the two terms).
First, we need an algorithm to generate our slug. A method I use is to have as many humanly readable characters displayed in the URL without them being URL-encoded. To do this, we create a utility class that contains static functions, just to keep them quasi-namespaced:
class Strings {
public static function CreateSlug($title) {
$match = array(
"/[^a-zA-Z0-9\\-.,+' ]/",
"/ +/",
"/-+/",
"/^-/",
"/-$/"
);
$replace = array(
"",
"-",
"-",
"",
""
);
return preg_replace($match, $replace, strtolower($title));
}
};
As you’ve guessed, we’ve put this code in the project’s lib directory, under the name Strings.class.php, to make autoloading work for us. Now all we need to do to generate a slug is to call the Strings::CreateSlug() function. The example is purposefully verbose and inefficient, so I leave it up to the reader to apply the appropriate changes before actually using this code.
For example, the code above generates a slug for “A quick trip to the Bahamas” as follows: a-quick-trip-to-the-bahamas.
Let’s simplify our routing rule by just specifying:
/:post_slug
This simplifies the fetching of records by only searching using one field - the BlogPostPeer::POST_SLUG.
But alas! we are still at a crossroads - sooner or later someone will write a post with a preexisting title. In cases such as this, you could force the blogger to reconsider the title, but that’s just bad user experience. Instead, try checking for existing slugs first, then, if need be, append a unique identifier, such as an integer increment, or a date stamp. That way, you get the maximally readable URL without all the appended cruft that comes with a date-stamped URL or a similar scheme.
As always, this has only been a suggestion. Be creative with URLs, the more descriptive they are, the more SEO-friendly they are before you even start thinking about it!




January 22nd, 2007 at 1:48 pm
You forgot about the archive browsing ability with those date-time URLs. Stripping the $day out of the URL gives me the archive of year 2007 and month 01, stripping out the month gives me the archive of year 2007. That’s much more SEO-friendly in my eyes.
January 22nd, 2007 at 1:53 pm
Good point, I haven’t thought about that.
The case in point here was just to show the alternative to a behaviour found in other application and how that can be achieved with Symfony.
But I do agree with you on that point, but discussing the whole SEO theory and practice would take a lot more than just the one post.
January 24th, 2007 at 12:03 pm
Shiflett has a similar post on the subject. He has a good point on using month name (or abbreviation) instead of a number, keeps it more friendly.
February 22nd, 2008 at 12:38 pm
class Strings {
public static function CreateSlug($title) {
$match = array(
“/[^a-zA-Z0-9\-.,+’ ]/”,
“/ +/”,
“/-+/”,
“/^-/”,
“/-$/”
);
$replace = array(
“”,
“-”,
“-”,
“”,
“”
);
return preg_replace($match, $replace, strtolower($title));
}
); <<<<
^^^^^
to :
}
February 22nd, 2008 at 1:03 pm
Ah, true, thanks - I’ll update the code.
May 4th, 2008 at 12:47 pm
I badly understand English, but in general I have understood this clause
, also I wish to tell that in my head that that has exchanged. Now I shall think on another. Actually I shall try it, and I hope to me it will help. And I consider that in some moments you are really right. But not on 100 %, excuse
Thanks, prosperities to you!
P.S. Excuse for that that badly I write on English
November 12th, 2008 at 8:15 pm
More thank’s.