Skip to main content
Documentation Architecture

Server-Rendered Routes

Bridgetown comes with a production-ready web server based on the battle-hardened one-two punch of Rack + Puma. On top of Puma we’ve layered on Roda, a refreshingly fast & lightweight web routing toolkit created by Jeremy Evans. On a basic level, it handles serving of all statically-built site files you access when you run bin/bridgetown start.

Bridgetown lets you create your own Roda-based API routes in the server/routes folder. An example ships in each new Bridgetown project for you to examine (server/routes/hello.rb.sample). These routes provide the standard features you may be accustomed to if you’ve used Roda standalone.

However, to take full advantage of all the Bridgetown has to offer, we recommend you load up our SSR and Dynamic Routes plugins. Simply add to your configuration in config/initializers.rb:

init :ssr
init :"bridgetown-routes"

# …or you can just init the routes, which will init :ssr automatically:

init :"bridgetown-routes"

For more information on setup, read our documentation on configuring Roda and on configuring Puma.

Table of Contents #

Bridgetown SSR via Roda #

Server-Side Rendering, known as SSR, has made its peace with SSG (Static Site Generation), and we are increasingly seeing an SSG/SSR “hybrid” architecture emerge in tooling throughout the web dev industry.

Bridgetown takes advantage of this evolving paradigm by providing a streamlined path for booting a site up in-memory. This means you can write a server-side API to render content whenever it is requested. Here’s an example of what that looks like:

# ./server/routes/preview.rb

class Routes::Preview < Bridgetown::Rack::Routes
  route do |r|
    r.on "preview" do
      # Our special rendering pathway to preview a page
      # route: /preview/:collection/:path
      r.get String, String do |collection, path|
        item = Bridgetown::Model::Base.find("repo://#{collection}/#{path}")

        unless item.content.present?
          response.status = 404
          next Bridgetown::Model::Base.find("repo://pages/_pages/404.html")
        end

        item
      end
    end
  end
end

This route handles any /preview/:collection/:path URLs which are accessed just like any other statically-generated resource. It will find a content item via a repo origin ID and render that item as HTML. For example: /preview/posts/_posts%2F2022-01-10-hello-world.md would SSR the Markdown content located in src/_posts/2022-01-10-hello-world.md.

If you’re wondering “but, uh, where’s the HTML rendering part?!”, the Bridgetown Roda configuration automatically handles the rendering of any models or resources which are returned in a route block.

SSR is great for generating preview content on-the-fly, but you can use it for any number of instances where it’s not feasible to pre-build your content. In addition, you can use SSR to “refresh” stale content…for example, you could pre-build all your product pages statically, but then request a newer version of the page (or better yet, just a fragment of it) whenever the static page is viewed which would then contain the up-to-date pricing (perhaps coming from a PostgreSQL database or some other external data source). And if you cache that data using Redis in, say, 10-minute increments, you’ve just built yourself an extremely performant e-commerce solution. This is only a single example!

Priority Flag #

You can configure a Routes class with a specific priority flag. This flag determines what order the router is loaded in relative to other routers.

The default priority is :normal. Valid values are:

:lowest, :low, :normal, :high, and :highest. Highest priority plugins are run first, lowest priority are run last.

Examples of specifying this flag:

class Routes::InitialSetup < Bridgetown::Rack::Routes
  priority :highest

  route do |r|
    r.session[:adding_this] ||= "value"
  end
end

class Routes::LaterOn < Bridgetown::Rack::Routes
  route do |r|
    r.get "later" do
      { session_value: r.session[:adding_this] } # :session_value => "value"
    end
  end
end

Accessing the Current Site, Collections, and Resources #

You can use the bridgetown_site helper in your Roda code to access the current site object. From there, you can access data, collections, and resources for aid in rendering. For example, if you knew of a particular resource by title you wanted to find, you could write:

bridgetown_site.collections.posts.resources.find { _1.data.title == "My Post" }

You can return a resource at the end of any Roda block to have it render out automatically, or you could pass it along as data to some other resource, or use some resource data within a return string or hash value (which autoconverts to JSON).

Performance considerations around loaded content

By default, all available collections are read in when the Roda server boots up. This might not be a big deal in production since it’s a one-time procedure, but bear in mind that on large sites, having all that data loaded in memory could prove costly. In addition, in development, any time you make a change to a file and the site rebuilds, resources are re-read into memory.

You can configure collections, including the built-in pages and posts collections, to be skipped when your site’s running in SSR mode. Just set skip_for_ssr to true for collection metadata in your config file. For example, to skip reading posts in config/initializers.rb:

Bridgetown.configure do
  # other configuration here

  collections do
    posts do
      skip_for_ssr true
    end
  end
end

Most of the time though, on modestly-sized sites, this shouldn’t prove to be a major issue.

File-based Dynamic Routes #

But wait, there’s more! We also provide a plugin called bridgetown-routes which gives you the ability to write file-based dynamic routes with integrated view templates right inside your source folder.

To opt-into the bridgetown-routes gem, make sure it’s enabled in your Gemfile:

gem "bridgetown-routes"

and added in config/initializers.rb:

init :"bridgetown-routes"

A file-based route is comprised of two parts:

  • A Roda block at the top, contained within special delimiters. This block is processed initially when the route URL is accessed, and before any template rendering has begun.
  • A view template underneath the Roda block, rendered via the render_with method inside the Roda block.

A Roda block can contain a single route handler via r.get, or you can add additional handling of HTTP methods (r.post, etc.) or even sub-routes—though it’s recommended to stay simple and use individual file-based routes as much as possible. Note that if even if you define multiple route types in your Roda block, you only have a single template per-route.

Let’s take a look at how this all works. First, an example of a route saved to src/_routes/items/index.erb. It provides the /items URL which shows a list of item links:

---<%
# route: /items
r.get do
  render_with data: {
    layout: :page,
    title: "Dynamic Items",
    items: [
      { number: 1, slug: "123-abc" },
      { number: 2, slug: "456-def" },
      { number: 3, slug: "789-xyz" },
    ]
  }
end
%>---

<ul>
  <% data.items.each do |item| %>
    <li><a href="/items/<%= item[:slug] %>">Item #<%= item[:number] %></a></li>
  <% end %>
</ul>

Since all the data in the above example is created and rendered by the server in real-time, there’s no way to know ahead of time which routes should be accessible via /items/:slug. That’s why bridgetown-routes supports routing placeholders at the filesystem level! Let’s go ahead and define our item-specific route in src/_routes/items/[slug].erb:

---<%
# route: /items/:slug
r.get do
  item_id, *item_sku = r.params[:slug].split("-")
  item_sku = item_sku.join("-")

  render_with data: {
    layout: :page,
    title: "Item Page",
    item_id: item_id,
    item_sku: item_sku
  }
end
%>---

<p><strong>Item ID:</strong> <%= data.item_id %></p>

<p><strong>Item SKU:</strong> <%= data.item_sku %></p>

This is a contrived example of course, but you can easily imagine loading a specific item from a data source based on the incoming parameter(s) and providing that item data to the view, all within a single file.

You can even use placeholders in folder names! A route saved to src/_routes/books/[id]/chapter/[chapter_id].erb would match to something like /books/234259/chapter/5 and let you access r.params[:id] and r.params[:chapter_id]. Pretty nifty.

Testing is straightforward as well. Simply place .test.rb files alongside your routes, and you’ll be able to use Capybara to write fast integration tests including interactions requiring Javascript (assuming Cuprite is also installed). (docs coming soon)

Route Template Delimiters #

Bridgetown lets you use a few different delimiters for the Roda block at the top, depending on your template format. For example, ---<% and %>--- would work well for an .erb file, but ###ruby and ### would be ideal for an .rb file.

See Ruby Front Matter for additional details (not that a Roda block is front matter, but the delimiters used are the same).

Routes in Islands Architecture #

You can add routes folders inside of one or more islands. For example, you could add a route file at src/_islands/paradise/routes/dreamy.erb, and the URL would then resolve to the island name plus the route name (/paradise/dreamy). If you name your route file index.(ext), then the route path would be just the island name (/paradise).

For more information about islands, read our Islands Architecture documentation.

URL Helpers #

You can use the relative_url and absolute_url helpers within your Roda code any time you need to reference a particular URL, to ensure any base path or locale prefix gets added automatically. It also will work with any object which responds to a method like relative_url or url. For example:

r.redirect relative_url("/path/to/page")

r.redirect relative_url(obj)

Islands Architecture