Catch-all handling for old URLs in Silex to improve SEO
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.