diff --git a/docs/_docs/pages.md b/docs/_docs/pages.md index d707d0f44da..6b29267fc69 100644 --- a/docs/_docs/pages.md +++ b/docs/_docs/pages.md @@ -34,3 +34,38 @@ If you have a lot of pages, you can organize them into subfolders. The same subf ## Changing the output URL You might want to have a particular folder structure for your source files that changes for the built site. With [permalinks](/docs/permalinks) you have full control of the output URL. + +## Liquid Representation + +From Jekyll 4.1 onwards, there is a minor change in how instances of `Jekyll::Page` are exposed to layouts and other Liquid +templates. `Jekyll::Page` instances now use a `Liquid::Drop` instead of a `Hash`. This change results in greater performance +for a site with numerous *standlone pages not within a collection*. + +### For plugin developers + +While end-users do not need to take any extra action due to this change, plugin authors depending on the existing behavior *may* +need to make minor changes to their plugins. + +If a `Jekyll::Page` subclass' `to_liquid` method calls `super`, it will have to be slightly modified. +```ruby +class Foo::BarPage < Jekyll::Page + def to_liquid(*) + payload = super # This needs to be changed to `super.to_h` + # to obtain a Hash as in v4.0.0. + + do_something(payload) # Logic specific to `Foo::BarPage` objects + end +end +``` + +`Jekyll::Page` subclasses won't inherit the optimization automatically until the next major version. However, plugin authors +can opt-in to the optimization in their subclasses by wrapping the temporary `liquid_drop` method if the subclass doesn't +override the superclass method: +```ruby +class Foo::BarPage < Jekyll::Page + def to_liquid(*) + liquid_drop # Returns an instance of `Jekyll::Drops::PageDrop`. + # Will be removed in Jekyll 5.0. + end +end +``` diff --git a/lib/jekyll/drops/page_drop.rb b/lib/jekyll/drops/page_drop.rb new file mode 100644 index 00000000000..3a6e961367b --- /dev/null +++ b/lib/jekyll/drops/page_drop.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Jekyll + module Drops + class PageDrop < Drop + extend Forwardable + + mutable false + + def_delegators :@obj, :content, :dir, :name, :path, :url + private def_delegator :@obj, :data, :fallback_data + end + end +end diff --git a/lib/jekyll/page.rb b/lib/jekyll/page.rb index f353252d825..2e346fc5f25 100644 --- a/lib/jekyll/page.rb +++ b/lib/jekyll/page.rb @@ -70,6 +70,38 @@ def dir end end + # For backwards-compatibility in subclasses that do not redefine + # the `:to_liquid` method, stash existing definition under a new name + # + # TODO: Remove in Jekyll 5.0 + alias_method :legacy_to_liquid, :to_liquid + private :legacy_to_liquid + + # Private + # Subclasses can choose to optimize their `:to_liquid` method by wrapping + # it around this definition. + # + # TODO: Remove in Jekyll 5.0 + def liquid_drop + @liquid_drop ||= begin + defaults = site.frontmatter_defaults.all(relative_path, type) + unless defaults.empty? + Utils.deep_merge_hashes!(data, Utils.deep_merge_hashes!(defaults, data)) + end + Drops::PageDrop.new(self) + end + end + private :liquid_drop + + # Public + # + # Liquid representation of current page + # + # TODO: Remove optional parameter in Jekyll 5.0 + def to_liquid(attrs = nil) + self.class == Jekyll::Page ? liquid_drop : legacy_to_liquid(attrs) + end + # The full path and filename of the post. Defined in the YAML of the post # body. # diff --git a/test/test_page.rb b/test/test_page.rb index 16aa3ff90f3..023560c90e6 100644 --- a/test/test_page.rb +++ b/test/test_page.rb @@ -50,6 +50,42 @@ def do_render(page) assert_equal "/+/%25%23%20+.html", @page.url end + should "be exposed to Liquid as a Liquid::Drop subclass" do + page = setup_page("properties.html") + liquid_rep = page.to_liquid + refute_equal Hash, liquid_rep.class + assert_equal true, liquid_rep.is_a?(Liquid::Drop) + assert_equal Jekyll::Drops::PageDrop, liquid_rep.class + end + + should "make attributes accessible for use in Liquid templates" do + page = setup_page("/contacts", "index.html") + template = Liquid::Template.parse(<<~TEXT) + Name: {{ page.name }} + Path: {{ page.path }} + URL: {{ page.url }} + TEXT + expected = <<~TEXT + Name: index.html + Path: contacts/index.html + URL: /contacts/ + TEXT + assert_equal(expected, template.render!("page" => page.to_liquid)) + end + + should "make front matter data accessible for use in Liquid templates" do + page = setup_page("properties.html") + template = Liquid::Template.parse(<<~TEXT) + TITLE: {{ page.title }} + FOO: {{ page.foo }} + TEXT + expected = <<~TEXT + TITLE: Properties Page + FOO: bar + TEXT + assert_equal expected, template.render!("page" => page.to_liquid) + end + context "in a directory hierarchy" do should "create URL based on filename" do @page = setup_page("/contacts", "bar.html") diff --git a/test/test_page_without_a_file.rb b/test/test_page_without_a_file.rb index f06706962c2..6deca7b537c 100644 --- a/test/test_page_without_a_file.rb +++ b/test/test_page_without_a_file.rb @@ -76,6 +76,12 @@ def render_and_write end end end + + should "be exposed to Liquid as a Hash" do + liquid_rep = @page.to_liquid + refute_equal Jekyll::Drops::PageDrop, liquid_rep.class + assert_equal Hash, liquid_rep.class + end end context "with site-wide permalink configuration" do