Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →Short answer: if Rails is returning an HTML string, put the CSS text inside a <style> element in that document, then call render html:. Use render inline: only when the string is an ERB template that must be evaluated. If the target is a PDF or image, pass the CSS string through the renderer’s inline-style option (for example, Grover’s style_tag_options). Nokogiri can parse or modify the markup, but it does not calculate browser-style CSS layout.
Start with the output you actually need
“Rendering HTML” can mean several different operations in Ruby. The correct way to load a CSS string depends on whether you are returning a web response, evaluating an ERB template, generating a PDF or image, or merely parsing markup.
| Need | Use | What it does |
|---|---|---|
| Return a small HTML document from Rails | render html: |
Returns HTML as text/html; a plain string is escaped unless marked safe. |
| Evaluate ERB tags held in a string | render inline: |
Runs the string as an ERB template, rather than returning it literally. |
| Apply raw CSS text to browser HTML | Embed it in a <style> element |
Creates a self-contained document the browser can style. |
| Produce a PDF, PNG or JPEG | Grover or another document renderer | Uses a browser engine; pass CSS through that library’s inline-style API. |
| Inspect or transform HTML structure | Nokogiri | Parses a tree; it is not a visual CSS renderer. |
Return HTML with a CSS string in Rails
Embed the CSS in the document head
The simplest route is to interpolate the CSS into a <style> block before rendering. This produces one HTML string containing both markup and styles:
class ReportsController < ApplicationController
def show
css = <<~CSS
body { font-family: sans-serif; margin: 2rem; }
.notice { color: #176b3a; font-weight: 600; }
CSS
html = <<~HTML
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
#{css}
</style>
</head>
<body>
<p class="notice">Ready</p>
</body>
</html>
HTML
render html: html.html_safe
end
end
The html_safe call is appropriate only when the complete string is trusted or has been safely constructed. Rails escapes a normal string passed to render html:; marking it safe tells Rails not to escape the document’s tags. Never use html_safe to push untrusted user input into a response. Escape user text and build markup with Rails tag helpers instead. CSS supplied by users also needs a separate policy: arbitrary CSS can inject styles, external requests or browser-specific behavior.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Keep the layout behavior explicit
An inline HTML response does not use a Rails layout by default. If you need the application layout, request it explicitly:
#1 Best Overall
render html: html.html_safe, layout: true
# or
render html: html.html_safe, layout: "print"
For a small, self-contained response, omitting the layout avoids accidentally adding a second document head or duplicate style tags. For a larger view, a normal template file is easier to maintain than a long Ruby heredoc.
Choose render inline: when the string is ERB
render html: returns literal HTML. It does not evaluate ERB delimiters. If the string itself contains template expressions, use render inline::
def greeting
@name = "Ada"
render inline: <<~ERB
<!doctype html>
<html>
<head>
<style>
.name { color: #176b3a; }
</style>
</head>
<body>
<h1 class="name">Hello, <%= @name %>!</h1>
</body>
</html>
ERB
end
Layouts are also off by default for inline templates; pass layout: true or a named layout when required. Rails guidance treats inline templates as a niche option: once the markup is more than a short response, keep it in a view file and load the CSS through the normal asset pipeline.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →When the CSS is a file or URL instead of a raw string
Use a stylesheet link for a resource, not for a CSS string:
<%= stylesheet_link_tag "reports", media: "all" %>
That helper emits a <link> element pointing to an asset or URL. It does not take the contents of a CSS string and inline them. If you need one self-contained response, read or construct the text and place it in <style>; if caching, fingerprinting and reuse matter, keep the stylesheet as an asset and link it.
Rank #2
Generate a PDF or image with inline CSS
Grover: pass CSS through style_tag_options
Grover accepts inline HTML and uses Puppeteer and Chromium to create PDF, PNG or JPEG output. Its inline style option takes a content field:
style_tag_options = [
{ content: <<~CSS
body { font-family: sans-serif; }
.body { background: red; padding: 24px; }
CSS
}
]
pdf = Grover.new(
'<html><body class="body"><h1>Heading</h1></body></html>',
style_tag_options: style_tag_options
).to_pdf
File.binwrite("report.pdf", pdf)
Grover also accepts stylesheet entries by URL or filesystem path. A direct call must account for relative assets: Chromium resolves relative URLs against the display URL, which defaults to http://example.com when no display URL is supplied. Set a suitable display URL or rewrite image, font and stylesheet references as absolute URLs so the browser can fetch them.
grover = Grover.new(
html,
display_url: "https://app.example.test/reports/42",
style_tag_options: [{ content: css }]
)
image_bytes = grover.to_png
Chromium is a runtime dependency, so deployment must include a compatible browser and the system libraries it needs. The available material does not establish performance, JavaScript parity or universal platform compatibility; verify those against the exact Grover, Puppeteer and Chromium versions you deploy.
WickedPDF: a documented string-to-PDF route
WickedPDF documents pdf_from_string for HTML supplied in memory and provides stylesheet helpers for linked files. Its documentation example is for version 0.9.4, so check the installed version before copying options into a current application. For file-backed stylesheets, use the absolute-path approach recommended by that documentation rather than assuming a relative URL will resolve in the PDF process.
Why Nokogiri does not “load” CSS
Nokogiri’s HTML5 API is useful for parsing and changing markup:
Rank #3
document = Nokogiri.HTML5(html)
fragment = Nokogiri::HTML5.fragment('<p class="notice">Ready</p>')
fragment.at_css(".notice").content = "Updated"
It builds a document tree; it does not run a browser layout engine, compute computed styles, load fonts, or paint pixels. Use it before a renderer when you need to insert a <style> node or alter classes, then send the resulting HTML to a browser-based PDF or image tool. The HTML5 API is not available on JRuby according to Nokogiri’s documentation, so JRuby applications need to confirm which parser API is supported in their version.
A safe, reusable helper for CSS strings
Centralize document construction so escaping and the doctype do not vary between controllers:
module HtmlDocument
module_function
def with_css(body_html, css)
<<~HTML
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>#{css}</style>
</head>
<body>#{body_html}</body>
</html>
HTML
end
end
html = HtmlDocument.with_css(
ERB::Util.html_escape("User supplied text"),
"body { color: #222; }"
)
render html: html.html_safe
In production code, prefer Rails tags or a view for complex or user-generated body content. The helper above demonstrates the boundary: trusted structure and CSS are assembled first, while user text is escaped before insertion.
“Or skip the browser setup”
If your actual goal is a clean screenshot of a rendered page rather than building a local Chromium pipeline, ScreenshotNeo accepts a URL and returns PNG, JPEG, WebP or PDF. It removes cookie-consent banners, newsletter popups and chat widgets before capture; bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and response headers report the page verdict and billing status. It also provides an MCP server with take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients.
One request is enough (see the ScreenshotNeo API documentation):
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Every feature is available on every plan. The free tier includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account to try it.
Rank #4
Troubleshooting common failures
The response shows escaped tags
Symptom: the browser displays <h1> as text. Cause: Rails escaped a normal string passed to render html:. Fix: mark only a trusted, correctly constructed document as safe, or return a normal template. Escape user values before interpolation; never mark raw request parameters safe.
ERB appears literally
Symptom: <%= @name %> reaches the browser unchanged. Cause: render html: returns literal text. Fix: use render inline: for a deliberately evaluated ERB string, or move the template into a view.
The page has no application layout
Cause: inline HTML and inline ERB rendering disable layouts by default. Fix: pass layout: true or a named layout, and ensure the layout does not add a second complete HTML document.
Recommended Free Tools
Styles work in a browser but not in a PDF or image
Check that the renderer received the CSS through its inline-style option, that the selector matches the generated markup, and that relative images and fonts resolve from the renderer’s display URL. Browser-engine output can differ by Chromium or library version, so reproduce with the same versions used in deployment.
Images or fonts are missing in Grover
Relative URLs may resolve against the default http://example.com. Supply display_url or use absolute, reachable URLs. Confirm that the rendering process has network access and permission to read local files.
Nokogiri output is structurally correct but unstyled
That is expected: parsing and serialization do not paint a page. Pass the serialized HTML to a browser renderer when visual output is required.
Best Value
The PDF process fails in production
Verify that Chromium and its native dependencies are installed for the deployment image, and pin compatible Grover, Puppeteer and browser versions. The Ruby code alone cannot supply a missing browser executable.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallOutdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchPerformance, reliability and cost decisions
- Small HTTP responses: inline CSS avoids an extra asset request but increases every response size. Use a cached asset for large, shared stylesheets.
- PDF/image jobs: browser startup and page loading dominate latency. Reuse a controlled rendering setup where the library supports it, and keep CSS and assets deterministic.
- Relative assets: make URL resolution explicit; failures often look like CSS bugs but are actually missing files, blocked network access or an incorrect base URL.
- Security: treat both HTML and CSS as code-like input. Escape user text, constrain any user-provided markup, and avoid exposing internal URLs to a renderer that can fetch arbitrary resources.
- Billing for ScreenshotNeo: only clean shots are billed; bot checks, blank pages, timeouts, failed loads and cache hits cost nothing, with the result described by response headers.
Practical decision checklist
- Decide whether you are returning HTML, evaluating ERB, or producing a PDF/image.
- For a Rails response, put trusted CSS text in a
<style>element and userender html:. - Use
render inline:only when ERB evaluation is intentional. - For Grover, pass CSS with
style_tag_options: [{ content: css }]and setdisplay_urlwhen relative assets exist. - Use Nokogiri for parsing and transformation, never as the visual renderer.
- Test escaping, layout selection, asset URLs and the exact production browser/runtime before shipping.
Frequently Asked Questions
Can I pass a CSS string directly to Rails’ stylesheet_link_tag helper?
No. That helper emits a link to a stylesheet resource. Put raw text in a style element, or save it as an asset and link the asset.
Should a CSS string be marked html_safe?
Only the surrounding document should be marked safe when its markup and CSS are trusted. Escape user-provided values before interpolation; do not use html_safe as a shortcut around input validation.
What does ScreenshotNeo return when a target page fails?
Its response identifies the page verdict and billing status in headers; failed loads, blank pages, bot checks, timeouts and cache hits are not billed.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

