Dynamic routes with Ruby on Rails

Often the need to build dynamic pages with static routes into Ruby on Rails applications comes up. One such example is an, ‘About Us’, page. The content has to be modifiable through a web interface, but the route is typically static, such as, /about. There are many approaches to handling this without getting into building something complex like a full content management system.

Here is a minimal model to represent a page.

Page(id: uuid, title: string, permalink: string, content: string)

A typical model would have a resourceful route defined for it (resources :pages) letting a page be accessible at /pages/:id. Overriding Page#to_param and setting it to the permalink attribute is a step in the right direction by producing a URL like /pages/about. It’s not ideal to define a named route that points to an action which loads a particular page:

##
# This approach works, but has drawbacks...
#

# config/routes.rb
get 'about', to: 'pages#about'

# app/controllers/pages_controller.rb
...
def about
  @page = Page.find_by(permalink: 'about')
end
...

This works, but it’s too rigid. If the permalink changes, or another page is added, the routes and controller would need to be updated. Routes cannot be dynamically generated and updated (i.e. defining named routes for all pages at present and in the future). However, Rails provides routing constraints, which can be used to achieve something similar.

get ':permalink',
  to: 'pages#show',
  constraints: lambda { |request| Page.exists?(permalink: request[:permalink]) }

This effectively defines a top-level route for all pages. In most cases it’s best to place this route near the bottom, so that is has the lowest priority. Here’s the controller action to back this route up:

# app/controllers/pages_controller.rb
...
def show
  @page = Page.find_by(permalink: params[:permalink])
end
...

An advantage of this approach over routing all unmatched requests to a controller action is that the Rails router can still take care of serving the 404 page!