The problem

Every time you make changes to URLs of a website, you risk messing up your hard earned Search Engine Optimization (SEO) scores. To eliminate 404 errors, it’s a best practise to forward old URLs with a 301 status code (moved permanently).

The approach above works great if you’re disciplined. But when making large changes to the URL structure of a website, an automated solution is needed.

This simple solution I’ve built has been working for a few years now.

My solution

<?php

// Catch-all route - this gets checked before a 404 error is thrown 
// Place at the end of your routes file

$app->get('{url}', function($url) use ($app){
    return redirectIfLegacyExistsOrAbort($app, '/'.$url, 'This page does not exist');
})->assert('url', '.+')->value('url', '');

// legacyRoutes is a collection of all the old URLs that don't exist anymore
// Put his where you define your app globals

$app['legacyRoutes'] = array(
    // format: '/old_url' => 'new-route-name'
    '/contact' => 'contact-page',
    '/features' => 'features-page'
    // and so on..
);

// redirectIfLegacyExistsOrAbort is a simple check: 
// - if the URL is found in the collection of old URLs, make a 301 direct
// - if not, throw a 404 with a kind description
// Put this where you define your helper functions

function redirectIfLegacyExistsOrAbort(Silex\Application $app, $url, $reason){
    if(array_key_exists($url, $app['legacyRoutes'])){
        $match = $app['legacyRoutes'][$url];
        return $app->redirect($app['url_generator']->generate($match, array('_locale' => 'en')),301);
    }
    else{
        // Not a known URL, throw 404
        return $app->abort(404, $reason);
    }
}

The basic idea behind this code is pretty simple:

  • Maintain a collection of old URLs (I recommend to automatically check for 404 errors based on an old sitemap and/or Search Console).
  • Before the application throws a 404, it hits the catch-all route. Here you check if the URL is in the collection of old URLs.
  • If it exists, forward to the new page. If not, throw a 404.

The reason I’ve separated this to a separate function is to be able to call it from multiple parts of the application. For example, when implementing multi-language URL validation (e.g. “example.com/en/contact”) the url “example.com/contact” may return errors (“contact” is not a valid locale). In that case, extending the validation by checking for the old URL and forwarding to “example.com/_locale/contact” may be desired.


Sources