static-rails
Build and serve your static sites from your Rails app
tl;dr in development, static-rails runs your static site generators & proxies requests; in production, it compiles and serves the final assets
Static site generators are hot right now. Maybe you're hip with "the Jamstack", or maybe your API documentation is generated by Hugo, or maybe your marketing folks use Jekyll for the company blog.
Up until now, compiling static assets with any degree of sophistication beyond
dumping them in your app's public/
directory represented a significant
deviation from the "Rails Way". But the alternative—keeping your static sites
wholly separate from your Rails app—raises myriad operational challenges, from
tracking multiple git repositories, to managing multiple server configurations,
to figuring out a way to share common JavaScript and CSS assets between them
all.
No longer! static-rails lets you use your static asset generators of choice without forcing you to abandon your monolithic Rails architecture.
Here's what it does:
-
In development, static-rails launches your sites' local servers and then proxies any requests to wherever you've mounted them in your Rails app so you can start a single server and transition work between your static sites and Rails app seamlessly
-
When deploying, static-rails will compile all your static assets when
rake assets:precompile
is run, meaning your assets will be built automatically when pushed to a platform like Heroku -
In production, static-rails will serve your sites' compiled assets from disk with a similar features and performance to what you're familiar with if you've ever hosted files out of your
public/
directory
Install
Add this to your Gemfile:
gem "static-rails"
Then run this generator to create a configuration file
config/initializers/static.rb
:
$ rails g static_rails:initializer
You can check out the configuration options in the generated file's comments.
Want an example of setting things up? You're in luck, there's an example app right in this repo!
Configuring the gem
(Want to dive right in? The generated initializer enumerates every option and the example app's config sets up 4 sites.)
Top-level configuration
So, what should you stick in your initializer's StaticRails.config do |config|
block? These options are set right off the config
object and control the
overall behavior of the gem itself, across all your static sites:
-
config.proxy_requests (Default:
!Rails.env.production?
) when true, the gem's middleware requests that match where you've mounted your static site and proxy them to the development server -
config.serve_compiled_assets (Default:
Rails.env.production?
) when true, the gem's middleware will find your static assets on disk and serve them using the same code that Rails uses to serve files out ofpublic/
-
config.ping_server_timeout (Default:
5
) the number of seconds that (whenproxy_requests
is true, that the gem will wait for a response from a static site's server on any given request before timing out and raising an error -
config.set_csrf_token_cookie (Default:
false
) when true, the gem's middleware will set a cookie named_csrf_token
with each request of your static site. You can use this to set the'x-csrf-token'
header on any requests from your site back to routes hosted by the Rails app that are protected from CSRF forgery (if you're not using Rails' cookie store for sessions or you're okay with API calls bypassing Rails CSRF, leave this off)
Configuring your static sites themselves
To tell the gem about your static sites, assign an array of hashes as sites
(e.g. config.sites = [{…}]
). Each of those hashes have the following options:
-
name (Required) A unique name for the site (primarily used for logging)
-
source_dir (Required) The file path (relative to the Rails app's root) to the static site's project directory
-
url_subdomain (Default:
nil
) Constrains the static site's assets to only be served for requests to a given subdomain (e.g. for a Rails app hostingexample.com
, a Hugo site atblog.example.com
would set this to"blog"
) -
url_root_path (Default:
/
) The base URL path at which to mount the static site (e.g. if you want your Jekyll site hosted atexample.com/docs
, you'd set this to/docs
). For most static site generators, you'll want to configure it to serve assets from the same path so links and other references are correct (see below for examples) -
url_skip_paths_starting_with (Default:
[]
) If you want to mount your static site to/
but allow the Rails app to serve APIs from/api
, you can set the path prefix["/api"]
here to tell the gem's middleware not to try to proxy or serve the request from your static site, but rather let Rails handle it -
start_server (Default `!Rails.env.production?) When true, the gem will start the site's server (and if it ever exits, restart it) as your Rails app is booting up. All output from the server will be forwarded to STDOUT/STDERR
-
server_command (Required if
start_server
is true) the command to run to start the site's server, from the working directory ofsource_dir
(e.g.hugo server
) -
ping_server (Default: true) if this and
start_server
are both true, then wait to proxy any requests until the server is accepting TCP connections -
env (Default:
{}
) any environment variables you need to pass to either the server or compile commands (e.g.env: {"BUNDLE_PATH" => "vendor/bundle"}
). Note that this configuration file is Ruby, so if you need to provide different env vars based on Rails environment, you have the power to do that! -
server_host (Default:
localhost
) the host your static site's server will run on -
server_port (Required if
proxy_requests
is true) the port your static site's server will accept requests on -
server_path (Default:
"/"
) the root URL path to which requests should be proxied -
compile_comand (Required) the command to be run by both the
static:compile
andassets:precompile
Rake commands (e.g.npm run build
), with working directory set to the site'ssource_dir
-
compile_dir (Required when
serve_compiled_assets
is true) the root file path to which production assets are compiled, relative to the site'ssource_dir
-
compile_404_file_path (Optional) When
serve_compiled_assets
is true, this file (relative to thecompile_dir
) will be served whenever the request's path does not match a file on disk
Configuring your static site generators
Assuming you won't be mounting your static site to your app's root /
path,
you'll probably need to configure its base URL path somehow. Here are
some tips (and if your tool of choice isn't listed, we'd love a pull
request!):
Using Jekyll
If you have a Jekyll app that you plan on serving from a non-root path (say,
/docs
), then you'll need to set the baseurl
in _config.yml
:
baseurl: "/docs"
(Note that this means running the Jekyll application directly using bundle exec jekyll serve
will also start serving at http://127.0.0.1:4000/docs/
)
Using Hugo
If you are mounting your Hugo app to anything but the root
path, you'll need to specify that path in the baseURL
of your root
config.toml
file, like so:
baseURL = "http://blog.example.com/docs"
Additionally, getting Hugo to play nicely when being proxied by your Rails
server in development and test can be a little tricky, because most themes will
render fully-qualified URLs into markup when running the hugo server
command.
That means if you're forwarding http://blog.localhost:3000/docs
to a Hugo
server running on http://localhost:1313
, it's very likely the static files
(e.g. all the links on the page) will have references to
http://localhost:1313
, which may result in accidentally navigating away from
your Rails development server unexpectedly.
To mitigate this, there are a few things you can do:
- Favor
.RelPermalink
in your templates over.Permalink
where possible. - In place of referring to
{{.Site.BaseURL}}
in your templates, generate a base path with{{ "/" | relURL }}
(given the abovebaseURL
, this will render"/marketing/"
)
Also, because Hugo will serve /livereload.js
from the root, live-reloading probably
won't work in development when running through the static-rails proxy.
You might consider disabling it with --disableLiveReload
unless you're serving
Hugo from a root path ("/
").
A static-rails config for a Hugo configuration in sites
might look like:
{
name: "docs",
url_root_path: "/docs",
source_dir: "static/docs",
server_command: "hugo server",
server_port: 8080,
compile_command: "hugo",
compile_dir: "static/docs/public"
}
Using Eleventy
If you are mounting your Eleventy app to anything but
the root path, you'll want to configure a path prefix in .eleventy.js
module.exports = {
pathPrefix: "/docs/"
}
Alternatively, you can specify this from the command line with --pathprefix /docs
.
A static-rails config for an Eleventy configuration in sites
might look like:
{
name: "docs",
url_root_path: "/docs",
source_dir: "static/docs",
server_command: "npx @11ty/eleventy --serve --pathprefix /docs",
server_port: 8080,
compile_command: "npx @11ty/eleventy --pathprefix /docs",
compile_dir: "static/docs/_site"
}
Using Gatsby
If you're mounting a Gatsby site to a non-root path
(e.g. in static-rails, you've configured its url_root_path
to, say,
careers
), then you'll want to configure the same root path in Gatsby as well,
so that its development servers and built assets line up correctly.
To do this, first add the pathPrefix
property to gatsby-config.js
:
module.exports = {
pathPrefix: `/careers`,
// …
}
Next, add the flag --prefix-paths
to both the Gatsby site's server_command
and compile_command
, or else the setting will be ignored.
A static-rails config for a Gatsby configuration in sites
might look like:
{
name: "gatsby",
url_root_path: "/docs",
source_dir: "static/docs",
server_command: "npx gatsby develop --prefix-paths",
server_port: 8000,
compile_command: "npx gatsby build --prefix-paths",
compile_dir: "static/docs/public"
},
Code of Conduct
This project follows Test Double's code of conduct for all community interactions, including (but not limited to) one-on-one communications, public posts/comments, code reviews, pull requests, and GitHub issues. If violations occur, Test Double will take any action they deem appropriate for the infraction, up to and including blocking a user from the organization's repositories.