If you've built Rails applications, you've likely used the popular High Voltage gem to handle static pages. But what happens when your application outgrows the constraints of this gem, particularly when using modern view components like Phlex within a multi-tenant or namespaced application?
In this post, I'll share how I built a custom page finder for our Rails application that supports traditional ERB templates and Phlex views across multiple namespaces without relying on external gems.
The Problem
I used High Voltage to render static pages in my multi-tenant Rails application. Each "app" (or tenant) within our application had its own namespace and potentially its own versions of each static page.
As I migrated from ERB to Phlex views, we encountered several challenges:
- Namespace Awareness: High Voltage wasn't designed to be aware of our app-specific namespaces
- Phlex Support: We needed to find both ERB templates and Phlex view classes
- Resolution Order: We needed a clear, predictable resolution order to find the correct view
- Debugging Difficulty: It was hard to debug why certain views weren't being found
The Solution
I decided to build my own custom page finder that would:
- Be namespace-aware for my multi-tenant setup
- Support both Phlex views and ERB templates
- Have a clear, customizable resolution order
- Include debugging capabilities that could be toggled via environment variables
The Implementation
Here's the complete implementation of our custom page finder:
Pages Controller
lang-ruby
# app/controllers/pages_controller.rb
class PagesController < ApplicationController
skip_before_action :authenticate!
before_action :set_locale
layout(LAYOUT_PROC)
def show
debug_page_finder("======== PAGE DEBUG ========")
debug_page_finder("Current.app: #{Current.app}")
debug_page_finder("params[:id]: #{params[:id]}")
debug_page_finder("Current page: #{current_page}")
if current_page.is_a?(String)
render(
template: current_page,
locals: { current_page: },
)
else
render current_page.new
end
end
def sitemap
@articles = Article.published.select(:id, :slug, :updated_at)
if Rails.root.join("app/views/pages/#{Current.app}/sitemap.xml.erb").exist?
render "pages/#{Current.app}/sitemap", layout: false
else
render "pages/sitemap", layout: false
end
end
def robots
render "pages/robots", layout: false, content_type: "text/plain"
end
private
def current_page
@current_page ||= page_finder.find
end
def page_finder
@page_finder ||= PageFinder.new(params[:id])
end
def debug_page_finder(message)
Rails.logger.debug(message) if ENV["DEBUG_PAGE_FINDER"].present?
end
class PageFinder
attr_reader :page_id
def initialize(page_id)
@page_id = page_id
end
def find
# First try to find a matching Phlex view
phlex_view = find_phlex_view
return phlex_view if phlex_view
# Then try to find a matching ERB template
path = find_erb_template
path || raise(ActionController::RoutingError, "No such page: #{page_id}")
end
private
def debug_page_finder(message)
Rails.logger.debug(message) if ENV["DEBUG_PAGE_FINDER"].present?
end
def find_phlex_view
classified_app = Current.app&.classify
classified_page_id = page_id.camelize # because classify, is silly (singularizes weirdly)
debug_page_finder("Finding Phlex View:")
debug_page_finder("App: #{Current.app}, page_id: #{page_id}")
debug_page_finder("Classified App: #{classified_app}, Classified page_id: #{classified_page_id}")
# Try in this order:
phlex_views = []
# 1. App-specific view with the current page_id: Views::Coding::Home
phlex_views << "Views::#{classified_app}::#{classified_page_id}" if classified_app.present?
# 2. App-specific view under Pages namespace: Views::Pages::Coding::Home
phlex_views << "Views::Pages::#{classified_app}::#{classified_page_id}" if classified_app.present?
# 3. Standard Pages view: Views::Pages::Home
phlex_views << "Views::Pages::#{classified_page_id}"
# 4. Direct view: Views::Home
phlex_views << "Views::#{classified_page_id}"
# Try each view class in order
phlex_views.each do |view_class|
debug_page_finder("Trying: #{view_class}, exists: #{Object.const_defined?(view_class)}")
return view_class.constantize if Object.const_defined?(view_class)
end
nil
end
def find_erb_template
content_path = Rails.root.join("app", "views", "pages")
# Try in this order:
templates = []
# 1. App-specific page: app/views/pages/coding/home.html.erb
templates << content_path.join(Current.app, page_id).to_s if Current.app.present?
# 2. Regular page: app/views/pages/home.html.erb
templates << content_path.join(page_id).to_s
# Check each template path
templates.each do |template_path|
erb_path = "#{template_path}.html.erb"
debug_page_finder("Checking ERB template: #{erb_path}")
return template_path if File.exist?("#{erb_path}")
end
nil
end
end
endRoutes Configuration
No changes compared to HighVoltage
lang-ruby # config/routes.rb Rails.application.routes.draw do # Static pages get "/*id" => "pages#show", as: :page, format: false root to: "pages#show", id: "home" end
How It Works
Let's break down how this custom page finder works:
1. Initialization
When a request comes in for a static page like `/about`, the `PagesController` creates a new `PageFinder` with the page ID from the params.
2. Resolution Order
The page finder attempts to find a matching view in the following order:
For Phlex Views:
- App-specific direct view: `Views::{App}::{Page}`
- Example: `Views::Coding::Home`
- App-specific view under Pages namespace: `Views::Pages::{App}::{Page}`
- Example: `Views::Pages::Coding::Home`
- Standard Pages view: `Views::Pages::{Page}`
- Example: `Views::Pages::Home`
- Direct view: `Views::{Page}`
- Example: `Views::Home`
For ERB Templates:
If no Phlex view is found, the page finder looks for ERB templates in:
- App-specific template: `app/views/pages/{app}/{page}.html.erb`
- Example: `app/views/pages/coding/home.html.erb`
- Standard template: `app/views/pages/{page}.html.erb`
- Example: `app/views/pages/home.html.erb`
3. Rendering
Once a view is found, it's handled differently based on its type:
- Phlex Views: Instantiated and rendered directly
- ERB Templates: Rendered as traditional templates
4. Debugging
The PageFinder includes comprehensive debugging support that can be enabled with an environment variable:
DEBUG_PAGE_FINDER=1 bin/rails server
