Maintaining legacy routes

SEO can be the reason for improve URL namings. However, existing routes are still expected to work properly. After all, someone could have saved a URL or even linked it.

Legacy routes

An example might be landing pages for cities.
The URLs to the cities pages:

# config/routes.rb
Rails.application.routes.draw do
  resource :city, only: :show
end

look like this based on unique slugs (e.g. for Berlin): travel.com/city/berlin.

Redirect legacy routes

Let’s assume, that after these landing pages have gone live, one decided to rename the routes, like: travel.com/to/berlin. Of course, the old routes still should return proper response. The Rails routes generator provides the very nice ActionDispatch::Routing::Redirection#redirect to help out. Basically it redirects the old routes to the new routes. It also automatically sends a status code (by default 301 - Moved permanently) to the browser. With 301 most browsers remove the legacy route from their cache and cache the new route instead. The same practice holds true for search engines.
The routing generator with redirects looks like this for the example:

# config/routes.rb
Rails.application.routes.draw do
  # new route
  get 'to/:slug', to: 'cities#show', as: :city
  # legacy route will be redirected to new route
  get 'city/:slug', to: redirect('to/%{slug}')
end

Redirects with constraints

As soon as the redirect depends on logic, it is necessary to outsource this logic into Rack application, just to keep the routes.rb clean. That step also makes sense in case of handling multiple similar redirects.
A classic example is redirecting only the cities routes, which are already revealed (those which may be known at all). That applies to all cities, which were created until a certain time.
For all new cities the 404-page is expected to be rendered.
The extended routing generator:

# config/routes.rb
Rails.application.routes.draw do
  get 'to/:slug', to: 'cities#show', as: :city
  # The CityRedirector redirects
  # just the relevant cities
  get 'city/:slug', to: redirect('CityRedirector')
end

The CityRedirector loads all city slugs that were created until 2017-07-01 and checks if the slug parameter of the requested city is included. In case it is not, there is no redirect performed:

class CityRedirector
  DUE_DATE = Date.new(2017, 7, 1)

  def self.call(params, request)
    "to/#{params[:slug]}" if legacy_city_slugs.include?(params[:slug])
  end

  private

  def self.legacy_city_slugs
    @@legacy_city_slugs ||= City.where('created_at < ?', DUE_DATE)
                                .pluck('slug')
  end
end

After a some time, it is worthwhile to analyze the outdated routes, if they actually still are requested to some extent. If the amount of requests to the legacy routes is low, it makes sense to think about removing the legacy routes and their redirects likewise.