#117 Semi-Static Pages (revised)
- Download:
- source codeProject Files in Zip (63.6 KB)
- mp4Full Size H.264 Video (23.3 MB)
- m4vSmaller H.264 Video (10.1 MB)
- webmFull Size VP8 Video (10.6 MB)
- ogvFull Size Theora Video (23.5 MB)
In almost any Rails application there will be a few pages that contain static content such as an “About Us” page or a privacy policy. How should we structure these static pages? In this episode we’ll show you a few different ways to do this.
Static Static Pages
The “About Us” and “Privacy Policy” links don’t currently lead anywhere as we haven’t created these pages so we’ll do this now. When faced with a problem its a good idea to consider the simplest thing that could possibly work and here this means that we should avoid trying to shoehorn the static pages into a RESTful-style controller. It’s perfectly acceptable to make a non-RESTful controller in a Rails app so we’ll create a new info controller to hold these pages.
$ rails g controller info about privacy license
Each action in this controller will generally be left empty as it’ll only be rendering a view template. We could remove the method definitions from the controller but we’ll keep them as documentation for the actions that exist in the controller. We can now fill each view template with the static content we want to display. Once we’ve done this we can then visit, say, the info
page at /info/about
. The link to this page still doesn’t work, though, as we haven’t pointed it to the right action so we’ll fix that, too.
<%= link_to_unless_current "About Us", info_about_path %>
If we want our “About Us” page to be at /about
instead of at /info/about
we can make changes to our routes file. The generator created routes for our pages and we’ve been using these so far. If we want to change the path to /about
we’ll need to specify a controller and action as these can no longer be inferred from the path.
Store::Application.routes.draw do get "about", to: 'info#about' get "info/privacy" get "info/license" resources :products root to: 'products#index' end
We’ll also need to change the path in the layout file to point to the new path.
<%= link_to_unless_current "About Us", about_path %>
The link will now work at its new path. If we have many pages like these our routes file will get rather long so we’ll generate the routes dynamically instead, like this:
Store::Application.routes.draw do %w[about privacy license].each do |page| get page, controller: 'info', action: page end resources :products root to: 'products#index' end
This will work just the same as our previous routes did.
Dynamic Static Pages
While this approach generally works well with static pages it’s not perfect for every situation. The biggest issue is that the content of these pages is now part of the Rails app so if a change needs to be made a developer will have to modify the relevant view template, commit it to source control and then deploy the application. Having static information pages doesn’t scale well, either. If we have dozens of these pages maintaining them can become messy.
We can solve both of these problems by moving the content into the database and interacting with it through a RESTful-style controller. We’ll do this by generating some scaffolding to simulate an admin interface. (In a production application we’d need to create a proper administrative interface with something like ActiveAdmin which was covered in episode 284.) We’ll call this resource page and give it name
, permalink
and content
fields. We’ll also tell it to skip the stylesheets as we have some styling set up in this application. Note that we give the permalink
field an index as we’ll be searching by this.
$ rails g scaffold page name permalink:string:index content:text --skip-stylesheets $ rake db:migrate
If we visit /pages now we’ll see our scaffolding. We’ll make a new page with a permalink of about
and add it to the database. When we add this page we’re taken to the show
action which is what we want to display to the user. It needs a lot of work on how the content is displayed, however.
We’ll replace the default show
template with a simple template that displays the name
and content
. We’ll use simple_format
for the content so that blocks of text separated by line breaks are wrapped in paragraph tags.
<h1><%= @page.name %></h1> <%= simple_format @page.content %>
When we reload the page now it looks much better.
Next we’ll fix the URL. Our new “About Us” page has the path /pages/1
but it should instead be accessible from its permalink at /pages/about
. To enable we’ll modify the to_param
method in the Page
model. This is the method that Rails uses to convert a model into a URL parameter and we’ll have it return the permalink
. We’ll also add a validator to make sure that each permalink added is unique.
class Page < ActiveRecord::Base attr_accessible :content, :name, :permalink validates_uniqueness_of :permalink def to_param permalink end end
Next we’ll modify the PagesController
that was generated by the scaffolding and change the way we find the page in the show
action so that it’s found by its permalink
instead of by its id
.
def show @page = Page.find_by_permalink!(params[:id]) respond_to do |format| format.html # show.html.erb format.json { render json: @page } end end
Note that we use an exclamation mark with the find here so that a 404
error is raised if a page with a given permalink isn’t found. We’ll make a similar change to the edit
, update
and destroy
links so that we’re fetching a page in the same way each time. When we visit /pages/about
now we’ll see our “About Us” page. If we want to link to this page, like we do in the layout, we’ll need to change that path that the link points to from about_path
to page_path("about")
.
<%= link_to_unless_current "About Us", page_path("about") %> |
Better URLs For Static Pages
Clicking on the “About Us” link now points us to the right page but what if we want the path to just be /about
instead of /pages/about
? This is a little trickier as the permalink content is dynamic but it can be done. We’ll need to make some changes to the routes file to get this to work.
Store::Application.routes.draw do resources :pages, except: :show resources :products root to: 'products#index' get ':id', to: 'pages#show', as: :page end
We’ve removed most of the old routes here as they’re no longer necessary and instead added a catch-all route at the bottom of the file that points to the PagesController
’s show
action. We’ve given the route the name page so that the URL continues to work when we link to it. Note that the pages resources now excludes the show action so that this is routed instead by the catch-all route. With this change in place the “About Us” link now points to /about
and still links to our dynamic page. We need to be aware that now we have a catch-all route if we visit a non-existent path such as /foobar
this will trigger the PagesController
and raise a RecordNotFound
exception. This will give us a 404 error to the user, which is likely to be what we want, but we do need to remember that this controller will be hit by any path that’s entered and which doesn’t exist.
So far we’ve done all this without any external gems but there are some that we could consider using. One option is to use Redcarpet for formatting the page content using Markdown and this was covered in episode 207. Another formatting option is Liquid. This is handy if we have some dynamic content that we want to mix into the middle of the page’s content. This was covered in episode 118, although this episode is a little of of date now.
There are some gems that are designed to solve the static page problem as a whole. One is High Voltage which takes a similar approach to what we did at the beginning of this episode. It can be argued that this is simple enough to do from scratch but the gem is available if we want to use it. If our application has a lot of these content pages we could consider using a full-on content management system such as Refinery which was covered in episode 332, although there are many other CMS options to consider. Finally if we need to support internationalization on static pages we could consider using Copycopter which was covered in episode 336.