Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 61 additions & 14 deletions entry_types/scrolled/app/helpers/pageflow_scrolled/themes_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,20 @@ def scrolled_theme_properties_style_tag(theme)
end

def scrolled_theme_font_face_rules(theme)
theme.options.fetch(:font_faces, []).filter_map { |face|
FontFaceRule.new(face, theme:) { |path| scrolled_theme_asset_path(theme, path) }.generate
}.join("\n")
scrolled_theme_font_faces(theme).filter_map(&:generate).join("\n")
end

def scrolled_theme_font_preload_link_tags(theme)
safe_join(
scrolled_theme_font_faces(theme).filter_map(&:preload_source).map do |source|
tag.link(rel: 'preload',
as: 'font',
type: source[:type],
href: source[:url],
crossorigin: 'anonymous',
data: {theme: ''})
end
)
end

def scrolled_theme_typography_rules(theme)
Expand All @@ -59,6 +70,12 @@ def scrolled_theme_properties_rules(theme)

private

def scrolled_theme_font_faces(theme)
theme.options.fetch(:font_faces, []).map do |face|
FontFaceRule.new(face, theme:) { |path| scrolled_theme_asset_path(theme, path) }
end
end

# @api private
class FontFaceRule
FORMATS = {
Expand All @@ -68,6 +85,13 @@ class FontFaceRule
'.otf' => 'opentype'
}.freeze

MIME_TYPES = {
'woff2' => 'font/woff2',
'woff' => 'font/woff',
'truetype' => 'font/ttf',
'opentype' => 'font/otf'
}.freeze

WEIGHT_PATTERN = /\A(normal|bold|\d{1,4}( \d{1,4})?)\z/
STYLE_PATTERN = /\A(normal|italic)\z/
FORMAT_PATTERN = /\A(woff2?|truetype|opentype|embedded-opentype|svg)(-variations)?\z/
Expand All @@ -88,7 +112,7 @@ def initialize(face, theme:, &resolve_path)
end

def generate
return if family.blank? || source_values.empty?
return unless valid?

<<~CSS
@font-face {
Expand All @@ -97,10 +121,24 @@ def generate
CSS
end

# Only the first source is preloaded since the browser downloads
# exactly one of the alternative formats.
def preload_source
return unless valid? && face[:preload]

source = sources_with_safe_urls.first

{url: source[:url], type: mime_type(source)}
end

private

attr_reader :face, :theme

def valid?
family.present? && sources_with_safe_urls.any?
end

def declarations
[
%(font-family: "#{family}";),
Expand All @@ -115,10 +153,11 @@ def family
end

def source_values
@source_values ||=
sources
.reject { |source| source[:url].match?(UNSAFE_IN_URL) }
.map { |source| source_value(source[:url], source[:format]) }
sources_with_safe_urls.map { |source| source_value(source) }
end

def sources_with_safe_urls
@sources_with_safe_urls ||= sources.reject { |source| source[:url].match?(UNSAFE_IN_URL) }
end

def sources
Expand Down Expand Up @@ -151,14 +190,22 @@ def resolve_url(url)
@resolve_path.call(url).to_s
end

def source_value(url, format)
format = [format, FORMATS[extension(url)]].find do |candidate|
candidate.to_s.match?(FORMAT_PATTERN)
end
def source_value(source)
format = source_format(source)

return %(url("#{url}")) unless format
return %(url("#{source[:url]}")) unless format

%(url("#{url}") format("#{format}"))
%(url("#{source[:url]}") format("#{format}"))
end

def mime_type(source)
MIME_TYPES[source_format(source).to_s.delete_suffix('-variations')]
end

def source_format(source)
[source[:format], FORMATS[extension(source[:url])]].find do |candidate|
candidate.to_s.match?(FORMAT_PATTERN)
end
end

def extension(url)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<%= scrolled_sprockets_asset_tags(entry, entry_mode: entry_mode) %>
<%= scrolled_frontend_stylesheet_packs_tag(entry, entry_mode: entry_mode, seed_options: seed_options) %>

<%= scrolled_theme_font_preload_link_tags(entry.theme) %>
<%= scrolled_theme_properties_style_tag(entry.theme) %>
<%= scrolled_theme_stylesheet_pack_tags(entry.theme) %>

Expand Down
26 changes: 26 additions & 0 deletions entry_types/scrolled/doc/creating_themes/custom_typography.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ The following keys are supported:
| `style` | Either `normal` or `italic`. |
| `unicode_range` | Code points provided by the font file. See below. |
| `file_role` | Role of an uploaded theme customization file to use instead of `src`. |
| `preload` | Pass `true` to let the browser start downloading the font file right away. See below. |

`font-display: swap` is always included so that text remains visible
while font files are loading. Faces with invalid values are skipped.
Expand Down Expand Up @@ -82,6 +83,31 @@ specify formats per source:
'fonts/open-sans-400-normal.woff']}
```

### Preloading Fonts

Browsers only download a font file once they lay out text that uses
the font face. Mark the few faces that are needed for the first
screenful to have the file requested as early as possible:

``` ruby
{family: 'Open Sans',
weight: '400',
src: 'fonts/open-sans-400-normal.woff2',
preload: true}
```

Published entries then contain a link tag in the head:

``` html
<link rel="preload" as="font" type="font/woff2"
href="/assets/fonts/open-sans-400-normal.woff2" crossorigin="anonymous">
```

Only the first source of the face is preloaded since the browser
downloads exactly one of the alternative formats. Preloading more
fonts than the entry displays right away delays other resources - only
mark faces that are used above the fold.

### Reducing Font File Size

Fonts that support many scripts can be split into subsets. Declare one
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,138 @@ def upload_font(entry, file_name)
end
end

describe '#scrolled_theme_font_preload_link_tags' do
before do
allow(helper).to receive(:asset_pack_path) { |path| "/packs/#{path}" }
end

it 'renders link tag for face marked for preloading' do
theme = Pageflow::Theme.new(:test,
font_faces: [
{family: 'Avenir', src: '/fonts/a.woff2', preload: true}
])

html = helper.scrolled_theme_font_preload_link_tags(theme)

expect(html).to have_css('link[data-theme][rel="preload"][as="font"]' \
'[href="/fonts/a.woff2"][type="font/woff2"]' \
'[crossorigin="anonymous"]',
visible: false)
end

it 'skips faces not marked for preloading' do
theme = Pageflow::Theme.new(:test,
font_faces: [
{family: 'Avenir', src: '/fonts/a.woff2'},
{family: 'Oswald', src: '/fonts/b.woff2', preload: false}
])

html = helper.scrolled_theme_font_preload_link_tags(theme)

expect(html).to be_blank
end

it 'only preloads first source of face' do
theme = Pageflow::Theme.new(:test,
font_faces: [
{family: 'Avenir',
src: ['/fonts/a.woff2', '/fonts/a.woff'],
preload: true}
])

html = helper.scrolled_theme_font_preload_link_tags(theme)

expect(html).to have_css('link[href="/fonts/a.woff2"]', visible: false)
expect(html).not_to have_css('link[href="/fonts/a.woff"]', visible: false)
end

it 'resolves relative src in theme directory' do
theme = Pageflow::CustomizedTheme.find(
entry: create(:entry),
theme: Pageflow::Theme.new(:test,
font_faces: [
{family: 'Avenir', src: 'fonts/a.woff2', preload: true}
])
)

html = helper.scrolled_theme_font_preload_link_tags(theme)

expect(html).to have_css(
'link[href="/packs/static/pageflow-scrolled/themes/test/fonts/a.woff2"]',
visible: false
)
end

it 'derives type from format of source' do
theme = Pageflow::Theme.new(:test,
font_faces: [
{family: 'Avenir',
src: '/fonts/a.woff2',
format: 'woff2-variations',
preload: true}
])

html = helper.scrolled_theme_font_preload_link_tags(theme)

expect(html).to have_css('link[type="font/woff2"]', visible: false)
end

it 'omits type for unknown format' do
theme = Pageflow::Theme.new(:test,
font_faces: [
{family: 'Avenir', src: '/fonts/a.bin', preload: true}
])

html = helper.scrolled_theme_font_preload_link_tags(theme)

expect(html).to have_css('link[href="/fonts/a.bin"]:not([type])', visible: false)
end

it 'skips faces that do not result in a font face rule' do
theme = Pageflow::Theme.new(:test,
font_faces: [
{src: '/fonts/a.woff2', preload: true},
{family: 'Oswald', preload: true},
{family: 'Karla', src: '/fonts/a".woff2', preload: true}
])

html = helper.scrolled_theme_font_preload_link_tags(theme)

expect(html).to be_blank
end

it 'resolves file role of uploaded theme customization file' do
entry = create(:published_entry, type_name: 'scrolled')
file = Pageflow.theme_customizations.upload_file(
site: entry.site,
entry_type_name: 'scrolled',
type_name: :font,
attachment: fixture_file_upload('font.woff2')
)
Pageflow.theme_customizations.update(
site: entry.site,
entry_type_name: 'scrolled',
overrides: {
font_faces: [{family: 'font1', file_role: 'font_font1_400_normal', preload: true}]
},
file_ids: {font_font1_400_normal: file.id}
)

html = helper.scrolled_theme_font_preload_link_tags(entry.theme)

expect(html).to have_css('link[type="font/woff2"][href*="original/font.woff2"]',
visible: false)
end

it 'handles missing theme option' do
theme = Pageflow::Theme.new(:test)

html = helper.scrolled_theme_font_preload_link_tags(theme)

expect(html).to be_blank
end
end

describe '#scrolled_theme_typography_rules' do
it 'returns rules for theme' do
theme = Pageflow::Theme.new(:test,
Expand Down
Loading