diff --git a/app/helpers/pageflow/themes_helper.rb b/app/helpers/pageflow/themes_helper.rb index 9eaa19bca3..6246599677 100644 --- a/app/helpers/pageflow/themes_helper.rb +++ b/app/helpers/pageflow/themes_helper.rb @@ -6,10 +6,11 @@ def themes_options_json_seed(config = Pageflow.config) config.themes.to_h { |theme| [theme.name, theme.options] }.to_json.html_safe end - def theme_json_seeds(config) + def theme_json_seeds(config, theme_asset_resolver: SprocketsThemeAssetResolver.new) render_json_partial('pageflow/themes/theme', collection: config.themes.to_a, - as: :theme) + as: :theme, + theme_asset_resolver:) end end end diff --git a/app/views/pageflow/editor/entries/seed.json.erb b/app/views/pageflow/editor/entries/seed.json.erb index f86c29dfc0..349e8cec29 100644 --- a/app/views/pageflow/editor/entries/seed.json.erb +++ b/app/views/pageflow/editor/entries/seed.json.erb @@ -16,7 +16,10 @@ "site": <%= render_json_seed(@entry.site) %>, "widget_types": <%= widget_types_json_seeds(@entry_config) %>, "page_types": <%= page_type_json_seeds(@entry_config) %>, - "themes": <%= theme_json_seeds(@entry_config) %>, + "themes": <%= theme_json_seeds( + @entry_config, + theme_asset_resolver: @entry.entry_type.theme_asset_resolver + ) %>, "common": <%= render_json { |json| common_entry_seed(json, @entry) }.html_safe %>, diff --git a/app/views/pageflow/editor/entries/show.html.erb b/app/views/pageflow/editor/entries/show.html.erb index 6997fceed1..cb723c768c 100644 --- a/app/views/pageflow/editor/entries/show.html.erb +++ b/app/views/pageflow/editor/entries/show.html.erb @@ -3,7 +3,6 @@ <% content_for(:head) do %> <%= editor_entry_type_fragment(@entry, :head) %> - <%= entry_theme_stylesheet_link_tag(@entry) %> <%= entry_stylesheet_link_tag(@entry) %> <% end %> diff --git a/app/views/pageflow/structured_data/_entry.json.jbuilder b/app/views/pageflow/structured_data/_entry.json.jbuilder index 35cfd0ab7c..e127fa278f 100644 --- a/app/views/pageflow/structured_data/_entry.json.jbuilder +++ b/app/views/pageflow/structured_data/_entry.json.jbuilder @@ -23,7 +23,9 @@ if meta_data[:publisher].present? json.name meta_data[:publisher].split(',').map(&:squish) json.logo do json.set! '@type', 'ImageObject' - json.url structured_data_normalize_protocol(asset_url(entry.theme.print_logo_path)) + json.url structured_data_normalize_protocol( + entry.entry_type.theme_asset_resolver.publisher_logo_url(entry, view_context: self) + ) end end end diff --git a/app/views/pageflow/themes/_theme.json.jbuilder b/app/views/pageflow/themes/_theme.json.jbuilder index 552cee889e..1dcc29ecef 100644 --- a/app/views/pageflow/themes/_theme.json.jbuilder +++ b/app/views/pageflow/themes/_theme.json.jbuilder @@ -1,8 +1,7 @@ json.(theme, :name, :options) -json.preview_image_url(image_url(theme.preview_image_path)) -json.preview_thumbnail_url(image_url(theme.preview_thumbnail_path)) -json.stylesheet_path(stylesheet_path(theme.stylesheet_path)) +json.preview_image_url(theme_asset_resolver.preview_image_url(theme, view_context: self)) +json.preview_thumbnail_url(theme_asset_resolver.preview_thumbnail_url(theme, view_context: self)) json.home_button theme.has_home_button? json.overview_button theme.has_overview_button? diff --git a/entry_types/paged/app/views/pageflow_paged/editor/entries/_head.html.erb b/entry_types/paged/app/views/pageflow_paged/editor/entries/_head.html.erb index ee4d0f4314..46e4fcb3fe 100644 --- a/entry_types/paged/app/views/pageflow_paged/editor/entries/_head.html.erb +++ b/entry_types/paged/app/views/pageflow_paged/editor/entries/_head.html.erb @@ -1,5 +1,6 @@ <%= stylesheet_link_tag 'pageflow/application_with_simulated_media_queries', media: 'all' %> <%= stylesheet_link_tag 'pageflow_paged/editor', media: 'all' %> +<%= entry_theme_stylesheet_link_tag(entry) %> <%= render 'layouts/pageflow_paged/loading_spinner_inline_script' %> diff --git a/entry_types/paged/app/views/pageflow_paged/editor/entries/_seed.json.jbuilder b/entry_types/paged/app/views/pageflow_paged/editor/entries/_seed.json.jbuilder index 419e0443e4..f3e75e289e 100644 --- a/entry_types/paged/app/views/pageflow_paged/editor/entries/_seed.json.jbuilder +++ b/entry_types/paged/app/views/pageflow_paged/editor/entries/_seed.json.jbuilder @@ -1 +1,5 @@ -# Pageflow Paged does not use editor JSON seed +theme_stylesheet_paths = Pageflow.config_for(entry).themes.to_h do |theme| + [theme.name, stylesheet_path(theme.stylesheet_path)] +end + +json.theme_stylesheet_paths(theme_stylesheet_paths) diff --git a/entry_types/paged/lib/pageflow_paged.rb b/entry_types/paged/lib/pageflow_paged.rb index 551ccefd1f..01b1b5852f 100644 --- a/entry_types/paged/lib/pageflow_paged.rb +++ b/entry_types/paged/lib/pageflow_paged.rb @@ -12,7 +12,8 @@ def entry_type frontend_app: PageflowPaged::EntriesController.action(:show), configuration: PageflowPaged::Configuration, editor_app: PageflowPaged::Engine, - editor_fragment_renderer:) + editor_fragment_renderer:, + theme_asset_resolver: Pageflow::SprocketsThemeAssetResolver.new) end private diff --git a/entry_types/paged/packages/pageflow-paged/src/editor/initializers/stylesheetReloading.js b/entry_types/paged/packages/pageflow-paged/src/editor/initializers/stylesheetReloading.js index 3ddd75bf82..7a06f534c3 100644 --- a/entry_types/paged/packages/pageflow-paged/src/editor/initializers/stylesheetReloading.js +++ b/entry_types/paged/packages/pageflow-paged/src/editor/initializers/stylesheetReloading.js @@ -14,7 +14,6 @@ editor.addInitializer(function(options) { }); state.entry.metadata.on('change:theme_name', function() { - var theme = state.entry.getTheme(); - stylesheet.update('theme', theme.get('stylesheet_path')); + stylesheet.update('theme', state.entry.getThemeStylesheetPath()); }); }); diff --git a/entry_types/paged/packages/pageflow-paged/src/editor/models/PagedEntry.js b/entry_types/paged/packages/pageflow-paged/src/editor/models/PagedEntry.js index d4eb955b87..dcc77dd9ce 100644 --- a/entry_types/paged/packages/pageflow-paged/src/editor/models/PagedEntry.js +++ b/entry_types/paged/packages/pageflow-paged/src/editor/models/PagedEntry.js @@ -4,6 +4,8 @@ import {PreviewEntryData} from './PreviewEntryData'; export const PagedEntry = Entry.extend({ setupFromEntryTypeSeed(seed, state){ + this.themeStylesheetPaths = seed.theme_stylesheet_paths; + state.entryData = new PreviewEntryData({ entry: this, storylines: state.storylines, @@ -12,6 +14,10 @@ export const PagedEntry = Entry.extend({ }); }, + getThemeStylesheetPath: function() { + return this.themeStylesheetPaths[this.metadata.get('theme_name')]; + }, + supportsPhoneEmulation: function() { return features.isEnabled('editor_emulation_mode') } diff --git a/entry_types/paged/spec/controllers/pageflow_paged/editor/entries_controller_spec.rb b/entry_types/paged/spec/controllers/pageflow_paged/editor/entries_controller_spec.rb index e7c5ebadfd..ae2eba91fc 100644 --- a/entry_types/paged/spec/controllers/pageflow_paged/editor/entries_controller_spec.rb +++ b/entry_types/paged/spec/controllers/pageflow_paged/editor/entries_controller_spec.rb @@ -17,6 +17,28 @@ def main_app Rails.application.class.routes.url_helpers end + describe 'editor fragments' do + it 'renders the theme stylesheet in the head fragment' do + entry = Pageflow::DraftEntry.find(create(:entry).id) + + result = PageflowPaged.entry_type.editor_fragment_renderer.head_fragment(entry) + + expect(result).to have_selector('link[data-name="theme"]', visible: false) + end + + it 'includes theme stylesheet paths in the seed fragment' do + entry = Pageflow::DraftEntry.find(create(:entry).id) + + result = JSON.parse( + PageflowPaged.entry_type.editor_fragment_renderer.seed_fragment(entry) + ) + + expect(result.dig('theme_stylesheet_paths', 'default')).to( + match(%r{/assets/pageflow/themes/default-[a-f0-9]+\.css}) + ) + end + end + describe '#partials' do it 'reponds with success' do entry = create(:entry) diff --git a/entry_types/scrolled/doc/creating_themes.md b/entry_types/scrolled/doc/creating_themes.md index 0ac2b1106f..d63fe6607f 100644 --- a/entry_types/scrolled/doc/creating_themes.md +++ b/entry_types/scrolled/doc/creating_themes.md @@ -11,6 +11,15 @@ only a subset of icons [required for modern browsers](https://evilmartians.com/chronicles/how-to-favicon-in-2021-six-files-that-fit-most-needs) has been included. Webmanifests are generated dynamically. +The following images are used by the editor and for structured data: + +* `preview.png` +* `preview_thumbnail.png` +* `logo_print.png` + +If one of these files is missing, Pageflow falls back to resolving it +from the legacy Sprockets theme directory. + Register the theme in the theme plugin created by the `pageflow_scrolled:install` generator: diff --git a/entry_types/scrolled/lib/generators/pageflow_scrolled/install/templates/theme/logo_print.png b/entry_types/scrolled/lib/generators/pageflow_scrolled/install/templates/theme/logo_print.png new file mode 100644 index 0000000000..5f4ed4a6e1 Binary files /dev/null and b/entry_types/scrolled/lib/generators/pageflow_scrolled/install/templates/theme/logo_print.png differ diff --git a/entry_types/scrolled/lib/generators/pageflow_scrolled/install/templates/theme/preview.png b/entry_types/scrolled/lib/generators/pageflow_scrolled/install/templates/theme/preview.png new file mode 100644 index 0000000000..5d44e40605 Binary files /dev/null and b/entry_types/scrolled/lib/generators/pageflow_scrolled/install/templates/theme/preview.png differ diff --git a/entry_types/scrolled/lib/generators/pageflow_scrolled/install/templates/theme/preview_thumbnail.png b/entry_types/scrolled/lib/generators/pageflow_scrolled/install/templates/theme/preview_thumbnail.png new file mode 100644 index 0000000000..5c4c34925b Binary files /dev/null and b/entry_types/scrolled/lib/generators/pageflow_scrolled/install/templates/theme/preview_thumbnail.png differ diff --git a/entry_types/scrolled/lib/pageflow_scrolled.rb b/entry_types/scrolled/lib/pageflow_scrolled.rb index 9142612b8d..08f9cc0216 100644 --- a/entry_types/scrolled/lib/pageflow_scrolled.rb +++ b/entry_types/scrolled/lib/pageflow_scrolled.rb @@ -20,6 +20,7 @@ def entry_type configuration: PageflowScrolled::Configuration, editor_fragment_renderer:, editor_app: PageflowScrolled::Engine, + theme_asset_resolver: PageflowScrolled::ThemeAssetResolver.new, web_app_manifest: PageflowScrolled::WebAppManifest, theme_files: { logo_mobile: LOGO_OPTIONS, diff --git a/entry_types/scrolled/lib/pageflow_scrolled/theme_asset_resolver.rb b/entry_types/scrolled/lib/pageflow_scrolled/theme_asset_resolver.rb new file mode 100644 index 0000000000..c90f90ecbd --- /dev/null +++ b/entry_types/scrolled/lib/pageflow_scrolled/theme_asset_resolver.rb @@ -0,0 +1,33 @@ +module PageflowScrolled + # Resolves theme assets from Webpack with a Sprockets fallback for + # backwards compatibility with existing themes. + class ThemeAssetResolver + def initialize(fallback = Pageflow::SprocketsThemeAssetResolver.new) + @fallback = fallback + end + + def preview_image_url(theme, view_context:) + webpack_asset_url(theme, 'preview.png', view_context:) || + @fallback.preview_image_url(theme, view_context:) + end + + def preview_thumbnail_url(theme, view_context:) + webpack_asset_url(theme, 'preview_thumbnail.png', view_context:) || + @fallback.preview_thumbnail_url(theme, view_context:) + end + + def publisher_logo_url(entry, view_context:) + webpack_asset_url(entry.theme, 'logo_print.png', view_context:) || + @fallback.publisher_logo_url(entry, view_context:) + end + + private + + def webpack_asset_url(theme, path, view_context:) + logical_path = "static/pageflow-scrolled/themes/#{theme.directory_name}/#{path}" + asset_path = view_context.current_shakapacker_instance.manifest.lookup(logical_path) + + view_context.url_to_asset(asset_path) if asset_path + end + end +end diff --git a/entry_types/scrolled/spec/pageflow_scrolled/theme_asset_resolver_spec.rb b/entry_types/scrolled/spec/pageflow_scrolled/theme_asset_resolver_spec.rb new file mode 100644 index 0000000000..74683f98db --- /dev/null +++ b/entry_types/scrolled/spec/pageflow_scrolled/theme_asset_resolver_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +module PageflowScrolled + RSpec.describe ThemeAssetResolver do + let(:fallback) { double } + let(:manifest) { double } + let(:shakapacker) { double(manifest:) } + let(:view_context) { double(current_shakapacker_instance: shakapacker) } + let(:theme) { Pageflow::Theme.new(:custom) } + + describe '#preview_image_url' do + it 'resolves conventional Webpack asset from manifest' do + expect(manifest).to receive(:lookup) + .with('static/pageflow-scrolled/themes/custom/preview.png') + .and_return('/packs/preview-123.png') + expect(view_context).to receive(:url_to_asset) + .with('/packs/preview-123.png') + .and_return('https://example.com/packs/preview-123.png') + expect(fallback).not_to receive(:preview_image_url) + + result = described_class.new(fallback).preview_image_url(theme, view_context:) + + expect(result).to eq('https://example.com/packs/preview-123.png') + end + + it 'falls back to Sprockets when asset is missing from manifest' do + allow(manifest).to receive(:lookup).and_return(nil) + expect(fallback).to receive(:preview_image_url) + .with(theme, view_context:) + .and_return('https://example.com/assets/preview.png') + + result = described_class.new(fallback).preview_image_url(theme, view_context:) + + expect(result).to eq('https://example.com/assets/preview.png') + end + end + + describe '#preview_thumbnail_url' do + it 'resolves conventional Webpack asset from manifest' do + expect(manifest).to receive(:lookup) + .with('static/pageflow-scrolled/themes/custom/preview_thumbnail.png') + .and_return('/packs/preview_thumbnail-123.png') + expect(view_context).to receive(:url_to_asset) + .with('/packs/preview_thumbnail-123.png') + .and_return('https://example.com/packs/preview_thumbnail-123.png') + + result = described_class.new(fallback).preview_thumbnail_url(theme, view_context:) + + expect(result).to eq('https://example.com/packs/preview_thumbnail-123.png') + end + end + + describe '#publisher_logo_url' do + it 'resolves conventional Webpack asset from manifest' do + entry = double(theme:) + expect(manifest).to receive(:lookup) + .with('static/pageflow-scrolled/themes/custom/logo_print.png') + .and_return('/packs/logo_print-123.png') + expect(view_context).to receive(:url_to_asset) + .with('/packs/logo_print-123.png') + .and_return('https://example.com/packs/logo_print-123.png') + + result = described_class.new(fallback).publisher_logo_url(entry, view_context:) + + expect(result).to eq('https://example.com/packs/logo_print-123.png') + end + end + end +end diff --git a/lib/pageflow/entry_type.rb b/lib/pageflow/entry_type.rb index bb4ba81fc9..a35ca5dc28 100644 --- a/lib/pageflow/entry_type.rb +++ b/lib/pageflow/entry_type.rb @@ -5,7 +5,7 @@ module Pageflow class EntryType # @api private attr_reader :name, :frontend_app, :editor_fragment_renderer, :configuration, :editor_app, - :theme_files, :web_app_manifest + :theme_asset_resolver, :theme_files, :web_app_manifest # @param name [String] A unique name. # @@ -22,6 +22,9 @@ class EntryType # interface used by editor Backbone collections. Mounted at # `/editor/entries/:id//` # + # @param theme_asset_resolver [#preview_image_url, #preview_thumbnail_url, + # #publisher_logo_url] Resolves URLs of theme assets used by core. + # # @param theme_files [Hash] A hash of the following form defining # what files can be uploaded when customizing themes of the # entry type and which Paperclip styles shall be processed: @@ -31,12 +34,14 @@ class EntryType # @param web_app_manifest [#call] Receives published entry and # returns JSON for webmanifest file. def initialize(name:, frontend_app:, editor_fragment_renderer:, configuration:, editor_app: nil, + theme_asset_resolver: SprocketsThemeAssetResolver.new, theme_files: {}, web_app_manifest: nil) @name = name @frontend_app = frontend_app @editor_fragment_renderer = editor_fragment_renderer @configuration = configuration @editor_app = editor_app + @theme_asset_resolver = theme_asset_resolver @theme_files = theme_files @web_app_manifest = web_app_manifest end diff --git a/lib/pageflow/sprockets_theme_asset_resolver.rb b/lib/pageflow/sprockets_theme_asset_resolver.rb new file mode 100644 index 0000000000..2b8e648e57 --- /dev/null +++ b/lib/pageflow/sprockets_theme_asset_resolver.rb @@ -0,0 +1,19 @@ +module Pageflow + # Resolves conventional theme assets from the Sprockets asset pipeline. + # + # Used as the default for backwards compatibility with entry types and + # themes created before entry types could provide their own resolver. + class SprocketsThemeAssetResolver + def preview_image_url(theme, view_context:) + view_context.image_url(theme.preview_image_path) + end + + def preview_thumbnail_url(theme, view_context:) + view_context.image_url(theme.preview_thumbnail_path) + end + + def publisher_logo_url(entry, view_context:) + view_context.asset_url(entry.theme.print_logo_path) + end + end +end diff --git a/spec/helpers/pageflow/structured_data_helper_spec.rb b/spec/helpers/pageflow/structured_data_helper_spec.rb index 4dec3b9f3c..d90513f732 100644 --- a/spec/helpers/pageflow/structured_data_helper_spec.rb +++ b/spec/helpers/pageflow/structured_data_helper_spec.rb @@ -61,6 +61,27 @@ module Pageflow 'thumbnailUrl' => image_file.thumbnail_url(:thumbnail_large)) end + it 'uses entry type theme asset resolver for publisher logo' do + entry = create(:published_entry, + revision_attributes: {publisher: 'Some publisher'}) + resolver = double + allow(entry.entry_type).to receive(:theme_asset_resolver).and_return(resolver) + allow(resolver).to receive(:publisher_logo_url) + .with(entry, view_context: anything) + .and_return('https://example.com/publisher.png') + + html = helper.structured_data_for_entry(entry) + + expect(html).to have_json_ld( + 'publisher' => a_hash_including( + 'logo' => { + '@type' => 'ImageObject', + 'url' => 'https://example.com/publisher.png' + } + ) + ) + end + it 'skips metadata if not present' do pageflow_configure do |config| config.default_keywords_meta_tag = '' diff --git a/spec/helpers/pageflow/themes_helper_spec.rb b/spec/helpers/pageflow/themes_helper_spec.rb index fb96d29ef1..7ca5db0b07 100644 --- a/spec/helpers/pageflow/themes_helper_spec.rb +++ b/spec/helpers/pageflow/themes_helper_spec.rb @@ -31,6 +31,23 @@ module Pageflow expect(result.dig('options', 'some')).to eq('value') end + + it 'uses supplied theme asset resolver' do + Pageflow.config.themes.register(:test_theme) + resolver = double( + preview_image_url: 'https://example.com/preview.png', + preview_thumbnail_url: 'https://example.com/preview_thumbnail.png' + ) + + result = JSON.parse( + helper.theme_json_seeds(Pageflow.config, theme_asset_resolver: resolver) + ).last + + expect(result).to include( + 'preview_image_url' => 'https://example.com/preview.png', + 'preview_thumbnail_url' => 'https://example.com/preview_thumbnail.png' + ) + end end end end diff --git a/spec/pageflow/sprockets_theme_asset_resolver_spec.rb b/spec/pageflow/sprockets_theme_asset_resolver_spec.rb new file mode 100644 index 0000000000..485430474b --- /dev/null +++ b/spec/pageflow/sprockets_theme_asset_resolver_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +module Pageflow + RSpec.describe SprocketsThemeAssetResolver do + describe '#preview_image_url' do + it 'resolves conventional preview image path' do + theme = Theme.new(:custom) + view_context = double + + expect(view_context).to receive(:image_url) + .with('pageflow/themes/custom/preview.png') + .and_return('https://example.com/preview.png') + + result = described_class.new.preview_image_url(theme, view_context:) + + expect(result).to eq('https://example.com/preview.png') + end + end + + describe '#preview_thumbnail_url' do + it 'resolves conventional preview thumbnail path' do + theme = Theme.new(:custom) + view_context = double + + expect(view_context).to receive(:image_url) + .with('pageflow/themes/custom/preview_thumbnail.png') + .and_return('https://example.com/preview_thumbnail.png') + + result = described_class.new.preview_thumbnail_url(theme, view_context:) + + expect(result).to eq('https://example.com/preview_thumbnail.png') + end + end + + describe '#publisher_logo_url' do + it 'resolves conventional print logo path' do + entry = double(theme: Theme.new(:custom)) + view_context = double + + expect(view_context).to receive(:asset_url) + .with('pageflow/themes/custom/logo_print.png') + .and_return('https://example.com/logo_print.png') + + result = described_class.new.publisher_logo_url(entry, view_context:) + + expect(result).to eq('https://example.com/logo_print.png') + end + end + end +end