Single File Rails Apps

Image by Annie Ruygt
At first glance single file Rails application seems like a toy project, but they do have practical applications. Let's take a look at how Sitepress, a semi-static site generator, uses a single page Rails application at its core for a site-generator that can both compile static websites and run inside of Rails.

There's been a few interesting blog posts written about single file Rails applications, but they all seem to stop short of describing practical use cases where you might actually ship a single page Rails application.

Problem: I Need a Lightweight CMS in a Rails Application

A few years ago I was cleaning up a large Rails application. Part of that clean-up job was consolidating all of the different ways content was being managed around the application. The most common thing I've seen is a controller created per page, then that controller's #show action being pointed towards that one content page. This is a very common pattern in small Rails applications, and it gets tedious fast.

At the same time, I was building static websites with Middleman, which had fantastic APIs like Sitemap, Frontmatter, and various view helpers that made building websites fun. How could I get that inside Rails?

The obvious thing to do is try adding the middleman gem to Rails, but I found out quickly that wasn't going to work.

Before a Gem Is Born

The first thing I do before I write any code for a gem is write down a few basic requirement.

Here's what I needed for my project:

  1. Middleman features, like Sitemap and Frontmatter, that work in Rails.
  2. Must work from within an existing Rails application.
  3. Bonus if I can run a compilation command that emits static HTML, CSS, and JavaScript files.

Once I have my requirements, I start looking around to see if something already exists that satisfy them.

The closest project I found that could be embedded in Rails was High Voltage, but it lacked a Sitemap and Frontmatter. I did a few experiments where I tried embedding Middleman and Jecklly into a Rails application, but those quickly failed.

I quickly realized the only path forward was to create a new gem, and Sitepress was born.

Say Hello to Sitepress

When Sitepress was originally built, it was a goal to have it run both inside Rails and outside of Rails. I spent most of my time getting Sitepress working great inside of Rails.

Getting it working outside of Rails was a pain. I wired up a rack server, used Tilt for rendering, then I needed helpers. Getting layouts working properly was full of bugs. I needed to have a decent answer for assets. The more I got into it, the more I felt like I was re-inventing a lot of Rails features, and even worse, wasting my time trying to re-integrate all of this stuff so it would work similar to Rails.

About the time I felt like giving up on the stand-alone version of Sitepress, I ran across a blog post entitled, "Single File Rails Applications". That's when the light-bulb went off in my head and I re-framed the problem for stand-alone Sitepress, "what if I can use a Rails application to run stand-alone Sitepress?"

If I could pull that off I'd get access to tailwind-rails, an asset pipeline, all its view helpers, a layout and templating system that works, and access to an entire ecosystem of plugins.

A Look at Sitepress's Single File Rails App

The fun thing about Sitepress is that its just a Rails app so you get to use all of the helpers you're already familier with in Rails. This means if you're into Majestic Monoliths, you can embed Sitepress in your Rails application and have a great content management system without the complexity of backing it by a database.

#
require "action_controller/railtie"
require "sprockets/railtie"
require "sitepress-rails"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

# Configure the rails application.
module Sitepress
  class Server < Rails::Application
    # Control whether or not to display friendly error reporting messages
    # in Sitepress. The development server turns this on an handles exception,
    # while the compile and other environments would likely have this disabled.
    config.enable_site_error_reporting = false

    # When in a development environment, we'll want to reload the site between
    # requests so we can see the latest changes; otherwise, load the site once
    # and we're done.
    config.enable_site_reloading = false

    # Default to a development environment type of configuration, which would reload the site.
    # This gets reset later depending on a preference in the `before_initialize` callback.
    config.eager_load = true
    config.cache_classes = true

    config.before_initialize do
      # Eager load classes, content, etc. to boost performance when site reloading is disabled.
      config.eager_load = !config.enable_site_reloading

      # Cache classes for speed in production environments when site reloading is disabled.
      config.cache_classes = !config.enable_site_reloading
    end

    # Path that points the the Sitepress UI rails app; which displays routes, error messages.
    # etc. to the user if `enable_site_error_reporting` is enabled.
    config.root = File.join(File.dirname(__FILE__), "../../rails")

    # Rails won't start without this.
    config.secret_key_base = SecureRandom.uuid

    # Setup routes. The `constraints` key is set to `nil` so the `SiteController` can
    # treat a page not being found as an exception, which it then handles. If the constraint
    # was set to the default, Sitepress would hand off routing back to rails if something isn't
    # found and fail silently.
    routes.append { sitepress_pages root: true, controller: "site", constraints: nil }

    # A logger without a formatter will crash when Sprockets is enabled.
    logger           = ActiveSupport::Logger.new(STDOUT)
    logger.formatter = config.log_formatter
    config.logger    = ActiveSupport::TaggedLogging.new(logger)

    # Debug mode disables concatenation and preprocessing of assets.
    # This option may cause significant delays in view rendering with a large
    # number of complex assets.
    config.assets.debug = false

    # Suppress logger output for asset requests.
    config.assets.quiet = true

    # Do not fallback to assets pipeline if a precompiled asset is missed.
    config.assets.compile = true

    # Allow any host to connect to the development server. The actual binding is
    # controlled by server in the `sitepress-cli`; not by Rails.
    config.hosts << proc { true } if config.respond_to? :hosts

    # Stand-alone boot locations
    paths["config/initializers"] << File.expand_path("./config/initializers")
  end
end

This is a big win for individuals or small teams who want to publish a few landing pages, some help pages, and terms of service, etc. and as Fly has shown, it's pretty straight forward to get this running quickly around the world, close to your customers, without adding the latency of a CDN to your application.

When Do Single File Rails Applications Make Sense?

Single page Rails applications shine when you need to deploy Rails applications to a bunch of workstations, as opposed to deploying them to your servers.

  • Preview servers for web content - Static site builders, like Sitepress, use single page Ruby applications to boot a preview server that developers can use to preview their changes.

  • Local Web UI that manages local services - Maybe you need to build a GUI that will run on a server in a data center or a workstation that's accessible remotely, but you don't want all the "heft" of a full blown rails app.

  • Server-Side Rendered site - Static websites are fast and have low operational complexity, but maybe you want to do a few dynamic things on the server like localize content for people. A semi-static website can help that's running on a small Rails app.

  • Test cases - When reporting a bug in Rails core, it's helpful to isolate the exact Rails gems and configuration to help a maintain reproduce a bug. This approach can save a lot of time for the maintainer and increase the liklehood that your issue will get fixed.

This shortlist will hopefully give you a few practical ideas that you can add to your toolbox for developing Rails applications.