URL maintenance

For several reasons it may be necessary to change existing URLs (for example, SEO). Of course, the old routes should still work. After all, a URL could have been saved or even linked.

Legacy routes

City landing pages could stand as an example.
The URLs to the city pages:

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

For instance the slug for a city like Berlin accordingly looks like travel.com/city/berlin.

Redirect legacy routes

After these landing pages went live, the following routes are supposed to map the cities from now on: reisen.com/to/berlin. Of course the old routes should still work.
The helper #redirect is the friend in this regards and is available in the routes generator of Rails. Basically, it redirects the old routes to the new routes. It also automatically sends a status code 301 to the browser. Most browsers then remove the outdated route and cache the new route instead.
The routes generator now looks like this for the example:

# config/routes.rb
Rails.application.routes.draw do
  # new route
  resource :city, as: 'to', only: :show
  # legacy route is redirected to the new route
  get 'city/:slug', to: redirect('to/%{slug}')

Redirects with dependencies

As soon as the redirect depends on logic, it makes sense to extract this logic into a separate class, just to keep the routes.rb concise. Another reason may be to handle several similar cases.
A classic example is to only redirect the cities to the new route, which are already productive. So all the cities that were created in the system up to a certain point in time.
So it is expected to not support the legacy route structure for newly added cities for obvious reasons. Instead the 404-page should be rendered.
The extended routes generator:

# config/routes.rb
Rails.application.routes.draw do
  resource :city, as: 'to', only: :show
  # The CityRedirector is expected to only
  # redirect the routes to the relevant cities
  get 'city/:slug', to: redirect('CityRedirector')

The CityRedirector loads all city slugs, which were created until 01.01.2018 once and checks if the slug parameter of the requested city is included. If not, no redirect will be executed:

class CityRedirector
  NEW_CITY_SLUG_DATE = Date.new 2018, 1, 1

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


  def self.legacy_city_slugs
    @@legacy_city_slugs ||= City.where('created_at < ?', NEW_CITY_SLUG_DATE)

Alternatively, if the logic allows to move the redirects into the webserver itself already, then the redirect processing time can be even optimized. So for example the mod_rewrite for Apache (current version 2.4).
However, this requires maintaining routes at different places. Besides, the focus on legacy routes is rarely the performance.