perron 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +11 -15
  3. data/app/controllers/perron/concierge_controller.rb +4 -1
  4. data/app/helpers/perron/markdown_helper.rb +3 -2
  5. data/app/helpers/perron/meta_tags_helper.rb +3 -9
  6. data/app/helpers/perron/paginate_helper.rb +40 -0
  7. data/app/views/perron/concierge/show.html.erb +1 -1
  8. data/lib/perron/collection.rb +2 -1
  9. data/lib/perron/configuration.rb +29 -4
  10. data/lib/perron/data_source.rb +6 -4
  11. data/lib/perron/deprecator.rb +1 -1
  12. data/lib/perron/engine.rb +11 -8
  13. data/lib/perron/feeds/split.rb +46 -0
  14. data/lib/perron/feeds.rb +17 -0
  15. data/lib/perron/html_processor/absolute_urls.rb +1 -1
  16. data/lib/perron/install/deploy.yml.erb +14 -0
  17. data/lib/perron/paginate.rb +58 -0
  18. data/lib/perron/relation.rb +27 -7
  19. data/lib/perron/resource/class_methods.rb +6 -0
  20. data/lib/perron/resource/configuration.rb +16 -3
  21. data/lib/perron/resource/metadata.rb +11 -4
  22. data/lib/perron/resource/publishable.rb +9 -1
  23. data/lib/perron/resource/separator.rb +11 -5
  24. data/lib/perron/resource/sourceable.rb +63 -9
  25. data/lib/perron/resource/updatable.rb +46 -0
  26. data/lib/perron/resource.rb +7 -0
  27. data/lib/perron/site/builder/feeds/atom.erb +1 -1
  28. data/lib/perron/site/builder/feeds/atom.rb +8 -2
  29. data/lib/perron/site/builder/feeds/json.rb +8 -2
  30. data/lib/perron/site/builder/feeds/rss.rb +8 -2
  31. data/lib/perron/site/builder/feeds.rb +32 -10
  32. data/lib/perron/site/builder/page.rb +19 -4
  33. data/lib/perron/site/builder/paths.rb +23 -1
  34. data/lib/perron/site/builder/sitemap.rb +23 -10
  35. data/lib/perron/site/builder.rb +24 -0
  36. data/lib/perron/site/validate.rb +18 -5
  37. data/lib/perron/tasks/deploy.rake +69 -0
  38. data/lib/perron/version.rb +1 -1
  39. data/lib/perron.rb +1 -0
  40. data/perron.gemspec +1 -1
  41. metadata +10 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 72473e733aa6ec4313dc71e66b2a949dea6497b427e304cb4656be0d0a5fbc86
4
- data.tar.gz: 37a57b14891cf3fef0dfb1a4c98b414852fdd1bd3f3e377c674e15673f6d2547
3
+ metadata.gz: 84ad5823f9409ec7734c3fef06a9d57602e5da59c14e939d2e4acb8f3f2b4e22
4
+ data.tar.gz: 9e8e47f8706704ec8d8d6efb66ccbb6eb87f815570166f0bd188ce0e6f085b9a
5
5
  SHA512:
6
- metadata.gz: 478b181f494c2c0ac54e996c0d9f289c7694bedbcd203fd435c9222ecdd07ad6c5a3f5e8baeb497ee2f77499e3580f0f915439d2feba1edf8ab42385285a7e96
7
- data.tar.gz: 19750396f542ac909322aa5cd3ed247dc10a03509ab521bdb75dd0ae705c502e80b3431737d5c34eb42bcacda17c0ef0c35889cb65561339646f6eb7a2ee1e58
6
+ metadata.gz: 766753cd5c6e74d3353ad603015e316f7158a1a79c28ed8b0fbcbd8d9a7c8c0a1a4cb88e372fbe741548a83e0d0a042da44df8c8545cc6973eb768283177dae8
7
+ data.tar.gz: 605182a711103ad0e0ca07c330290ee2ab1574fcfda9596e042ea7365a4f329485a407692ce4c5e94d5c720e1100bb1c79ef4300ccb04eb4119f989af895e2a5
data/Gemfile.lock CHANGED
@@ -1,10 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- perron (1.0.0)
4
+ perron (1.2.0)
5
5
  csv
6
6
  json
7
- mata (~> 0.8.0)
7
+ mata (~> 0.10.0)
8
8
  psych
9
9
  rails (>= 7.2.0)
10
10
 
@@ -136,11 +136,10 @@ GEM
136
136
  net-pop
137
137
  net-smtp
138
138
  marcel (1.0.4)
139
- mata (0.8.0)
139
+ mata (0.10.0)
140
140
  listen (~> 3.0)
141
141
  rack (>= 3.0)
142
142
  mini_mime (1.1.5)
143
- mini_portile2 (2.8.9)
144
143
  minitest (5.27.0)
145
144
  net-imap (0.5.9)
146
145
  date
@@ -152,24 +151,21 @@ GEM
152
151
  net-smtp (0.5.1)
153
152
  net-protocol
154
153
  nio4r (2.7.4)
155
- nokogiri (1.18.8)
156
- mini_portile2 (~> 2.8.2)
154
+ nokogiri (1.19.3-aarch64-linux-gnu)
157
155
  racc (~> 1.4)
158
- nokogiri (1.18.8-aarch64-linux-gnu)
156
+ nokogiri (1.19.3-aarch64-linux-musl)
159
157
  racc (~> 1.4)
160
- nokogiri (1.18.8-aarch64-linux-musl)
158
+ nokogiri (1.19.3-arm-linux-gnu)
161
159
  racc (~> 1.4)
162
- nokogiri (1.18.8-arm-linux-gnu)
160
+ nokogiri (1.19.3-arm-linux-musl)
163
161
  racc (~> 1.4)
164
- nokogiri (1.18.8-arm-linux-musl)
162
+ nokogiri (1.19.3-arm64-darwin)
165
163
  racc (~> 1.4)
166
- nokogiri (1.18.8-arm64-darwin)
164
+ nokogiri (1.19.3-x86_64-darwin)
167
165
  racc (~> 1.4)
168
- nokogiri (1.18.8-x86_64-darwin)
166
+ nokogiri (1.19.3-x86_64-linux-gnu)
169
167
  racc (~> 1.4)
170
- nokogiri (1.18.8-x86_64-linux-gnu)
171
- racc (~> 1.4)
172
- nokogiri (1.18.8-x86_64-linux-musl)
168
+ nokogiri (1.19.3-x86_64-linux-musl)
173
169
  racc (~> 1.4)
174
170
  parallel (1.27.0)
175
171
  parser (3.3.8.0)
@@ -5,8 +5,11 @@ module Perron
5
5
  end
6
6
 
7
7
  def run_command
8
- system(params[:command])
8
+ command = params[:command]
9
9
 
10
+ return redirect_back fallback_location: root_path unless command.start_with?("bin/rails generate content")
11
+
12
+ system(command)
10
13
  redirect_back fallback_location: root_path
11
14
  end
12
15
  end
@@ -4,10 +4,11 @@ require "perron/markdown"
4
4
 
5
5
  module Perron
6
6
  module MarkdownHelper
7
- def markdownify(content = nil, process: [], resource: nil, &block)
7
+ def markdownify(content = nil, process: nil, resource: nil, &block)
8
8
  text = block_given? ? capture(&block).strip_heredoc : content
9
+ processors = (process.nil? || process.empty?) ? Perron.configuration.default_processors : process
9
10
 
10
- Perron::Markdown.render(text, processors: process, resource: resource || @resource)
11
+ Perron::Markdown.render(text, processors: processors, resource: resource || @resource)
11
12
  end
12
13
  end
13
14
  end
@@ -2,16 +2,10 @@
2
2
 
3
3
  module Perron
4
4
  module MetaTagsHelper
5
- def meta_tags(options = {}) = Perron::Metatags.new(resource.metadata).render(options)
5
+ def meta_tags(options = {})
6
+ metadata = (@metadata || {}).merge(@resource&.metadata || {})
6
7
 
7
- private
8
-
9
- Resource = Data.define(:path, :collection, :metadata, :published_at)
10
-
11
- def resource
12
- return Resource.new(request.path, nil, @metadata, nil) if @metadata.present?
13
-
14
- @resource || Resource.new(request.path, nil, {}, nil)
8
+ Perron::Metatags.new(metadata).render(options)
15
9
  end
16
10
  end
17
11
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perron
4
+ module PaginateHelper
5
+ def paginate(scope, page: nil, **options)
6
+ page ||= (params[:page] || 1).to_i
7
+ resource_class = scope.model_class
8
+
9
+ config = resource_class.configuration.pagination
10
+ per_page = options[:per_page] || config.per_page
11
+ page_path_template = options[:path_template] || config.path_template
12
+
13
+ route = find_index_route(resource_class)
14
+ base_path = route_path(route)
15
+
16
+ use_query_params = Rails.env.development? || Rails.env.local?
17
+ paginate = Paginate.new(scope, page: page, per_page: per_page, base_path: base_path, page_path_template: page_path_template, use_query_params: use_query_params)
18
+
19
+ [paginate, paginate.items]
20
+ end
21
+
22
+ private
23
+
24
+ def find_index_route(resource_class)
25
+ controller_name = resource_class.name.demodulize.sub("Controller", "").underscore.pluralize
26
+
27
+ Rails.application.routes.routes.find do |r|
28
+ r.defaults[:controller] == "content/#{controller_name}" &&
29
+ r.defaults[:action] == "index"
30
+ end
31
+ end
32
+
33
+ def route_path(route)
34
+ return "/#{route.name}/" unless route
35
+
36
+ path_spec = route.path.spec.to_s
37
+ path_spec.sub(/\(.*?\)/, "").gsub(/:[^\/]+/, "").sub(/\/$/, "") + "/"
38
+ end
39
+ end
40
+ end
@@ -211,7 +211,7 @@
211
211
  <code>bin/rails generate content Post --new "My first post"</code>
212
212
 
213
213
  <%= form_with url: perron_run_command_path do |form| %>
214
- <%= form.hidden_field :command, value: 'bin/rails generate content Post --new "My first post"' %>
214
+ <%= form.hidden_field :command, value: 'bin/rails generate content Post --new' %>
215
215
 
216
216
  <% if File.file?("app/content/posts/untitled.md") %>
217
217
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm45.66,85.66-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35a8,8,0,0,1,11.32,11.32Z"/></svg>
@@ -16,7 +16,7 @@ module Perron
16
16
  end
17
17
 
18
18
  def all(resource_class = "Content::#{name.classify}".safe_constantize)
19
- Perron::Relation.new(load_resources(resource_class).select(&:published?))
19
+ Perron::Relation.new(load_resources(resource_class).select(&:published?), resource_class)
20
20
  end
21
21
  alias_method :resources, :all
22
22
 
@@ -52,6 +52,7 @@ module Perron
52
52
 
53
53
  Dir.glob("#{@collection_path}/**/*.*")
54
54
  .select { allowed_extensions.include?(File.extname(it)) }
55
+ .reject { File.basename(it, ".*").downcase == "readme" }
55
56
  .map { resource_class.new(it) }
56
57
  end
57
58
  end
@@ -15,8 +15,6 @@ module Perron
15
15
 
16
16
  @config.output = "output"
17
17
 
18
- @config.output_server_strict = true
19
-
20
18
  @config.mode = :standalone
21
19
 
22
20
  @config.live_reload = false
@@ -37,6 +35,8 @@ module Perron
37
35
 
38
36
  @config.markdown_options = {}
39
37
 
38
+ @config.default_processors = []
39
+
40
40
  @config.search_scope = []
41
41
 
42
42
  @config.cache_data_sources = false
@@ -45,12 +45,17 @@ module Perron
45
45
  @config.sitemap.enabled = false
46
46
  @config.sitemap.priority = 0.5
47
47
  @config.sitemap.change_frequency = :monthly
48
+ @config.sitemap.emit_priority = false
49
+ @config.sitemap.emit_changefreq = false
48
50
 
49
51
  @config.site_name = nil
50
52
  @config.site_description = nil
51
53
 
52
54
  @config.metadata = ActiveSupport::OrderedOptions.new
53
55
  @config.metadata.title_separator = " — "
56
+
57
+ @config.before_build = nil
58
+ @config.after_build = nil
54
59
  end
55
60
 
56
61
  def input = Rails.root.join("app", "content")
@@ -61,10 +66,30 @@ module Perron
61
66
 
62
67
  def mode = @config.mode.to_s.inquiry
63
68
 
69
+ def output_server_strict = @config.fetch(:output_server_strict) { mode.standalone? }
70
+
64
71
  def additional_routes
65
72
  @additional_routes || (mode.integrated? ? [] : %w[root_path])
66
73
  end
67
74
 
75
+ def deploy
76
+ Perron.deprecator.deprecation_warning(:deploy, "Perron.configuration.deploy is deprecated and its behavior will change in a future 1.x release (target: 1.3.0).")
77
+
78
+ @deploy ||= ActiveSupport::OrderedOptions.new.tap do |config|
79
+ def config.method_missing(method_name, *args, &block)
80
+ if method_name.to_s.end_with?("=")
81
+ super
82
+ else
83
+ self[method_name] ||= ActiveSupport::OrderedOptions.new
84
+ end
85
+ end
86
+
87
+ def config.respond_to_missing?(method_name, include_private = false)
88
+ !method_name.to_s.end_with?("=") || super
89
+ end
90
+ end
91
+ end
92
+
68
93
  attr_writer :additional_routes
69
94
 
70
95
  def url
@@ -82,8 +107,8 @@ module Perron
82
107
  end
83
108
  end
84
109
 
85
- def respond_to_missing?(method_name)
86
- @config.respond_to?(method_name) || super
110
+ def respond_to_missing?(method_name, ...)
111
+ @config.respond_to?(method_name, ...) || super
87
112
  end
88
113
  end
89
114
  end
@@ -66,11 +66,13 @@ module Perron
66
66
  end
67
67
 
68
68
  data.map.with_index do |item, index|
69
- unless item.is_a?(Hash)
70
- raise Errors::DataParseError, "Item at index #{index} in `#{@file_path}` must be a hash/object, got #{item.class}"
69
+ if item.is_a?(Hash)
70
+ Item.new(item, identifier: @identifier)
71
+ elsif item.is_a?(String)
72
+ item
73
+ else
74
+ raise Errors::DataParseError, "Item at index #{index} in `#{@file_path}` must be a hash/object or string, got #{item.class}"
71
75
  end
72
-
73
- Item.new(item, identifier: @identifier)
74
76
  end
75
77
  end
76
78
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Perron
4
4
  def self.deprecator
5
- @deprecator ||= ActiveSupport::Deprecation.new("1.0", "Perron")
5
+ @deprecator ||= ActiveSupport::Deprecation.new("1.3.0", "Perron")
6
6
  end
7
7
  end
data/lib/perron/engine.rb CHANGED
@@ -30,17 +30,19 @@ module Perron
30
30
  end
31
31
 
32
32
  initializer "perron.concierge", before: :add_builtin_route do |app|
33
- app.config.after_initialize do
34
- app.routes.append do
35
- namespace :perron do
36
- post :run_command, to: "concierge#run_command"
37
- end
33
+ if Rails.env.development?
34
+ app.config.after_initialize do
35
+ app.routes.append do
36
+ namespace :perron do
37
+ post :run_command, to: "concierge#run_command"
38
+ end
38
39
 
39
- root to: "perron/concierge#show" unless app.routes.named_routes.key?(:root)
40
+ root to: "perron/concierge#show" unless app.routes.named_routes.key?(:root)
41
+ end
40
42
  end
41
- end
42
43
 
43
- app.routes.finalize!
44
+ app.routes.finalize!
45
+ end
44
46
  end
45
47
 
46
48
  initializer "perron.inflections" do
@@ -57,6 +59,7 @@ module Perron
57
59
  load File.expand_path("../tasks/install.rake", __FILE__)
58
60
  load File.expand_path("../tasks/sync_sources.rake", __FILE__)
59
61
  load File.expand_path("../tasks/validate.rake", __FILE__)
62
+ load File.expand_path("../tasks/deploy.rake", __FILE__)
60
63
  end
61
64
  end
62
65
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perron
4
+ class Feeds
5
+ module Split
6
+ module_function
7
+
8
+ def grouped_resources(resources, extractor)
9
+ resources
10
+ .flat_map { |resource| extract(resource, extractor).map { [it, resource] } }
11
+ .group_by(&:first)
12
+ .transform_values { it.map(&:last) }
13
+ end
14
+
15
+ def split_values(resources, extractor)
16
+ resources
17
+ .flat_map { extract(it, extractor) }
18
+ .uniq
19
+ end
20
+
21
+ def extract(resource, extractor)
22
+ value = case extractor
23
+ when Symbol then resource.public_send(extractor)
24
+ when Proc then extractor.call(resource)
25
+ end
26
+
27
+ Array(value)
28
+ end
29
+
30
+ def split_path_for(type_config, value)
31
+ split_by = type_config[:split_by]
32
+
33
+ if split_by[:path]
34
+ split_by[:path].gsub(":value", value.to_s.parameterize)
35
+ else
36
+ extension = File.extname(type_config.path)
37
+ base_path = type_config.path.delete_suffix(extension)
38
+ field = split_by[:extractor]
39
+ field_name = field.is_a?(Symbol) ? field.to_s : "custom"
40
+
41
+ "#{base_path}/#{field_name}/#{value.to_s.parameterize}#{extension}"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
data/lib/perron/feeds.rb CHANGED
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "perron/feeds/split"
4
+
3
5
  module Perron
4
6
  class Feeds
5
7
  include ActionView::Helpers::TagHelper
8
+ include Perron::Feeds::Split
6
9
 
7
10
  def render(options = {})
8
11
  html_tags = []
@@ -22,6 +25,20 @@ module Perron
22
25
  title = "#{collection.name.humanize} #{type.to_s.humanize} Feed"
23
26
 
24
27
  html_tags << tag(:link, rel: "alternate", type: MIME_TYPES[type], title: title, href: absolute_url)
28
+
29
+ next unless feed[:split_by]
30
+
31
+ split_values(collection.resources, feed[:split_by][:extractor]).each do |value|
32
+ split_path = split_path_for(feed, value)
33
+ split_url = URI.join(url.root_url, split_path).to_s
34
+ split_title = if (title_template = feed[:split_by][:title])
35
+ title_template.gsub(":value", value.to_s.humanize)
36
+ else
37
+ "#{collection.name.humanize}: #{value.to_s.humanize} #{type.to_s.humanize} Feed"
38
+ end
39
+
40
+ html_tags << tag(:link, rel: "alternate", type: MIME_TYPES[type], title: split_title, href: split_url)
41
+ end
25
42
  end
26
43
  end
27
44
  end
@@ -9,7 +9,7 @@ module Perron
9
9
 
10
10
  next if src.blank? || absolute_url?(src)
11
11
 
12
- image["src"] = base_url + src
12
+ image["src"] = "#{base_url}/#{src.delete_prefix("/")}"
13
13
  end
14
14
  end
15
15
 
@@ -0,0 +1,14 @@
1
+ # Deploy configuration for Perron
2
+ # Requires Beam Up (`bundle add beam_up`)
3
+ # See also: https://perron.railsdesigner.com/docs/deploy/#deploy-task
4
+
5
+ # Uncomment and configure your provider:
6
+ # provider: seal_static
7
+
8
+ # seal_static:
9
+ # api_key: your_key_here
10
+
11
+ before_actions:
12
+ - RAILS_ENV=production bundle exec rails perron:build
13
+ after_actions:
14
+ - bundle exec rails perron:clobber
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perron
4
+ class Paginate
5
+ def initialize(collection, page:, per_page:, base_path: nil, page_path_template: nil, use_query_params: false)
6
+ @collection = collection
7
+ @per_page = per_page
8
+ @base_path = base_path
9
+ @page_path_template = page_path_template || "/page/:page/"
10
+ @use_query_params = use_query_params
11
+
12
+ @total_items = collection.size
13
+ @total_pages = total_items.zero? ? 0 : (total_items.to_f / per_page).ceil
14
+ @current_page = page.clamp(1, total_pages.zero? ? 1 : total_pages)
15
+ end
16
+
17
+ attr_reader :current_page, :total_pages, :total_items, :per_page
18
+
19
+ def items
20
+ offset = (@current_page - 1) * @per_page
21
+
22
+ @collection[offset, @per_page] || []
23
+ end
24
+
25
+ def next? = @current_page < @total_pages
26
+
27
+ def previous? = @current_page > 1
28
+
29
+ def next
30
+ return unless next?
31
+
32
+ page_path(@current_page + 1)
33
+ end
34
+
35
+ def previous
36
+ return unless previous?
37
+
38
+ target = (@current_page == 2) ? 1 : @current_page - 1
39
+
40
+ page_path(target)
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :use_query_params
46
+
47
+ def page_path(number)
48
+ return if number < 1
49
+ return if number > @total_pages && @total_pages > 0
50
+
51
+ return @base_path if number <= 1
52
+
53
+ return "#{@base_path}?page=#{number}" if @use_query_params
54
+
55
+ @base_path.sub(/\/$/, "") + @page_path_template.sub(":page", number.to_s)
56
+ end
57
+ end
58
+ end
@@ -2,9 +2,12 @@
2
2
 
3
3
  module Perron
4
4
  class Relation < Array
5
- def initialize(resources = [])
6
- super
5
+ def initialize(resources = [], model_class = nil)
6
+ super(resources)
7
+
8
+ @model_class = model_class
7
9
  end
10
+ attr_reader :model_class
8
11
 
9
12
  def where(**conditions)
10
13
  filtered = select do |resource|
@@ -19,21 +22,23 @@ module Perron
19
22
  end
20
23
  end
21
24
 
22
- Relation.new(filtered)
25
+ Relation.new(filtered, @model_class)
23
26
  end
24
27
 
25
- def limit(count) = Relation.new(first(count))
28
+ def limit(count) = Relation.new(first(count), @model_class)
26
29
 
27
- def offset(count) = Relation.new(drop(count))
30
+ def offset(count) = Relation.new(drop(count), @model_class)
28
31
 
29
32
  def order(attribute, direction = :asc)
30
33
  if attribute.is_a?(Hash)
31
34
  attribute, direction = attribute.first
32
35
  end
33
36
 
34
- sorted = sort_by { it.public_send(attribute) }
37
+ present, missing = partition { !it.public_send(attribute).nil? }
38
+ present = present.sort_by { it.public_send(attribute) }
39
+ present = present.reverse if direction == :desc
35
40
 
36
- Relation.new((direction == :desc) ? sorted.reverse : sorted)
41
+ Relation.new(present + missing, @model_class)
37
42
  end
38
43
 
39
44
  def pluck(*attributes)
@@ -47,5 +52,20 @@ module Perron
47
52
  end
48
53
  end
49
54
  end
55
+
56
+ def in_order_of(attribute, values, filter: true)
57
+ return Relation.new([]) if values.empty?
58
+
59
+ indexed = values.each_with_index.to_h
60
+
61
+ resources = if filter
62
+ select { indexed.key?(it.public_send(attribute)) }
63
+ .sort_by { indexed[it.public_send(attribute)] }
64
+ else
65
+ sort_by { indexed[it.public_send(attribute)] || Float::INFINITY }
66
+ end
67
+
68
+ Relation.new(resources)
69
+ end
50
70
  end
51
71
  end
@@ -22,6 +22,8 @@ module Perron
22
22
 
23
23
  def order(attribute, direction = :asc) = all.order(attribute, direction)
24
24
 
25
+ def in_order_of(attribute, values, filter: true) = all.in_order_of(attribute, values, filter:)
26
+
25
27
  def first(n = nil)
26
28
  n ? all.first(n) : all[0]
27
29
  end
@@ -44,6 +46,10 @@ module Perron
44
46
 
45
47
  def root = all.find(&:root?)
46
48
 
49
+ def destroy_all
50
+ all.each(&:destroy)
51
+ end
52
+
47
53
  def model_name
48
54
  @model_name ||= ActiveModel::Name.new(self, nil, name.demodulize.to_s)
49
55
  end
@@ -12,17 +12,17 @@ module Perron
12
12
 
13
13
  config.feeds = Options.new
14
14
 
15
- config.feeds.atom = ActiveSupport::OrderedOptions.new
15
+ config.feeds.atom = FeedTypeConfig.new
16
16
  config.feeds.atom.enabled = false
17
17
  config.feeds.atom.path = "feeds/#{collection.name.demodulize.parameterize}.atom"
18
18
  config.feeds.atom.max_items = 20
19
19
 
20
- config.feeds.json = ActiveSupport::OrderedOptions.new
20
+ config.feeds.json = FeedTypeConfig.new
21
21
  config.feeds.json.enabled = false
22
22
  config.feeds.json.path = "feeds/#{collection.name.demodulize.parameterize}.json"
23
23
  config.feeds.json.max_items = 20
24
24
 
25
- config.feeds.rss = ActiveSupport::OrderedOptions.new
25
+ config.feeds.rss = FeedTypeConfig.new
26
26
  config.feeds.rss.enabled = false
27
27
  config.feeds.rss.path = "feeds/#{collection.name.demodulize.parameterize}.xml"
28
28
  config.feeds.rss.max_items = 20
@@ -31,6 +31,9 @@ module Perron
31
31
  config.related_posts.enabled = false
32
32
  config.related_posts.max = 5
33
33
 
34
+ config.pagination = ActiveSupport::OrderedOptions.new
35
+ config.pagination.path_template = "/page/:page/"
36
+
34
37
  config.sitemap = ActiveSupport::OrderedOptions.new
35
38
  config.sitemap.exclude = false
36
39
  end
@@ -55,6 +58,16 @@ module Perron
55
58
  end
56
59
  end
57
60
  private_constant :Options
61
+
62
+ class FeedTypeConfig < ActiveSupport::OrderedOptions
63
+ def split_by(method_or_lambda = nil, path: nil, title: nil, &block)
64
+ extractor = method_or_lambda || block
65
+
66
+ self[:split_by] = {extractor: extractor}
67
+ self[:split_by][:path] = path if path
68
+ self[:split_by][:title] = title if title
69
+ end
70
+ end
58
71
  end
59
72
  end
60
73
  end
@@ -3,27 +3,34 @@
3
3
  module Perron
4
4
  class Resource
5
5
  class Metadata
6
- def initialize(resource:, frontmatter:, collection:)
6
+ def initialize(resource:, frontmatter:, collection:, controller_metadata: {})
7
7
  @resource = resource
8
8
  @frontmatter = frontmatter&.deep_symbolize_keys || {}
9
9
  @collection = collection
10
+ @controller_metadata = controller_metadata
10
11
  @config = Perron.configuration
11
12
  end
12
13
 
13
14
  def data
14
15
  @data ||= ActiveSupport::OrderedOptions
15
16
  .new
16
- .merge(apply_fallbacks_and_defaults(to: merged_site_collection_resource_frontmatter))
17
+ .merge(apply_fallbacks_and_defaults(to: merged_metadata))
17
18
  end
18
19
 
19
20
  private
20
21
 
21
- def merged_site_collection_resource_frontmatter = site_data.merge(collection_data).merge(@frontmatter)
22
+ def merged_metadata
23
+ site_data
24
+ .merge(collection_data)
25
+ .merge(@controller_metadata)
26
+ .merge(@frontmatter)
27
+ end
22
28
 
23
29
  def apply_fallbacks_and_defaults(to:)
24
30
  to[:title] ||= @config.site_name || Rails.application.name.underscore.camelize
25
31
 
26
32
  to[:canonical_url] ||= canonical_url
33
+ to[:explicit_canonical_url] = @frontmatter[:canonical_url]
27
34
 
28
35
  to[:image] = absolute_url(to[:image]) if to[:image]
29
36
 
@@ -65,7 +72,7 @@ module Perron
65
72
  return path if path.blank?
66
73
  return path if path.start_with?("http://", "https://", "//")
67
74
 
68
- Perron.configuration.url.delete_suffix("/") + path
75
+ "#{Perron.configuration.url.delete_suffix("/")}/#{path.delete_prefix("/")}"
69
76
  end
70
77
 
71
78
  def site_data