Skip to content

Enforcing Drupal URL aliases

I hate modules, especially core modules. I prefer code to be tightly integrated. I want it to work together. Is that too much to ask? In Drupal, most functionality has been stuffed in modules. There’s a Locale module, a Content Translation module and a Path module. What’s missing is a Working Together module.

For me, clean, meaningful URLs are a number one, two and three requirement for any website that I do. Drupal considers /node/54673 to be a cool URL. I don’t. So, as a kind of afterthought, Drupal comes with the Path module. This module allows you to set URL aliases per node.

The problem is that there’s no concept of a canonical URL. The URL alias works, but so does the node/3242 URL. Neither redirects to the other. In many cases this is not much of a problem (because regular visitors will not notice this) but for our current project it is.

We have a lot of blocks with URL dependent visibility settings. For example, for a section about investing we have a menu that is displayed on all URLs starting with /investing, such as /investing/projects and so on.

After editing a page, Drupal helpfully redirects the user to node/[nodenumber]. For us, this means that the menu is no longer displayed and even the theme will be wrong. (We use the Sections module to select a subtheme based on which section you’re in.)

Global Redirect doesn’t work

The Global Redirect module promises to solve this by allowing you to redirect node/[nodenumber] URLs to their alias if available. It kinda does, in some circumstances.

Our Drupal website sports two languages: English (EN) and Dutch (NL). English is the default language (not the fallback language; we don’t use a fallback) and doesn’t use a prefix. Dutch uses the nl prefix. Two example URLs:

URL alias Generic URL
http://www.example.com/investing/projects http://www.example.com/node/288
http://www.example.com/nl/beleggen/projecten http://www.example.com/nl/node/110

When /node/288 is requested, the client is correctly redirected to /investing/projects, but when /node/110 is requested, no redirect takes place. It will take place when prefixing /nl, but this is completely useless since Drupal’s built-in actions such as edit don’t redirect using this prefix, and these actions were what we needed this module for in the first place.

A very simple hack that does work

We ended up tearing our hair out trying to fix Global Redirect until we decided that we could just delete the module and replace it with a RewriteRule and a simple PHP script.

Modify: .htaccess

# Put this after RewriteBase and before Drupal's default rewrite rules
RewriteRule ^(../)?node/([0-9]+)$ fixurl.php?nid=$2 [L]

Add: fixurl.php

<?php
 
require_once './includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE);
 
$result = db_query("SELECT * FROM {url_alias} WHERE src = 'node/%d' LIMIT 1", $_GET['nid']);
if ( db_error() ) die("O agony!");
 
$url_alias_object = db_fetch_object($result);
$destination = $url_alias_object->dst;
 
$result = db_query("SELECT prefix FROM {languages} WHERE language = '%s'", $url_alias_object->language);
if ( db_error() ) die("O agony!");
 
$prefix = db_result($result);
 
if ( !empty($prefix) )
  $prefix .= '/';
 
header("Location: /$prefix$destination",TRUE,301);
 
?>

Shortcomings in our hack

The code assumes that every content page has an URL alias. For us, this is okay, because we need these pretty URLs to even have menus show up or to have the right page be displayed with the right theme.

Also, this code is specifically tailored to language code in the URL prefix. For subdomain based language selection, for example, you’d need to modify it.


    14 Comments ( Add comment / trackback )

    1. (permalink)
      Comment by halfgaar
      On June 10, 2009 at 14:39

      I put a link to this post at Global Redirect’s bug tracker:

      http://drupal.org/node/422742

    2. (permalink)
      Comment by halfgaar
      On June 18, 2009 at 12:41

      I added an optional ending slash to the rewrite rule check:

      RewriteRule ^(../)?node/([0-9]+)/?$ fixurl.php?nid=$2 [L]

      Otherwise node/18/ wouldn’t rewrite.

    3. (permalink)
      Comment by dan
      On July 25, 2009 at 06:04

      this is genius! thanks!

    4. (permalink)
      Comment by Pryke
      On August 7, 2009 at 09:36

      Nice, but could you post a “different domain to different languages” version of this script? (We not uses url prefixes – we use 2 domains to 2 languages)

    5. (permalink)
      Comment by halfgaar
      On August 7, 2009 at 23:45

      I’m not gonna write code that I can’t test, but isn’t it pretty easy? If it doesn’t work already, it requires only minor change.

    6. (permalink)
      Comment by Luis
      On October 8, 2009 at 20:18

      This code creates a problem, when we create new content and there isn’t a URL alias (
      by forgetfulness, for example. Or old page without alias), never sees the page, it redirects you to the homepage.

      Otherwise a great solution, thank you very much

    7. (permalink)
      Comment by Rowan Rodrik
      On October 9, 2009 at 22:08

      Hi Luis,

      Thanks for your comment. Indeed, our code isn’t very flexible in that it doesn’t work when your node doesn’t have an url_alias. If you’ve engineered a simple solution that doesn’t have this limitation, I’d be interested to see it.

    8. (permalink)
      Comment by Luis
      On October 14, 2009 at 18:08

      A simple solution would be to modify fixurl.php (but not quite elegant, I add the string “\ view” in a node page without Url alias)

      dst;
       
      $result = db_query("SELECT prefix FROM {languages} WHERE language = '%s'", $url_alias_object-&gt;language);
      if ( db_error() ) die("O agony!");
       
      $prefix = db_result($result);
       
      if ( !empty($prefix) )
        $prefix .= '/';
       
       
      if ( empty($destination) ) {
        $destination = 'node/'.$_GET['nid'];
        header("Location: /$prefix$destination/view"); 
        }
      else {
        header("Location: /$prefix$destination",TRUE,301);
        }
      ?>

    9. (permalink)
      Comment by Luis
      On October 14, 2009 at 18:45

      Code Update:

      replace the last line of the original fixurl.phpheader(”Location: /$prefix$destination”,TRUE,301); ” by the next code:

      if ( empty($destination) ) {
        $destination = 'node/'.$_GET['nid'];
        $result = db_query("SELECT language FROM {node} WHERE nid = '%d'", $_GET['nid']);
        $prefix = db_result($result);
        if ($prefix==en) $prefix = '';
        if ( !empty($prefix) )
          $prefix .= '/';
        header("Location: /$prefix$destination/view"); 
        }
      else {
        header("Location: /$prefix$destination",TRUE,301);
        }

    10. (permalink)
      Comment by Rowan Rodrik
      On October 15, 2009 at 13:45

      Hi Louis,

      Thanks for sharing your modifications with us; much appreciated! 🙂

    11. (permalink)
      Comment by Murray
      On February 11, 2010 at 02:29

      Thanks Rowan and Luis. Both your contributions have been very helpful. This code will help me fix the problems I currently have with the Google index. It has been noted elsewhere that the best thing to do is to fix where the bad URLs are coming from. In my case Solr is spitting out /node/x URLs for non default language articles. Just got to work out how to fix that now 🙂 Thanks again.

    12. (permalink)
      Comment by Tony Chung
      On February 25, 2010 at 00:45

      Very pretty. Now if only Drupal could automatically create the URL aliases for us, like WordPress. I sometimes wonder if I hack Drupal just for the power and the possibilities? There must be an easy way to use WordPress and Pods to do the same thing.

    13. (permalink)
      Comment by Tobias Sjösten
      On February 4, 2012 at 22:03

      I realize this is an old blog post but for completeness sake I felt like adding a pair of cents.

      This seems to be solved in Global Redirect by issue #201675 (http://drupal.org/node/201675). That is probably the best approach to avoid nasty side effects.

      You really should not base your block visibility on path aliases. Actually, you should not use block visibility at all. Rather use Context (http://drupal.org/project/context) or Panels (http://drupal.org/project/panels) to control where your blocks are placed. This can really help with your performance.

      @Tony Chung: Don’t blame the tool if you don’t know how to use it. 😉 There’s a module called Pathauto (http://drupal.org/project/pathauto) which does exactly what you want and unlike WordPress it’s not a performance hog if you decide to use custom path syntaxes.

    14. On February 27, 2015 at 00:04

      For Drupal 7 the modified version of this would be as below:

      define('DRUPAL_ROOT', getcwd());
       
      require_once './includes/bootstrap.inc';
      drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE);
       
      $result = db_query("SELECT alias FROM {url_alias} WHERE source = :pid LIMIT 1", array(':pid' =&gt; 'node/'.$_GET['nid']))-&gt;fetchAllKeyed(); 
       
      $first_value = reset($result); 
      $first_key = key($result); 
       
      header("Location: /$first_key",TRUE,301);

    Post a comment

    (required)
    (required)

    Your email is never published nor shared.

    (optional)
    Allowed HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>