diff --git a/lib/perron/html_processor.rb b/lib/perron/html_processor.rb index b35c784..1c4efb0 100644 --- a/lib/perron/html_processor.rb +++ b/lib/perron/html_processor.rb @@ -2,6 +2,7 @@ require "perron/html_processor/target_blank" require "perron/html_processor/lazy_load_images" +require "perron/html_processor/absolute_urls" module Perron class HtmlProcessor @@ -21,7 +22,8 @@ def process BUILT_IN = { "target_blank" => Perron::HtmlProcessor::TargetBlank, - "lazy_load_images" => Perron::HtmlProcessor::LazyLoadImages + "lazy_load_images" => Perron::HtmlProcessor::LazyLoadImages, + "absolute_urls" => Perron::HtmlProcessor::AbsoluteUrls }.tap do |processors| require "rouge" require "perron/html_processor/syntax_highlight" diff --git a/lib/perron/html_processor/absolute_urls.rb b/lib/perron/html_processor/absolute_urls.rb new file mode 100644 index 0000000..6ce4c0f --- /dev/null +++ b/lib/perron/html_processor/absolute_urls.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Perron + class HtmlProcessor + class AbsoluteUrls < HtmlProcessor::Base + def process + @html.css("img").each do |image| + src = image["src"] + + next if src.blank? || absolute_url?(src) + + image["src"] = base_url + src + end + end + + private + + def absolute_url?(src) + src.start_with?("http://", "https://", "//") + end + + def base_url + Perron.configuration.url.delete_suffix("/") + end + end + end +end diff --git a/lib/perron/resource/metadata.rb b/lib/perron/resource/metadata.rb index b501b99..1723476 100644 --- a/lib/perron/resource/metadata.rb +++ b/lib/perron/resource/metadata.rb @@ -25,6 +25,8 @@ def apply_fallbacks_and_defaults(to:) to[:canonical_url] ||= canonical_url + to[:image] = absolute_url(to[:image]) if to[:image] + to[:og_image] ||= to[:image] to[:twitter_image] ||= to[:og_image] @@ -52,13 +54,20 @@ def canonical_url begin Rails.application.routes.url_helpers.polymorphic_url( @resource, - **Perron.configuration.default_url_options + **Perron.configuration.default_url_options ) rescue false end end + def absolute_url(path) + return path if path.blank? + return path if path.start_with?("http://", "https://", "//") + + Perron.configuration.url.delete_suffix("/") + path + end + def site_data @config.metadata.except(:title_separator, :title_suffix).deep_symbolize_keys || {} end diff --git a/lib/perron/site/builder/feeds/atom.erb b/lib/perron/site/builder/feeds/atom.erb index 66e7c65..a54332b 100644 --- a/lib/perron/site/builder/feeds/atom.erb +++ b/lib/perron/site/builder/feeds/atom.erb @@ -31,9 +31,9 @@ <% base_url = url_for_resource(resource) %> <% if base_url %> - ]]> + ]]> <% else %> - ]]> + ]]> <% end %> <% resource.metadata.tags&.each do |tag| %> diff --git a/lib/perron/site/builder/feeds/json.erb b/lib/perron/site/builder/feeds/json.erb index be8dac8..142fb38 100644 --- a/lib/perron/site/builder/feeds/json.erb +++ b/lib/perron/site/builder/feeds/json.erb @@ -13,7 +13,7 @@ date_published: resource.published_at&.iso8601, title: resource.metadata.title, authors: (item_author && item_author.name ? [{ name: item_author.name, email: item_author.email, url: item_author.url, avatar: item_author.avatar }.compact] : nil), - content_html: Perron::Markdown.render(resource.content) + content_html: Perron::Markdown.render(resource.content, processors: ["absolute_urls"]) }.compact } }.to_json %> diff --git a/lib/perron/site/builder/feeds/rss.erb b/lib/perron/site/builder/feeds/rss.erb index 1cdec1f..a8d1548 100644 --- a/lib/perron/site/builder/feeds/rss.erb +++ b/lib/perron/site/builder/feeds/rss.erb @@ -21,7 +21,7 @@ <% end %> <%= resource.metadata.title %> - ]]> + ]]> <% end %> diff --git a/test/perron/html_processor/absolute_urls_test.rb b/test/perron/html_processor/absolute_urls_test.rb new file mode 100644 index 0000000..3d8b4ef --- /dev/null +++ b/test/perron/html_processor/absolute_urls_test.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'test_helper' + +class Perron::HtmlProcessor::AbsoluteUrlsTest < ActionView::TestCase + def process_html(html) + document = Nokogiri::HTML::DocumentFragment.parse(html) + + Perron::HtmlProcessor::AbsoluteUrls.new(document).process + + document.to_html + end + + test 'converts relative image src to absolute URL' do + html = 'A photo' + processed = process_html(html) + + assert_dom_equal 'A photo', processed + end + + test 'does not modify already absolute http URL' do + html = 'A photo' + processed = process_html(html) + + assert_dom_equal html, processed + end + + test 'does not modify already absolute https URL' do + html = 'A photo' + processed = process_html(html) + + assert_dom_equal html, processed + end + + test 'does not modify protocol-relative URL' do + html = 'A photo' + processed = process_html(html) + + assert_dom_equal html, processed + end + + test 'does not modify image without src attribute' do + html = 'A photo' + processed = process_html(html) + + assert_dom_equal html, processed + end + + test 'processes multiple images' do + html = '' + processed = process_html(html) + + assert_dom_equal '', + processed + end + + test 'does not affect content without images' do + html = '

Some text, but no images.

' + processed = process_html(html) + + assert_dom_equal html, processed + end +end diff --git a/test/perron/resource/metadata_test.rb b/test/perron/resource/metadata_test.rb index 9f41579..a4093e7 100644 --- a/test/perron/resource/metadata_test.rb +++ b/test/perron/resource/metadata_test.rb @@ -1,80 +1,80 @@ -require "test_helper" +require 'test_helper' class Perron::Resource::MetadataTest < ActiveSupport::TestCase include ConfigurationHelper def setup - @post = Content::Post.new("test/dummy/app/content/posts/2023-05-15-sample-post.md") + @post = Content::Post.new('test/dummy/app/content/posts/2023-05-15-sample-post.md') @post_frontmatter = Perron::Resource::Separator.new(@post.raw_content).frontmatter - @posts_collection = Perron::Site.collection("posts") + @posts_collection = Perron::Site.collection('posts') - @custom_page = Content::Page.new("test/dummy/app/content/pages/custom.md") + @custom_page = Content::Page.new('test/dummy/app/content/pages/custom.md') @custom_page_frontmatter = Perron::Resource::Separator.new(@custom_page.raw_content).frontmatter - @pages_collection = Perron::Site.collection("pages") + @pages_collection = Perron::Site.collection('pages') - @root_page = Content::Page.new("test/dummy/app/content/pages/root.erb") + @root_page = Content::Page.new('test/dummy/app/content/pages/root.erb') @root_page_frontmatter = Perron::Resource::Separator.new(@root_page.raw_content).frontmatter - @about_page = Content::Page.new("test/dummy/app/content/pages/about.md") + @about_page = Content::Page.new('test/dummy/app/content/pages/about.md') @about_page_frontmatter = Perron::Resource::Separator.new(@about_page.raw_content).frontmatter end - test "generates basic metadata with fallbacks for a standard blog post" do + test 'generates basic metadata with fallbacks for a standard blog post' do metadata = Perron::Resource::Metadata.new( resource: @post, frontmatter: @post_frontmatter, collection: @posts_collection ).data - assert_equal "Sample Post", metadata.title - assert_equal "Describing sample post", metadata.description + assert_equal 'Sample Post', metadata.title + assert_equal 'Describing sample post', metadata.description - assert_equal "http://localhost:3000/blog/sample-post/", metadata.og_url + assert_equal 'http://localhost:3000/blog/sample-post/', metadata.og_url assert_equal Date.new(2023, 5, 15).to_datetime, metadata.article_published_time - assert_equal "Dummy App", metadata.og_site_name + assert_equal 'Dummy App', metadata.og_site_name - assert_equal "summary_large_image", metadata.twitter_card + assert_equal 'summary_large_image', metadata.twitter_card - assert_equal "Sample Post", metadata.og_title - assert_equal "Sample Post", metadata.twitter_title - assert_equal "Describing sample post", metadata.og_description - assert_equal "Describing sample post", metadata.twitter_description + assert_equal 'Sample Post', metadata.og_title + assert_equal 'Sample Post', metadata.twitter_title + assert_equal 'Describing sample post', metadata.og_description + assert_equal 'Describing sample post', metadata.twitter_description end - test "frontmatter values take precedence over all defaults and fallbacks" do + test 'frontmatter values take precedence over all defaults and fallbacks' do metadata = Perron::Resource::Metadata.new( resource: @custom_page, frontmatter: @custom_page_frontmatter, collection: @pages_collection ).data - assert_equal "Custom OG Title For Sharing", metadata.title - assert_equal "Custom OG Title For Sharing", metadata.og_title, "og_title should be from frontmatter, not a fallback" - assert_equal "summary", metadata.twitter_card, "twitter_card should be from frontmatter, not the default" + assert_equal 'Custom OG Title For Sharing', metadata.title + assert_equal 'Custom OG Title For Sharing', metadata.og_title, 'og_title should be from frontmatter, not a fallback' + assert_equal 'summary', metadata.twitter_card, 'twitter_card should be from frontmatter, not the default' - assert_equal "/image.jpg", metadata.image - assert_equal "/og-image.jpg", metadata.og_image, "og_image should be from frontmatter, not a fallback from image" - assert_equal "/og-image.jpg", metadata.twitter_image, "twitter_image should fall back to the specific og_image" + assert_equal 'http://localhost:3000/image.jpg', metadata.image + assert_equal '/og-image.jpg', metadata.og_image, 'og_image should be from frontmatter, not a fallback from image' + assert_equal '/og-image.jpg', metadata.twitter_image, 'twitter_image should fall back to the specific og_image' end - test "inherits metadata from collection configuration" do - collection = Perron::Site.collection("posts") + test 'inherits metadata from collection configuration' do + collection = Perron::Site.collection('posts') metadata = Perron::Resource::Metadata.new( resource: @post, frontmatter: @post_frontmatter, collection: collection ).data - assert_equal "The Post Collection Team", metadata.author - assert_equal "The Post Collection Team", metadata.og_author, "og_author should fall back to collection author" - assert_equal "article", metadata.type - assert_equal "article", metadata.og_type, "og_type should fall back to collection type" + assert_equal 'The Post Collection Team', metadata.author + assert_equal 'The Post Collection Team', metadata.og_author, 'og_author should fall back to collection author' + assert_equal 'article', metadata.type + assert_equal 'article', metadata.og_type, 'og_type should fall back to collection type' end - test "inherits and merges metadata from site configuration" do + test 'inherits and merges metadata from site configuration' do Perron.configure do |config| - config.metadata = { author: "The Dummy App Team", locale: "en_GB", description: "Site-wide description" } + config.metadata = { author: 'The Dummy App Team', locale: 'en_GB', description: 'Site-wide description' } end metadata = Perron::Resource::Metadata.new( @@ -83,68 +83,68 @@ def setup collection: @pages_collection ).data - assert_equal "The Dummy App Team", metadata.author - assert_equal "en_GB", metadata.locale - assert_equal "en_GB", metadata.og_locale + assert_equal 'The Dummy App Team', metadata.author + assert_equal 'en_GB', metadata.locale + assert_equal 'en_GB', metadata.og_locale - assert_equal "This is the about page.", metadata.description - assert_equal "This is the about page.", metadata.og_description + assert_equal 'This is the about page.', metadata.description + assert_equal 'This is the about page.', metadata.og_description end - test "metadata precedence is frontmatter > collection > site" do - resource = Content::Post.new("test/dummy/app/content/posts/2023-06-15-another-post.md") + test 'metadata precedence is frontmatter > collection > site' do + resource = Content::Post.new('test/dummy/app/content/posts/2023-06-15-another-post.md') frontmatter = Perron::Resource::Separator.new(resource.raw_content).frontmatter Perron.configure do |config| - config.metadata.author = "Site Author" + config.metadata.author = 'Site Author' end - collection = Perron::Site.collection("posts") + collection = Perron::Site.collection('posts') metadata = Perron::Resource::Metadata.new(resource: resource, frontmatter: frontmatter, collection: collection).data - assert_equal "Kendall", metadata.author, "Frontmatter author should take highest precedence" + assert_equal 'Kendall', metadata.author, 'Frontmatter author should take highest precedence' end - test "removes nil values from final data after processing" do + test 'removes nil values from final data after processing' do metadata = Perron::Resource::Metadata.new( resource: @about_page, frontmatter: @about_page_frontmatter, collection: @pages_collection ).data - assert_not metadata.key?(:image), "key :image should be removed" - assert_not metadata.key?(:og_image), "key :og_image should be removed" - assert_not metadata.key?(:twitter_image), "key :twitter_image should be removed" - assert_not metadata.key?(:author), "key :author should be removed" + assert_not metadata.key?(:image), 'key :image should be removed' + assert_not metadata.key?(:og_image), 'key :og_image should be removed' + assert_not metadata.key?(:twitter_image), 'key :twitter_image should be removed' + assert_not metadata.key?(:author), 'key :author should be removed' end - test "generates canonical url for root" do + test 'generates canonical url for root' do metadata = Perron::Resource::Metadata.new( resource: @root_page, frontmatter: @root_page_frontmatter, collection: @pages_collection ).data - assert_equal "http://localhost:3000/", metadata.og_url + assert_equal 'http://localhost:3000/', metadata.og_url end - test "generates canonical url with trailing slash when configured" do + test 'generates canonical url with trailing slash when configured' do metadata = Perron::Resource::Metadata.new( resource: @about_page, frontmatter: @about_page_frontmatter, collection: @pages_collection ).data - assert_equal "http://localhost:3000/about/", metadata.og_url + assert_equal 'http://localhost:3000/about/', metadata.og_url end - test "title falls back to site name if not present anywhere" do + test 'title falls back to site name if not present anywhere' do metadata = Perron::Resource::Metadata.new( resource: @about_page, frontmatter: {}, collection: @pages_collection ).data - assert_equal "Dummy App", metadata.title, "Title should fall back to the configured site_name" + assert_equal 'Dummy App', metadata.title, 'Title should fall back to the configured site_name' end end