YASB – Yet Another Symfony Blog

January 20, 2007

Generating permalinks

Filed under: Database, General, PHP, Tips and tricks — Krof Drakula @ 12:22 am

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:

#lang php
// 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:

# lang php
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!

7 Comments

  1. 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.

    Comment by Pierre — January 22, 2007 @ 1:48 pm

  2. 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.

    Comment by Krof Drakula — January 22, 2007 @ 1:53 pm

  3. 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.

    Comment by fat — January 24, 2007 @ 12:03 pm

  4. class Strings { public static function CreateSlug($title) { $match = array( “/[^a-zA-Z0-9\-.,+’ ]/”, “/ +/”, “/-+/”, “/^-/”, “/-$/” ); $replace = array( “”, “-”, “-”, “”, “” ); return preg_replace($match, $replace, strtolower($title)); } ); <<<< ^^^^^

    to : }

    Comment by Alon — February 22, 2008 @ 12:38 pm

  5. Ah, true, thanks – I’ll update the code.

    Comment by Krof Drakula — February 22, 2008 @ 1:03 pm

  6. 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

    Comment by Buka — May 4, 2008 @ 12:47 pm

  7. More thank’s.

    Comment by Skiff — November 12, 2008 @ 8:15 pm

RSS feed for comments on this post.

Sorry, the comment form is closed at this time.

Powered by WordPress