Skip to main content.

Tuesday, July 26, 2005

Ever wanted to change http://site/item/1234 into e.g. http://site/nieuws/1234?

The hack presented in this article explains how to change the item, archive, archives, blog and member keywords that are used in fancy URL mode. Being able to change this could be useful for people running a non-english Nucleus website.

Besides renaming the stub files, four other files need to be changed: nucleus/libs/globalfunctions.php, fancyurls.config.php, .htaccess and index.php

Fancyurls.config.php

In fancyurls.config.php, we're going to add a set of new values to the $CONF array. These will define which keywords will be used when URLs are generated or parsed.

The updated file looks like this:

<?php
    // remember: this URL should _NOT_ end with a slash. 
    $CONF['Self'] = 'http://yoursite.com/path';
	
    // keywords to use in fancy URLs
    $CONF['ItemKey'] = 'nieuws';
    $CONF['ArchiveKey'] = 'archief';
    $CONF['ArchivesKey'] = 'archieven';
    $CONF['MemberKey'] = 'medewerker';
    $CONF['BlogKey'] = 'blog';
    $CONF['CategoryKey'] = 'categorie';
?>

Renaming the stub files

When using the fancy URLs in a default configuration, you'll have a bunch of stub files sitting in your root directory: archive, archives, blog, category, item and member. All of these need to be renamed.

In our example, we're renaming these to archief, archieven, blog, categorie, nieuws and medewerker.

.htaccess

Simply renaming the stub files is not enough, however. You'll also need to update the .htaccess file to reflect the name changes:

<FilesMatch "^nieuws$">
    ForceType application/x-httpd-php
</FilesMatch>
<FilesMatch "^archief$">
    ForceType application/x-httpd-php
</FilesMatch>
<FilesMatch "^medewerker$">
    ForceType application/x-httpd-php
</FilesMatch>
<FilesMatch "^archieven$">
    ForceType application/x-httpd-php
</FilesMatch>
<FilesMatch "^categorie$">
    ForceType application/x-httpd-php
</FilesMatch>
<FilesMatch "^blog$">
    ForceType application/x-httpd-php
</FilesMatch>

Globalfunctions.php

There's only one missing piece in the puzzle: parsing the incoming URLs and generating URLs to be inserted in the output. That's where globalfunctions.php comes in.

Parsing URLs

Around line 259, you'll find the part of code responsible for parsing incoming URLs. edit it as follows:

// decode path_info
if ($CONF['URLMode'] == 'pathinfo')
{
  // initialize keywords if this hasn't been done before
  if ($CONF['ItemKey'] == '')     $CONF['ItemKey'] = 'item';
  if ($CONF['ArchiveKey'] == '')  $CONF['ArchiveKey'] = 'archive';
  if ($CONF['ArchivesKey'] == '') $CONF['ArchivesKey'] = 'archives';
  if ($CONF['MemberKey'] == '')   $CONF['MemberKey'] = 'member';
  if ($CONF['BlogKey'] == '')     $CONF['BlogKey'] = 'blog';
  if ($CONF['CategoryKey'] == '') $CONF['CategoryKey'] = 'category';
        
  $data = explode("/",serverVar('PATH_INFO'));
  for ($i=0;$i<sizeof($data);$i++) {
    switch ($data[$i]) {
      case $CONF['ItemKey']:      // item/1 (blogid)
        $i++;
        if ($i<sizeof($data)) $itemid = intval($data[$i]);
        break;
      case $CONF['ArchivesKey']:    // archives/1 (blogid)
        $i++;
        if ($i<sizeof($data)) $archivelist = intval($data[$i]);
        break;
      case $CONF['ArchiveKey']:    // two possibilities: archive/yyyy-mm or archive/1/yyyy-mm (with blogid)
        if ((($i+1)<sizeof($data)) && (!strstr($data[$i+1],'-')) ){
          $blogid = intval($data[++$i]);
        }
        $i++;
        if ($i<sizeof($data)) $archive = $data[$i];
        break;
      case 'blogid':      // blogid/1
      case $CONF['BlogKey']:  // blog/1
        $i++;
        if ($i<sizeof($data)) $blogid = intval($data[$i]);
        break;
      case $CONF['CategoryKey']:  // category/1 (catid)
      case 'catid':
        $i++;
        if ($i<sizeof($data)) $catid = intval($data[$i]);
        break;
      case $CONF['MemberKey']:
        $i++;
        if ($i<sizeof($data)) $memberid = intval($data[$i]);
        break;
      default:
        // skip...
    }
  }
}

In this code, all hardcoded keyword occurrences (e.g. 'item') have been replaced by the matching keyword variable ($CONF['ItemKey'])

generating URLs

The code which generates URLs is a little further down the code, around line 920. Update this code in the same way as the URL-parsing code: by replacing keywords by the matching $CONF variable:

/**
  * Centralisation of the functions that generate links
  */
function createItemLink($itemid, $extra = '') {
  global $CONF;
  if ($CONF['URLMode'] == 'pathinfo')
    $link = $CONF['ItemURL'] . '/' . $CONF['ItemKey'] . '/' . $itemid;
  else
    $link = $CONF['ItemURL'] . '?itemid=' . $itemid;
  return addLinkParams($link, $extra);
}
function createMemberLink($memberid, $extra = '') {
  global $CONF;
  if ($CONF['URLMode'] == 'pathinfo')
    $link = $CONF['MemberURL'] . '/' . $CONF['MemberKey'] . '/' . $memberid;
  else
    $link = $CONF['MemberURL'] . '?memberid=' . $memberid;
  return addLinkParams($link, $extra);
}
function createCategoryLink($catid, $extra = '') {
  global $CONF;
  if ($CONF['URLMode'] == 'pathinfo')
    $link = $CONF['CategoryURL'] . '/' . $CONF['CategoryKey'] . '/' . $catid;
  else
    $link = $CONF['CategoryURL'] . '?catid=' . $catid;
  return addLinkParams($link, $extra);
}
function createArchiveListLink($blogid = '', $extra = '') {
  global $CONF;
  if (!$blogid)
    $blogid = $CONF['DefaultBlog'];
  if ($CONF['URLMode'] == 'pathinfo')
    $link = $CONF['ArchiveListURL'] . '/' . $CONF['ArchivesKey'] . '/' . $blogid;
  else
    $link = $CONF['ArchiveListURL'] . '?archivelist=' . $blogid;
  return addLinkParams($link, $extra);
}
function createArchiveLink($blogid, $archive, $extra = '') {
  global $CONF;
  if ($CONF['URLMode'] == 'pathinfo')
    $link = $CONF['ArchiveURL'] . '/' . $CONF['ArchiveKey'] . '/'.$blogid.'/' . $archive;
  else
    $link = $CONF['ArchiveURL'] . '?blogid='.$blogid.'&amp;archive=' . $archive;
  return addLinkParams($link, $extra);
}
function createBlogLink($url, $params) {
  return addLinkParams($url . '?', $params);
}
function createBlogidLink($blogid, $params = '') {
  global $CONF;
  if ($CONF['URLMode'] == 'pathinfo')
    $link = $CONF['BlogURL'] . '/' . $CONF['BlogKey'] . '/' . $blogid;
  else
    $link = $CONF['BlogURL'] . '?blogid=' . $blogid;
  return addLinkParams($link, $params);
}

Further extending

The URL generation/parsing system could be even further extended by adding plugin events (see Task 12: generate-url-event on the bug tracker). With such a system, super-fancy URLs like http://site/item/My_Item_Title could be supported without having to replace each <%itemlink%> by <%NP_FancierURL%>.

Comments

Great write-up. Thanks!

I must confess I would like to have the type of URL's you describe under 'Further extending', e.g. site/item/item-title, site/member/roel and site/blog/shortblogname. So I'm looking forward to the completion of task 12. :)

Posted by Roel at Wednesday, July 27, 2005 13:36:47

@Roel: I did some more work on task 12 and added GenerateURL/ParseURL events. All that's missing now is an URL to translate item-title to and from itemid...

Main problems I see with writing such a plugin:
- avoiding dozens of database queries to get item titles from ids
- solving the problems that arise when 2 items have the same title.

Posted by karma at Thursday, July 28, 2005 16:42:03

I suppose the FancierURL plugin does a query each time a blog item is accessed? Couldn't it be possible to cache that somehow?

Regarding the problem with two items with the same title: I'd say thats not a problem in cases where the date/category is also used in a part of url (for example site/category/item-title or site/year/month/item-title). Maybe there should be a check when an item is added to the db: "is there an item with the exact same title?" The answer would be either Yes or No, and the url-creator could somehow act with this information.

Btw, there is another issue: what happens when an item title is edited? I would think that the old fancier url should redirect to the new one. (i think the later versions of the FancierURL plugin did this).

Posted by Roel at Thursday, July 28, 2005 18:36:04

The FancierURL plugin receives the title/timestamp as a parameter when <%FancierURL%> is used as a template variable, so no query needed there. Other link situations besides this and nextlink/prevlink in skins are not covered by the plugin.

As for the new events, I made it so that the titles/timestamps are passed along with the event whenever possible.

I'm planning on writing an url-creating plugin a la FancierURL while I'm working on this. I'm thinking of handling things a little different than FancierURL however, by keeping an extra mapping table in the database:

hash(item_title_url_part) -> itemid

This table can contain multiple entries if the title has been changed. To parse an url, a simple lookup in the table will yield the item id.

Generating an URL is as simple as URLifying the item title.

To avoid duplicate titles, I'll probably end up working the date in there (/item/2005/07/title). This would solve most of the problems.

And an idea which has just popped up: If the mapping table above maps the same url to multiple itemids, the user could be presented the choice. (but then again, the question is how to present that choice :))

Posted by karma at Thursday, July 28, 2005 20:08:15

karma, may I suggest WP-style URLs?

Like this:

http://example.com/2005/08/... (single post)
http://example.com/2005/08/15 (daily archive)
http://example.com/2005/08 (monthly archive)

The .htaccess entries can easily be modified to handle this.

Posted by ketsugi at Thursday, August 18, 2005 20:43:03

Add Comment

This item is closed, it's not possible to add new comments to it or to vote on it