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 end
Routes 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:
1. **App-specific direct view**: `Views::{App}::{Page}`
Example: `Views::Coding::Home`
2. **App-specific view under Pages namespace**: `Views::Pages::{App}::{Page}`
Example: `Views::Pages::Coding::Home`
3. **Standard Pages view**: `Views::Pages::{Page}`
Example: `Views::Pages::Home`
4. **Direct view**: `Views::{Page}`
Example: `Views::Home`
For ERB Templates:
If no Phlex view is found, the page finder looks for ERB templates in:
1. **App-specific template**: `app/views/pages/{app}/{page}.html.erb`
Example: `app/views/pages/coding/home.html.erb`
2. **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