Profile picture Schedule a Meeting
c a n d l a n d . n e t

Adding Sitepress to Rails

Dusty Candland | | rails, sitepress, seo

I've been using and thought it's about time I fixed some of the issues that Proofreader is designed to surface. Like SEO & social meta tags, sitemaps, robots.txt.

I choose Sitepress to handle the content side of things, which make doing a lot of this easier. Here's a walk though.

Install and setup Sitepress

Sitepress - "Build ambitious content websites in Rails"

Add the gems. You can skip markdown-rails and rouge if you don't want Markdown support or syntax highlighting.

gem "sitepress-rails"
gem "markdown-rails"
gem "rouge"

Run the installers. They will create the basic initializers and structure for the content. Content ends up in app/content, along with helpers and layouts.

bin/rails generate sitepress:install
bin/rails generate markdown_rails:install

Since these pages will be cached in production, we want to make sure they're signed out when generated. I don't actually know if this is needed, but I want it to work that way in development regardless.

I ended up creating a sub directory for the website controllers, app/controllers/website/.

I'm also using Pundit Can which requires that I skip some filters.

class Website::WebsiteController < Sitepress::SiteController
  layout "website"

  skip_scoped_check :show
  skip_authorized_check :show


  def signed_in?

  def current_user

  def current_account

  def setup_current
    Current.user = nil
    Current.true_user = nil
    Current.account = nil
    Current.ip = request.remote_ip

Next update the routes so they user the overriden controller. I also removed sitepress_root in favor of using the root: true parameter.

sitepress_pages root: true, controller: "website/website"

For some reason, Slim isn't loaded correctly. I added an initialer for it.


ActionView::Template::Handlers.extensions << :slim

Restart the server

Now Sitepress should be working and serving pages!

SEO & Social

Turns out url_for doesn't add anything when passed a string, so we need this helper. Add to app/content/helpers/page_helper.rb.

module PageHelper
  # `url_for` doesn't create a url when a string is passed
  def path_url path
      host: Rails.application.routes.default_url_options.fetch(:host,,
      port: Rails.application.routes.default_url_options.fetch(:port, request.port),
      path: path

Meta Tags

Next up, meta tags. These are used by the search engines and social sites to get a better understanding of the page. Open Graph for Facebook, Twitter has some specifice tags, and keywords & description for search engines.

This definitely could be done without a gem, but I liked the Meta Tags gem and used that.

Add to the Gemfile and bundle.

gem "meta-tags"

Install it. This adds a config, but I didn't change anything there.

rails generate meta_tags:install

I want to use the frontmatter from Sitepress to override data for the meta tags. This maps the frontmatter to a hash for the set_meta_tags method.

Add this to app/content/helpers/page_helper.rb

  def to_meta_tags page
      canonical: canonical(page),

  def canonical page
    path_url(page.request_path.gsub(/index\.html\z/, "").gsub(/\.html\z/, "").gsub(/\/\z/, ""))

The canonical method strips index.html, .html, and tailing slashes to get a normalized URL. Also uses the other helper method, path_url we added above.

Sitepress has some layout support, but I used the build in Rails layouts. This is where we connect Sitepress to meta_tags with the set_meta_tags method.


- set_meta_tags to_meta_tags(current_page)

- content_for :content do
  = yield

= render template: "layouts/application"

Add the display_meta_tags to the application layout, or head.html.slim in my case. Here is where I set the defaults for tags that might not be set.

= display_meta_tags site: "",
  reverse: true,
  description: "Automates monitoring the technical details to make sure your content shines!",
  image_src: image_url("manolo-chretien-252147-mono.jpg"),
  og: { title: :title,
    description: :description,
    site_name: :site,
    url: :canonical,
    image: :image_src,
    type: "website",
  twitter: { card: "summary", site: "@proofreaderio" }


It's a good idea to have a sitemap. To set up we'll need a route, controller, and view.

The route.

get '/sitemap.xml' => 'sitemaps#index', defaults: { format: 'xml' }

The controller.

class Website::SitemapsController < ApplicationController
  layout false

  skip_authorized_check :index
  skip_scoped_check :index

  def index
    render formats: :xml

The view. Here we use Sitepress and path_url to create a map of pages. I'm removing any that have noindex frontmatter.

xml.instruct! :xml, version: "1.0"
xml.tag! "urlset", "xmlns" => "" do
  site = "app/content")
  site.resources.reject{}.each do |resource|
    xml.tag! "url" do
      xml.tag! "loc", path_url(resource.request_path)
      xml.lastmod resource.asset.updated_at.strftime("%F")


Not huge fan of breadcrumbs, but they are helpful for SEO and large content sites. Again we can the Sitepress meta data to create them. parents is based on the file/folder stucture of the content and is exactly what I needed.

In the app/views/layouts/website.html.slim file, I added this:

/ ...
- content_for :content do
  - current_page.parents.any?
      - current_page.parents.reverse.each_with_index do |parent, idx|
          a href=parent.request_path =
        - if idx < current_page.parents.size - 1
          li \
/ ...


Very similar to the sitemap, we can have a more dynamic robots.txt. You could also use the one from the public directory, but I liked the idea of having a bit more control by moving into Rails.

The route.

get "/robots.txt" => "website/robots#index", :defaults => {format: "text"}

The controller.

class Website::RobotsController < ApplicationController
  layout false

  skip_authorized_check :index
  skip_scoped_check :index

  def index
    render format: :text

The view. Used ERB for this one. index.text.erb. Also added a reference to the sitemap.

<% if Rails.env.production? %>
User-Agent: *
<% else %>
User-Agent: *
Noindex: /
<% end %>

Sitemap: <%= path_url("/sitemap.xml") %>

Remove the on in public/

rm public/robots.txt

Error pages, 404, 422, 500

Not sure if this actually helps with SEO, but I like the idea of having nicer error pages. Of course, there are some cases where the app is actually broken and we'll end up with the Nginx default error pages. Even still, I moved into Rails.

Remove the defaults

rm public/{404,422,400}.html

Tell Rails to use our routes for the exceptions in config/application.rb.

    config.exceptions_app = routes

Here's the 404 page in app/content/pages/404.html. I'm using the Sitepress meta data to list some suggestions that be helpful. Also note the noindex frontmatter.

title: Oops, we couldn't find what you're looking for - 404
noindex: true
  h1 =

  h2 Maybe something here?
    - PageModel.all.reject{}.each do |page|
      li : a href=page.request_path =

And the 422 page.

title: The change you wanted was rejected - 422
noindex: true
  h1 =

  p Maybe you tried to change something you didn't have access to.

Lastly, the 500 page.

title: We're sorry, but something went wrong - 500
noindex: true
  h1 =

  p We've been notified of the error and will investigate it shortly.

  p Feel free to contact us if you have any questions!
    a href="/contact" Contact Us


This is built in! Probably already handled by Nginx.

# config/environments/production.rb

Rails.application.configure do
  # ...
  # force HTTPS on production
  config.force_ssl = true

Redirect www to non-www

Generally I prefer without www. Added this to the top of config/routes.rb. Any request starting with www. will be redirected without it.

Rails.application.routes.draw do
  constraints(host: /\Awww\./i) do
    get "(*any)" => redirect { |params, request|
      URI.parse(request.url).tap { |uri| =\Awww\./, "") }.to_s



Making the above change will break tests b/c the defaults host is Here's how to default to

Add to test/test_helper.rb.

class ActionDispatch::IntegrationTest
  before { host! "" }


As the application grows, there will probably be cases where we want to redirect old URLs. I setup a new routes file for them in config/routes/redirects.rb.

Rails.application.routes.draw do
  # Redirects:
  # get '/stories', to: redirect('/articles')
  # get '/stories/:name', to: redirect('/articles/%{name}')

We need to tell Rails about those additional routes files in config/application.rb. This also allows us to organize the routes.

    config.paths["config/routes.rb"].concat Dir[Rails.root.join("config/routes/*.rb")]


A lot of this was adapted and updated from SEO & Ruby On Rails : the comprehensive guide 2018


These are webmentions via the IndieWeb and Mention this post from your site: