Render Rails Pages Without a Server
When debugging a Rails view or verifying template output, the typical workflow involves starting a development server, navigating to the page in a browser, and inspecting the HTML. This works, but it's slow and context-switching heavy.
I built a script that renders Rails pages directly from the command line. No server startup, no browser. Just point it at a route and get HTML.
Source: render.rb on GitHub
Basic Usage
# Render a page and show summary ./render.rb db/myevent.sqlite3 /people # Output: ✓ /people - 24680 bytes (24.1 KB) # Get full HTML output ./render.rb db/myevent.sqlite3 --html /heats > heats.html # Check if pages render successfully (silent, exit code only) ./render.rb db/myevent.sqlite3 --check /people /heats /solos echo $? # 0 = all succeeded, 1 = at least one failed # Search for specific content ./render.rb db/myevent.sqlite3 --search "Solos" /solos # Output: ✓ /solos - 'Solos' found The script accepts the database either as an argument or via the RAILS_APP_DB environment variable:
# These are equivalent: ./render.rb db/2025-boston.sqlite3 /people ./render.rb 2025-boston /people RAILS_APP_DB=2025-boston ./render.rb /people How It Works
The script bootstraps a minimal Rails environment and calls the routing stack directly:
# Load Rails without starting a server require File.expand_path('config/environment', rails_root) # Render a route env = { "PATH_INFO" => '/heats', "REQUEST_METHOD" => "GET", "QUERY_STRING" => "" } code, headers, response = Rails.application.routes.call(env) if code == 200 html = response.body.force_encoding('utf-8') puts "#{html.length} bytes" end That's it. No Rack server, no middleware stack (beyond what Rails installs), just direct routing.
Credit where due: This technique comes from Sitepress, a static site generator that integrates with Rails. Their Renderers::Server class uses the same approach to compile pages without starting a development server. My script adapts it for interactive debugging with database selection and command-line options.
Use Cases
1. Debugging Templates
When a view isn't rendering correctly, you can capture the HTML and inspect it:
./render.rb db/myevent.sqlite3 --html /people/42 > person.html open person.html # or pipe to less, grep, etc. Compare two versions:
git stash ./render.rb db/myevent.sqlite3 --html /heats > before.html git stash pop ./render.rb db/myevent.sqlite3 --html /heats > after.html diff before.html after.html 2. CI/CD Health Checks
Verify critical pages render without starting a full server:
#!/bin/bash # Run in CI after deployment ./render.rb production --check /people /heats /studios /solos if [ $? -ne 0 ]; then echo "Smoke test failed" exit 1 fi 3. Content Verification
Search for specific content in rendered output:
# Verify a person appears on the people page ./render.rb db/myevent.sqlite3 --search "John Smith" /people # Verify form elements exist ./render.rb db/myevent.sqlite3 --search 'name="person[name]"' /people/new 4. Multiple Pages at Once
Test several routes in one command:
./render.rb db/myevent.sqlite3 /people /heats /solos /studios # Output: # ✓ /people - 24680 bytes (24.1 KB) # ✓ /heats - 156234 bytes (152.6 KB) # ✓ /solos - 8934 bytes (8.7 KB) # ✓ /studios - 4521 bytes (4.4 KB) 5. Query String Support
Routes with query parameters work:
./render.rb db/myevent.sqlite3 --html "/people?type=student" > students.html Why Not Just Use curl?
You could curl localhost:3000/people after starting a server, but:
- Server startup is slow - Rails boot time adds seconds to every check
- Port conflicts - Another process might be using port 3000
- State management - You need to start/stop the server
- No database switching - Multi-tenant apps need different databases per request
The script eliminates all of this. Point at a database, get HTML.
LLM-Assisted Development
This script originated as a Claude Code skill—a way to give AI assistants visibility into what the application actually renders.
The primary use case is iterative debugging. I point Claude Code at a page and describe what's wrong:
"The /heats page is showing duplicate entries for heat 42. Fix it."
Claude can then:
- Read the rendered HTML to see exactly what's displaying
- Identify the problem in the controller or view
- Make a fix to the relevant code
- Re-render the page to verify the fix worked
- Iterate if the first attempt didn't solve it
Without this script, Claude would be guessing from template code alone. ERB templates with conditionals, loops, and partials are hard to reason about without seeing the actual output. The script closes that feedback loop.
A typical debugging session:
Me: The person show page for person 30 is missing their partner info. Claude: Let me render that page to see what's displaying. [runs: ./render.rb db/myevent.sqlite3 --html /people/30] I see the issue - the partner section is empty because the @partner variable isn't being set when the person is a follow. Let me fix the controller... [edits people_controller.rb] Let me verify that fixed it. [runs: ./render.rb db/myevent.sqlite3 --search "Partner:" /people/30] ✓ /people/30 - 'Partner:' found The partner info now displays correctly. This verify-fix-verify cycle often takes multiple iterations. Complex bugs might require three or four rounds of rendering, inspecting, and adjusting. The script makes each iteration fast—no server restarts, no browser refreshes, just render and check.
No "did you restart the server?" confusion. With a running development server, there's always a question: is the server running the latest code? Did the cache get flushed? Too often I've seen Claude confidently claim a fix worked, only to suggest I "must not have deployed" when the bug persists. With this script, every render loads the current code fresh. If the fix didn't work, the fix didn't work—no ambiguity about stale code or cached responses.
Why this matters for LLM-assisted development:
- Concrete over abstract: HTML output is unambiguous; template code requires interpretation
- Fast feedback: Each render takes seconds, enabling rapid iteration
- Always current code: No server to restart, no cache to flush—edits take effect immediately
- Searchable verification:
--searchconfirms specific content exists without parsing full HTML - Deterministic: Same database + same code = same output, no session state surprises
The skill wrapper tells Claude when and how to invoke the script. But the script itself has no LLM dependencies—it's plain Ruby that works standalone:
# Human usage - debug a template ./render.rb db/myevent.sqlite3 --html /heats | grep -A5 "Heat 42" # Or use in any script ruby -e ' system("./render.rb db/myevent.sqlite3 --check /heats") or puts "Page broken!" ' Adapting for Your Application
The script assumes a specific database selection pattern (RAILS_APP_DB environment variable) that I use for multi-tenant SQLite. You'll need to adapt this for your setup:
Single database (typical Rails app):
# Remove database selection logic, just load environment require File.expand_path('config/environment', rails_root) PostgreSQL multi-tenant:
# Set DATABASE_URL or connection switching ENV['DATABASE_URL'] = "postgres://localhost/#{database}" require File.expand_path('config/environment', rails_root) With authentication:
# Add session/user context to the env hash env = { "PATH_INFO" => path, "REQUEST_METHOD" => "GET", "rack.session" => { user_id: 1 }, # Simulated session # ... other Rack env vars } The core pattern—calling Rails.application.routes.call(env)—works for any Rails app.
Command Line Options
Usage: render.rb DATABASE [options] PATH [PATH...] Arguments: DATABASE Database file or name (e.g., db/2025-boston.sqlite3 or 2025-boston) Can also be 'test' or 'demo' Alternative: Set RAILS_APP_DB environment variable Options: --check Only check if page renders (exit 0 on success, 1 on failure) --html Output full HTML content (works with single path only) --search TEXT Search for specific text in rendered output -v, --verbose Show detailed information -h, --help Show help message Grab It
The script is MIT licensed and available at:
github.com/rubys/showcase/.claude/skills/render-page/scripts/render.rb
Copy it, adapt it to your database setup, and use it. It's 280 lines of Ruby with no dependencies beyond Rails itself.
For the Rails application this script was built for, see github.com/rubys/showcase. For background on Claude Code skills, see the official documentation.