Module: ReactOnRails::FontHelper
- Extended by:
- ActiveSupport::Concern
- Included in:
- Helper
- Defined in:
- lib/react_on_rails/font_helper.rb
Overview
First-party font optimization helper, a ‘next/font/local` analog for Rails.
Given a self-hosted (committed + fingerprinted) ‘.woff2` file already served by your asset pipeline, this returns the markup an ERB view should place in the document `<head>`:
1. `<link rel="preload" as="font" type="font/woff2" crossorigin>` so the
browser fetches the font in parallel with the first paint;
2. an `@font-face` rule with `font-display: swap` so text renders immediately
with a fallback and swaps in the web font when ready;
3. an optional metric-matched fallback `@font-face` (`size-adjust` plus
`ascent-override` / `descent-override` / `line-gap-override`) so the system
fallback occupies the same space as the web font, eliminating the layout
shift (CLS) that normally happens on the swap.
It mirrors the ‘<head>`-injection convention used by `react_component_hash` (see `ReactOnRails::Helper`): the caller wraps the return value in `content_for :head`, and the layout yields it inside `<head>`.
Example (ERB view):
<% content_for :head do %>
<%= react_on_rails_font_face(
family: "Inter",
src: asset_path("inter-latin-400-normal.woff2"),
weight: 400,
fallback: {
family: "Arial",
size_adjust: "107.12%",
ascent_override: "90.44%",
descent_override: "22.52%",
line_gap_override: "0.0%"
}
) %>
<% end %>
Then set the CSS font stack to the web font followed by its fallback face, e.g. ‘font-family: “Inter”, “Inter Fallback”, sans-serif;`.
Constant Summary collapse
- UNSAFE_TOKEN =
CSS/HTML metacharacters that would let an argument break out of the ‘<style>` / `<link>` context this helper emits.
/[<>"\r\n]/
Class Method Summary collapse
-
.ensure_safe!(name, value) ⇒ Object
Rejects values that could break out of the CSS/HTML context.
-
.fallback_font_face_rule(family, fallback, weight:, style:) ⇒ Object
Generates the metric-matched fallback face.
-
.font_face_markup(family:, src:, weight: 400, style: "normal", display: "swap", unicode_range: nil, preload: true, fallback: nil) ⇒ Object
Pure-string builder so the markup can be unit-tested without a view context.
- .font_face_rule(family:, src:, weight:, style:, display:, unicode_range:) ⇒ Object
- .preload_link(src) ⇒ Object
Instance Method Summary collapse
-
#react_on_rails_font_face(family:, src:, weight: 400, style: "normal", display: "swap", unicode_range: nil, preload: true, fallback: nil) ⇒ ActiveSupport::SafeBuffer
Emits the preload ‘<link>`, the primary `@font-face`, and (when `fallback:` is supplied) a metric-matched fallback `@font-face`.
Class Method Details
.ensure_safe!(name, value) ⇒ Object
Rejects values that could break out of the CSS/HTML context. Font helper arguments are emitted into trusted ‘<head>` markup, so they must be developer-controlled, not end-user input.
111 112 113 114 115 116 117 |
# File 'lib/react_on_rails/font_helper.rb', line 111 def self.ensure_safe!(name, value) return unless value.to_s.match?(UNSAFE_TOKEN) raise ArgumentError, "react_on_rails_font_face: #{name}=#{value.inspect} contains an unsafe character " \ "(<, >, \", or newline); font arguments must be developer-controlled, not end-user input." end |
.fallback_font_face_rule(family, fallback, weight:, style:) ⇒ Object
Generates the metric-matched fallback face. Hardcode ‘size-adjust` and the override percentages from the font’s published metrics (see the fonts docs for how the Inter-over-Arial numbers are derived). The ‘font-weight` and `font-style` mirror the primary face so the browser matches this fallback to the same elements (otherwise the size-adjust CLS protection silently fails for non-400 weights / non-normal styles), the way `next/font` generates its weight-matched fallback.
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/react_on_rails/font_helper.rb', line 144 def self.fallback_font_face_rule(family, fallback, weight:, style:) name = fallback[:name] || "#{family} Fallback" local = fallback.fetch(:family) lines = [ "@font-face {", %( font-family: "#{name}";), %( src: local("#{local}");), " font-weight: #{weight};", " font-style: #{style};" ] lines << " size-adjust: #{fallback[:size_adjust]};" if fallback[:size_adjust] lines << " ascent-override: #{fallback[:ascent_override]};" if fallback[:ascent_override] lines << " descent-override: #{fallback[:descent_override]};" if fallback[:descent_override] lines << " line-gap-override: #{fallback[:line_gap_override]};" if fallback[:line_gap_override] lines << "}" lines.join("\n") end |
.font_face_markup(family:, src:, weight: 400, style: "normal", display: "swap", unicode_range: nil, preload: true, fallback: nil) ⇒ Object
Pure-string builder so the markup can be unit-tested without a view context. Returns a plain (not html_safe) String.
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
# File 'lib/react_on_rails/font_helper.rb', line 82 def self.font_face_markup(family:, src:, weight: 400, style: "normal", display: "swap", unicode_range: nil, preload: true, fallback: nil) # Validate every value interpolated into the trusted <style>/<link> markup so no # argument can break out of the CSS/HTML context (see the contract in the docstring). ensure_safe!("family", family) ensure_safe!("src", src) ensure_safe!("weight", weight) ensure_safe!("style", style) ensure_safe!("display", display) ensure_safe!("unicode_range", unicode_range) if unicode_range if fallback ensure_safe!("fallback[:family]", fallback.fetch(:family)) ensure_safe!("fallback[:name]", fallback[:name]) if fallback[:name] %i[size_adjust ascent_override descent_override line_gap_override].each do |key| ensure_safe!("fallback[:#{key}]", fallback[key]) if fallback[key] end end rule = font_face_rule(family:, src:, weight:, style:, display:, unicode_range:) parts = [] parts << preload_link(src) if preload parts << +"<style>\n#{rule}" parts[-1] << "\n#{fallback_font_face_rule(family, fallback, weight:, style:)}" if fallback parts[-1] << "\n</style>" parts.join("\n") end |
.font_face_rule(family:, src:, weight:, style:, display:, unicode_range:) ⇒ Object
123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/react_on_rails/font_helper.rb', line 123 def self.font_face_rule(family:, src:, weight:, style:, display:, unicode_range:) lines = [ "@font-face {", %( font-family: "#{family}";), %( src: url("#{src}") format("woff2");), " font-weight: #{weight};", " font-style: #{style};", " font-display: #{display};" ] lines << " unicode-range: #{unicode_range};" if unicode_range lines << "}" lines.join("\n") end |
.preload_link(src) ⇒ Object
119 120 121 |
# File 'lib/react_on_rails/font_helper.rb', line 119 def self.preload_link(src) %(<link rel="preload" href="#{src}" as="font" type="font/woff2" crossorigin="anonymous">) end |
Instance Method Details
#react_on_rails_font_face(family:, src:, weight: 400, style: "normal", display: "swap", unicode_range: nil, preload: true, fallback: nil) ⇒ ActiveSupport::SafeBuffer
SECURITY: arguments are interpolated verbatim into the trusted CSS/HTML emitted into ‘<head>` and the result is marked `html_safe`. Pass only developer-controlled values (font names, asset paths), never end-user input. Values containing `<`, `>`, `“`, or a newline raise `ArgumentError`.
Emits the preload ‘<link>`, the primary `@font-face`, and (when `fallback:` is supplied) a metric-matched fallback `@font-face`.
72 73 74 75 76 77 78 |
# File 'lib/react_on_rails/font_helper.rb', line 72 def react_on_rails_font_face(family:, src:, weight: 400, style: "normal", display: "swap", unicode_range: nil, preload: true, fallback: nil) ReactOnRails::FontHelper.font_face_markup( family:, src:, weight:, style:, display:, unicode_range:, preload:, fallback: ).html_safe end |