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.
- checksums.yaml +4 -4
- data/Gemfile.lock +11 -15
- data/app/controllers/perron/concierge_controller.rb +4 -1
- data/app/helpers/perron/markdown_helper.rb +3 -2
- data/app/helpers/perron/meta_tags_helper.rb +3 -9
- data/app/helpers/perron/paginate_helper.rb +40 -0
- data/app/views/perron/concierge/show.html.erb +1 -1
- data/lib/perron/collection.rb +2 -1
- data/lib/perron/configuration.rb +29 -4
- data/lib/perron/data_source.rb +6 -4
- data/lib/perron/deprecator.rb +1 -1
- data/lib/perron/engine.rb +11 -8
- data/lib/perron/feeds/split.rb +46 -0
- data/lib/perron/feeds.rb +17 -0
- data/lib/perron/html_processor/absolute_urls.rb +1 -1
- data/lib/perron/install/deploy.yml.erb +14 -0
- data/lib/perron/paginate.rb +58 -0
- data/lib/perron/relation.rb +27 -7
- data/lib/perron/resource/class_methods.rb +6 -0
- data/lib/perron/resource/configuration.rb +16 -3
- data/lib/perron/resource/metadata.rb +11 -4
- data/lib/perron/resource/publishable.rb +9 -1
- data/lib/perron/resource/separator.rb +11 -5
- data/lib/perron/resource/sourceable.rb +63 -9
- data/lib/perron/resource/updatable.rb +46 -0
- data/lib/perron/resource.rb +7 -0
- data/lib/perron/site/builder/feeds/atom.erb +1 -1
- data/lib/perron/site/builder/feeds/atom.rb +8 -2
- data/lib/perron/site/builder/feeds/json.rb +8 -2
- data/lib/perron/site/builder/feeds/rss.rb +8 -2
- data/lib/perron/site/builder/feeds.rb +32 -10
- data/lib/perron/site/builder/page.rb +19 -4
- data/lib/perron/site/builder/paths.rb +23 -1
- data/lib/perron/site/builder/sitemap.rb +23 -10
- data/lib/perron/site/builder.rb +24 -0
- data/lib/perron/site/validate.rb +18 -5
- data/lib/perron/tasks/deploy.rake +69 -0
- data/lib/perron/version.rb +1 -1
- data/lib/perron.rb +1 -0
- data/perron.gemspec +1 -1
- metadata +10 -4
|
@@ -54,7 +54,15 @@ module Perron
|
|
|
54
54
|
return @date_from_filename if defined?(@date_from_filename)
|
|
55
55
|
|
|
56
56
|
match = File.basename(file_path).match(DATE_REGEX)
|
|
57
|
-
|
|
57
|
+
|
|
58
|
+
@date_from_filename =
|
|
59
|
+
if match
|
|
60
|
+
begin
|
|
61
|
+
Date.new(match[:year].to_i, match[:month].to_i, match[:day].to_i).in_time_zone
|
|
62
|
+
rescue Date::Error
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
end
|
|
58
66
|
end
|
|
59
67
|
end
|
|
60
68
|
end
|
|
@@ -19,12 +19,18 @@ module Perron
|
|
|
19
19
|
|
|
20
20
|
def parsed(content)
|
|
21
21
|
if content =~ /\A---\s*(.*?)\s*---\s*(.*)/m
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
parsed_yaml = YAML.safe_load($1, permitted_classes: [Date, Time])
|
|
23
|
+
|
|
24
|
+
if parsed_yaml.nil? || parsed_yaml.is_a?(Hash)
|
|
25
|
+
@frontmatter = parsed_yaml || {}
|
|
26
|
+
@content = $2.strip
|
|
27
|
+
|
|
28
|
+
return
|
|
29
|
+
end
|
|
27
30
|
end
|
|
31
|
+
|
|
32
|
+
@frontmatter = {}
|
|
33
|
+
@content = content
|
|
28
34
|
end
|
|
29
35
|
end
|
|
30
36
|
end
|
|
@@ -5,6 +5,11 @@ module Perron
|
|
|
5
5
|
module Sourceable
|
|
6
6
|
extend ActiveSupport::Concern
|
|
7
7
|
|
|
8
|
+
MODES = {
|
|
9
|
+
single: ->(dataset) { dataset.zip },
|
|
10
|
+
combinations: ->(dataset) { dataset.to_a.combination(2).to_a }
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
class_methods do
|
|
9
14
|
def sources(*arguments)
|
|
10
15
|
@source_definitions = parsed(*arguments)
|
|
@@ -32,12 +37,14 @@ module Perron
|
|
|
32
37
|
def generate_from_sources!
|
|
33
38
|
return unless source_backed?
|
|
34
39
|
|
|
35
|
-
|
|
40
|
+
extension = source_definitions.values.first&.dig(:extension) || "erb"
|
|
41
|
+
|
|
42
|
+
derive.each do |combo|
|
|
36
43
|
content = content_with combo
|
|
37
44
|
filename = filename_with combo
|
|
38
45
|
|
|
39
46
|
FileUtils.mkdir_p(output_dir)
|
|
40
|
-
File.write(output_dir.join("#{filename}
|
|
47
|
+
File.write(output_dir.join("#{filename}.#{extension}"), content)
|
|
41
48
|
end
|
|
42
49
|
end
|
|
43
50
|
|
|
@@ -48,7 +55,7 @@ module Perron
|
|
|
48
55
|
def parsed(*arguments)
|
|
49
56
|
return {} if arguments.empty?
|
|
50
57
|
|
|
51
|
-
arguments.flat_map do |argument|
|
|
58
|
+
definitions = arguments.flat_map do |argument|
|
|
52
59
|
case argument
|
|
53
60
|
when Hash
|
|
54
61
|
argument.to_a
|
|
@@ -58,11 +65,39 @@ module Perron
|
|
|
58
65
|
[[argument, {primary_key: :id}]]
|
|
59
66
|
end
|
|
60
67
|
end.to_h
|
|
68
|
+
|
|
69
|
+
if definitions.values.any? { it[:mode] }
|
|
70
|
+
raise ArgumentError, "mode is only supported for single-source definitions" if definitions.size > 1
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
definitions
|
|
61
74
|
end
|
|
62
75
|
|
|
63
|
-
def
|
|
76
|
+
def derive
|
|
64
77
|
datasets = source_names.map { resolve it }
|
|
65
78
|
|
|
79
|
+
if single_source_with_mode?
|
|
80
|
+
derive_from_single_source(datasets.first)
|
|
81
|
+
else
|
|
82
|
+
derive_from_multiple_sources(datasets)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def derive_from_single_source(dataset)
|
|
87
|
+
source_name = source_names.first
|
|
88
|
+
mode = source_definitions[source_name][:mode] || :single
|
|
89
|
+
method = MODES[mode.to_sym] || raise(ArgumentError, "Unknown mode: #{mode}")
|
|
90
|
+
|
|
91
|
+
method.call(dataset).each do |combo|
|
|
92
|
+
primary_key = source_definitions[source_name][:primary_key] || :id
|
|
93
|
+
|
|
94
|
+
combo.each do |item|
|
|
95
|
+
raise Errors::DataParseError, "Primary key `#{primary_key}` is nil for row" if item.public_send(primary_key).nil?
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def derive_from_multiple_sources(datasets)
|
|
66
101
|
datasets.first.product(*datasets[1..]).each do |combo|
|
|
67
102
|
combo.each_with_index do |item, index|
|
|
68
103
|
name = source_names[index]
|
|
@@ -74,18 +109,37 @@ module Perron
|
|
|
74
109
|
end
|
|
75
110
|
|
|
76
111
|
def content_with(combo)
|
|
77
|
-
data =
|
|
112
|
+
data = if single_source_with_mode?
|
|
113
|
+
source_name = source_names.first
|
|
114
|
+
names = source_definitions[source_name][:as]&.map(&:to_sym) || (1..combo.size).map { :"#{source_name}_#{it}" }
|
|
115
|
+
|
|
116
|
+
combo.each_with_index.to_h { |item, index| [names[index], item] }
|
|
117
|
+
else
|
|
118
|
+
source_names.each_with_index.to_h { |name, index| [name, combo[index]] }
|
|
119
|
+
end
|
|
120
|
+
|
|
78
121
|
source = Source.new(data)
|
|
79
122
|
|
|
80
123
|
source_template(source)
|
|
81
124
|
end
|
|
82
125
|
|
|
83
126
|
def filename_with(combo)
|
|
84
|
-
|
|
85
|
-
|
|
127
|
+
if single_source_with_mode?
|
|
128
|
+
source_name = source_names.first
|
|
129
|
+
primary_key = source_definitions[source_name][:primary_key] || :id
|
|
130
|
+
|
|
131
|
+
combo.map { it.public_send(primary_key) }.join("-")
|
|
132
|
+
else
|
|
133
|
+
source_names.each_with_index.map do |name, index|
|
|
134
|
+
primary_key = source_definitions[name][:primary_key]
|
|
135
|
+
|
|
136
|
+
combo[index].public_send(primary_key)
|
|
137
|
+
end.join("-")
|
|
138
|
+
end
|
|
139
|
+
end
|
|
86
140
|
|
|
87
|
-
|
|
88
|
-
|
|
141
|
+
def single_source_with_mode?
|
|
142
|
+
source_names.one? && source_definitions[source_names.first][:mode]
|
|
89
143
|
end
|
|
90
144
|
|
|
91
145
|
def output_dir = Perron.configuration.input.join(model_name.collection)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Perron
|
|
4
|
+
class Resource
|
|
5
|
+
module Updatable
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
def metadata
|
|
10
|
+
@metadata ||= Proxy.new(super, self)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class Proxy
|
|
15
|
+
def initialize(metadata, resource)
|
|
16
|
+
@metadata = metadata
|
|
17
|
+
@resource = resource
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def update(attributes)
|
|
21
|
+
attributes = attributes.deep_symbolize_keys
|
|
22
|
+
|
|
23
|
+
separator = Perron::Resource::Separator.new(@resource.raw_content)
|
|
24
|
+
frontmatter = separator.frontmatter.to_h.merge(attributes)
|
|
25
|
+
yaml = frontmatter.transform_keys(&:to_s).to_yaml.sub(/\A---\n/, "")
|
|
26
|
+
body = separator.content
|
|
27
|
+
|
|
28
|
+
File.write(@resource.file_path, "---\n#{yaml}---\n\n#{body}\n")
|
|
29
|
+
|
|
30
|
+
@resource.instance_variable_set(:@frontmatter, nil)
|
|
31
|
+
attributes.each { |key, value| @metadata[key] = value }
|
|
32
|
+
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def respond_to_missing?(name, include_private = false)
|
|
37
|
+
@metadata.respond_to?(name, include_private) || super
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def method_missing(name, *arguments, &block)
|
|
41
|
+
@metadata.public_send(name, *arguments, &block)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/perron/resource.rb
CHANGED
|
@@ -19,6 +19,7 @@ require "perron/resource/sourceable"
|
|
|
19
19
|
require "perron/resource/sweeper"
|
|
20
20
|
require "perron/resource/adjacency"
|
|
21
21
|
require "perron/resource/table_of_content"
|
|
22
|
+
require "perron/resource/updatable"
|
|
22
23
|
|
|
23
24
|
module Perron
|
|
24
25
|
class Resource
|
|
@@ -96,6 +97,10 @@ module Perron
|
|
|
96
97
|
slug == "/"
|
|
97
98
|
end
|
|
98
99
|
|
|
100
|
+
def destroy
|
|
101
|
+
File.delete(@file_path) and self
|
|
102
|
+
end
|
|
103
|
+
|
|
99
104
|
private
|
|
100
105
|
|
|
101
106
|
ID_LENGTH = 8
|
|
@@ -117,6 +122,8 @@ module Perron
|
|
|
117
122
|
end
|
|
118
123
|
|
|
119
124
|
def erb_processing?
|
|
125
|
+
return false if metadata.erb == false
|
|
126
|
+
|
|
120
127
|
@file_path.ends_with?(".erb") || metadata.erb == true
|
|
121
128
|
end
|
|
122
129
|
end
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
<% resources.each do |resource| %>
|
|
18
18
|
<entry>
|
|
19
|
-
<id><%= url_for_resource.call(resource)
|
|
19
|
+
<id><%= url_for_resource.call(resource) %></id>
|
|
20
20
|
<title><%= resource.metadata.title %></title>
|
|
21
21
|
<link href="<%= url_for_resource.call(resource) %>" rel="alternate" type="text/html"/>
|
|
22
22
|
<published><%= resource.published_at&.iso8601 %></published>
|
|
@@ -11,8 +11,10 @@ module Perron
|
|
|
11
11
|
include Feeds::Author
|
|
12
12
|
include Feeds::Template
|
|
13
13
|
|
|
14
|
-
def initialize(collection:)
|
|
14
|
+
def initialize(collection:, resources: nil, feed_config: nil)
|
|
15
15
|
@collection = collection
|
|
16
|
+
@resources = resources
|
|
17
|
+
@feed_config = feed_config
|
|
16
18
|
@configuration = Perron.configuration
|
|
17
19
|
end
|
|
18
20
|
|
|
@@ -28,12 +30,16 @@ module Perron
|
|
|
28
30
|
private
|
|
29
31
|
|
|
30
32
|
def resources
|
|
31
|
-
@
|
|
33
|
+
(@resources || @collection.resources)
|
|
32
34
|
.reject { it.metadata.feed == false }
|
|
33
35
|
.sort_by { it.metadata.published_at || it.metadata.updated_at || Time.current }
|
|
34
36
|
.reverse
|
|
35
37
|
.take(feed_configuration.max_items)
|
|
36
38
|
end
|
|
39
|
+
|
|
40
|
+
def feed_configuration
|
|
41
|
+
@feed_config || super
|
|
42
|
+
end
|
|
37
43
|
end
|
|
38
44
|
end
|
|
39
45
|
end
|
|
@@ -11,8 +11,10 @@ module Perron
|
|
|
11
11
|
include Feeds::Author
|
|
12
12
|
include Feeds::Template
|
|
13
13
|
|
|
14
|
-
def initialize(collection:)
|
|
14
|
+
def initialize(collection:, resources: nil, feed_config: nil)
|
|
15
15
|
@collection = collection
|
|
16
|
+
@resources = resources
|
|
17
|
+
@feed_config = feed_config
|
|
16
18
|
@configuration = Perron.configuration
|
|
17
19
|
end
|
|
18
20
|
|
|
@@ -28,12 +30,16 @@ module Perron
|
|
|
28
30
|
private
|
|
29
31
|
|
|
30
32
|
def resources
|
|
31
|
-
@
|
|
33
|
+
(@resources || @collection.resources)
|
|
32
34
|
.reject { it.metadata.feed == false }
|
|
33
35
|
.sort_by { it.metadata.published_at || it.metadata.updated_at || Time.current }
|
|
34
36
|
.reverse
|
|
35
37
|
.take(feed_configuration.max_items)
|
|
36
38
|
end
|
|
39
|
+
|
|
40
|
+
def feed_configuration
|
|
41
|
+
@feed_config || super
|
|
42
|
+
end
|
|
37
43
|
end
|
|
38
44
|
end
|
|
39
45
|
end
|
|
@@ -11,8 +11,10 @@ module Perron
|
|
|
11
11
|
include Feeds::Author
|
|
12
12
|
include Feeds::Template
|
|
13
13
|
|
|
14
|
-
def initialize(collection:)
|
|
14
|
+
def initialize(collection:, resources: nil, feed_config: nil)
|
|
15
15
|
@collection = collection
|
|
16
|
+
@resources = resources
|
|
17
|
+
@feed_config = feed_config
|
|
16
18
|
@configuration = Perron.configuration
|
|
17
19
|
end
|
|
18
20
|
|
|
@@ -28,12 +30,16 @@ module Perron
|
|
|
28
30
|
private
|
|
29
31
|
|
|
30
32
|
def resources
|
|
31
|
-
@
|
|
33
|
+
(@resources || @collection.resources)
|
|
32
34
|
.reject { it.metadata.feed == false }
|
|
33
35
|
.sort_by { it.metadata.published_at || it.metadata.updated_at || Time.current }
|
|
34
36
|
.reverse
|
|
35
37
|
.take(feed_configuration.max_items)
|
|
36
38
|
end
|
|
39
|
+
|
|
40
|
+
def feed_configuration
|
|
41
|
+
@feed_config || super
|
|
42
|
+
end
|
|
37
43
|
end
|
|
38
44
|
end
|
|
39
45
|
end
|
|
@@ -4,10 +4,14 @@ require "perron/site/builder/feeds/atom"
|
|
|
4
4
|
require "perron/site/builder/feeds/json"
|
|
5
5
|
require "perron/site/builder/feeds/rss"
|
|
6
6
|
|
|
7
|
+
require "perron/feeds/split"
|
|
8
|
+
|
|
7
9
|
module Perron
|
|
8
10
|
module Site
|
|
9
11
|
class Builder
|
|
10
12
|
class Feeds
|
|
13
|
+
include Perron::Feeds::Split
|
|
14
|
+
|
|
11
15
|
def initialize(output_path)
|
|
12
16
|
@output_path = output_path
|
|
13
17
|
end
|
|
@@ -18,21 +22,39 @@ module Perron
|
|
|
18
22
|
|
|
19
23
|
config = collection.configuration.feeds
|
|
20
24
|
|
|
21
|
-
if config.atom.enabled
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
generate_feed(collection: collection, type_config: config.atom, generator_class: Atom) if config.atom.enabled
|
|
26
|
+
generate_feed(collection: collection, type_config: config.json, generator_class: Json) if config.json.enabled
|
|
27
|
+
generate_feed(collection: collection, type_config: config.rss, generator_class: Rss) if config.rss.enabled
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def generate_feed(collection:, type_config:, generator_class:)
|
|
34
|
+
generator = generator_class.new(collection: collection)
|
|
35
|
+
content = generator.generate
|
|
24
36
|
|
|
25
|
-
|
|
26
|
-
create_file at: config.json.path, with: Json.new(collection: collection).generate
|
|
27
|
-
end
|
|
37
|
+
create_file(at: type_config.path, with: content) if content.present?
|
|
28
38
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
return unless type_config[:split_by]
|
|
40
|
+
|
|
41
|
+
grouped_resources(collection.resources, type_config[:split_by][:extractor]).each do |value, group|
|
|
42
|
+
path = split_path_for(type_config, value)
|
|
43
|
+
config = split_config(type_config, value, path)
|
|
44
|
+
content = generator_class.new(collection: collection, resources: group, feed_config: config).generate
|
|
45
|
+
|
|
46
|
+
create_file(at: path, with: content) if content.present?
|
|
32
47
|
end
|
|
33
48
|
end
|
|
34
49
|
|
|
35
|
-
|
|
50
|
+
def split_config(type_config, value, split_path)
|
|
51
|
+
ActiveSupport::OrderedOptions.new.tap do |config|
|
|
52
|
+
type_config.each { |key, value| config[key] = value unless key == :split_by }
|
|
53
|
+
|
|
54
|
+
config.path = split_path
|
|
55
|
+
config.title = "#{type_config.title.presence || Perron.configuration.site_name}: #{value.to_s.humanize}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
36
58
|
|
|
37
59
|
def create_file(at:, with:)
|
|
38
60
|
return if with.blank?
|
|
@@ -11,13 +11,15 @@ module Perron
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def render
|
|
14
|
-
|
|
14
|
+
info = route_info
|
|
15
|
+
return puts " ❌ ERROR: No route matches '#{@path}'" unless info
|
|
16
|
+
|
|
15
17
|
request = ActionDispatch::Request.new(env)
|
|
16
18
|
response = ActionDispatch::Response.new
|
|
17
19
|
|
|
18
|
-
request.path_parameters =
|
|
20
|
+
request.path_parameters = info
|
|
19
21
|
|
|
20
|
-
controller.dispatch(action, request, response)
|
|
22
|
+
controller.dispatch(info[:action], request, response)
|
|
21
23
|
|
|
22
24
|
return puts " ❌ ERROR: Request failed for '#{@path}' (Status: #{response.status})" unless response.successful?
|
|
23
25
|
|
|
@@ -41,7 +43,20 @@ module Perron
|
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
def route_info
|
|
44
|
-
@route_info ||=
|
|
46
|
+
@route_info ||= recognize_path_with_pagination_fallback(@path)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def recognize_path_with_pagination_fallback(path)
|
|
50
|
+
Rails.application.routes.recognize_path(path)
|
|
51
|
+
rescue ActionController::RoutingError
|
|
52
|
+
return nil unless (match = path.match(/\/(.+)\/page\/(\d+)\//))
|
|
53
|
+
|
|
54
|
+
base_path = "/#{match[1]}"
|
|
55
|
+
page_number = match[2].to_i
|
|
56
|
+
|
|
57
|
+
Rails.application.routes.recognize_path(base_path).tap do |route_info|
|
|
58
|
+
route_info[:page] = page_number
|
|
59
|
+
end
|
|
45
60
|
end
|
|
46
61
|
|
|
47
62
|
def env = Rack::MockRequest.env_for(@path, "HTTP_HOST" => Perron.configuration.default_url_options[:host])
|
|
@@ -28,12 +28,30 @@ module Perron
|
|
|
28
28
|
raise "Route `#{route.name}` (#{route.path.spec}) is an index route but requires parameters #{required_params}. Perron doesn't know how to generate these parameters."
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
base_path = routes.public_send("#{route.name}_path")
|
|
32
|
+
collection = collection_for(route)
|
|
33
|
+
return [base_path] unless collection
|
|
34
|
+
|
|
35
|
+
pagination = collection.configuration.pagination
|
|
36
|
+
return [base_path] unless pagination.per_page
|
|
37
|
+
|
|
38
|
+
total = collection.all.count
|
|
39
|
+
total_pages = (total.to_f / pagination.per_page).ceil
|
|
40
|
+
return [base_path] if total_pages <= 1
|
|
41
|
+
|
|
42
|
+
[base_path] + (2..total_pages).map do |page_number|
|
|
43
|
+
build_paginated_path(route, page_number, pagination.path_template)
|
|
44
|
+
end
|
|
32
45
|
when "show" then show_paths_for(route)
|
|
33
46
|
else []
|
|
34
47
|
end
|
|
35
48
|
end
|
|
36
49
|
|
|
50
|
+
def build_paginated_path(route, page_number, path_template)
|
|
51
|
+
routes.public_send("#{route.name}_path")
|
|
52
|
+
.sub(/\/$/, "") + path_template.sub(":page", page_number.to_s)
|
|
53
|
+
end
|
|
54
|
+
|
|
37
55
|
def show_paths_for(route)
|
|
38
56
|
resources_for(route).reject(&:root?).map do |resource|
|
|
39
57
|
routes.public_send("#{route.name}_path", resource)
|
|
@@ -73,6 +91,10 @@ module Perron
|
|
|
73
91
|
def sitemap_change_frequency = nil
|
|
74
92
|
|
|
75
93
|
def updated_at = nil
|
|
94
|
+
|
|
95
|
+
def canonical_url = nil
|
|
96
|
+
|
|
97
|
+
def explicit_canonical_url = nil
|
|
76
98
|
end
|
|
77
99
|
end
|
|
78
100
|
end
|
|
@@ -52,9 +52,8 @@ module Perron
|
|
|
52
52
|
routes.with_options(Perron.configuration.default_url_options) do |url|
|
|
53
53
|
with.url do
|
|
54
54
|
with.loc url.public_send(route_name.to_s.gsub("_path", "_url"))
|
|
55
|
-
with.priority Perron.configuration.sitemap.priority
|
|
56
|
-
with.changefreq Perron.configuration.sitemap.change_frequency
|
|
57
|
-
with.lastmod Time.current.iso8601
|
|
55
|
+
with.priority Perron.configuration.sitemap.priority if Perron.configuration.sitemap.emit_priority
|
|
56
|
+
with.changefreq Perron.configuration.sitemap.change_frequency if Perron.configuration.sitemap.emit_changefreq
|
|
58
57
|
end
|
|
59
58
|
end
|
|
60
59
|
end
|
|
@@ -76,8 +75,8 @@ module Perron
|
|
|
76
75
|
|
|
77
76
|
with.url do
|
|
78
77
|
with.loc loc
|
|
79
|
-
with.priority Perron.configuration.sitemap.priority
|
|
80
|
-
with.changefreq Perron.configuration.sitemap.change_frequency
|
|
78
|
+
with.priority Perron.configuration.sitemap.priority if Perron.configuration.sitemap.emit_priority
|
|
79
|
+
with.changefreq Perron.configuration.sitemap.change_frequency if Perron.configuration.sitemap.emit_changefreq
|
|
81
80
|
with.lastmod lastmod if lastmod
|
|
82
81
|
end
|
|
83
82
|
end
|
|
@@ -95,14 +94,15 @@ module Perron
|
|
|
95
94
|
|
|
96
95
|
loc = resource.root? ? routes.root_url(url_options) : routes.public_send("#{route.name}_url", resource, **url_options)
|
|
97
96
|
|
|
97
|
+
return if canonical_mismatch?(resource.metadata.explicit_canonical_url, loc)
|
|
98
98
|
return if added_urls.include?(loc)
|
|
99
99
|
added_urls << loc
|
|
100
100
|
|
|
101
101
|
routes.with_options(Perron.configuration.default_url_options) do |url|
|
|
102
102
|
with.url do
|
|
103
103
|
with.loc loc
|
|
104
|
-
with.priority priority
|
|
105
|
-
with.changefreq change_frequency
|
|
104
|
+
with.priority priority if Perron.configuration.sitemap.emit_priority
|
|
105
|
+
with.changefreq change_frequency if Perron.configuration.sitemap.emit_changefreq
|
|
106
106
|
with.lastmod resource.metadata.updated_at&.iso8601 if resource.metadata.updated_at.present?
|
|
107
107
|
end
|
|
108
108
|
end
|
|
@@ -111,10 +111,23 @@ module Perron
|
|
|
111
111
|
def last_modified_for(collection)
|
|
112
112
|
return unless collection
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
dates = resources.filter_map { it.metadata.updated_at || it.metadata.publication_date }
|
|
114
|
+
modification_dates = collection.send(:load_resources).select(&:buildable?).filter_map { it.metadata.updated_at }
|
|
116
115
|
|
|
117
|
-
|
|
116
|
+
modification_dates.max&.iso8601
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def canonical_mismatch?(canonical, location)
|
|
120
|
+
return false unless canonical.is_a?(String) && canonical.present?
|
|
121
|
+
|
|
122
|
+
canonical_uri = URI.parse(canonical)
|
|
123
|
+
canonical_uri = URI.join(location, canonical) unless canonical_uri.host
|
|
124
|
+
page_uri = URI.parse(location)
|
|
125
|
+
|
|
126
|
+
normalize = ->(uri) { [uri.host, uri.path].map(&:to_s).map(&:downcase).join("|") }
|
|
127
|
+
|
|
128
|
+
normalize.call(canonical_uri) != normalize.call(page_uri)
|
|
129
|
+
rescue URI::InvalidURIError
|
|
130
|
+
false
|
|
118
131
|
end
|
|
119
132
|
|
|
120
133
|
def routes = Rails.application.routes.url_helpers
|
data/lib/perron/site/builder.rb
CHANGED
|
@@ -16,6 +16,8 @@ module Perron
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def build
|
|
19
|
+
run_hook(:before_build)
|
|
20
|
+
|
|
19
21
|
if Perron.configuration.mode.standalone?
|
|
20
22
|
puts "🧹 Cleaning previous build…"
|
|
21
23
|
|
|
@@ -35,6 +37,8 @@ module Perron
|
|
|
35
37
|
output_preview_urls
|
|
36
38
|
|
|
37
39
|
puts "\n✅ Build complete"
|
|
40
|
+
ensure
|
|
41
|
+
run_hook(:after_build)
|
|
38
42
|
end
|
|
39
43
|
|
|
40
44
|
private
|
|
@@ -58,6 +62,26 @@ module Perron
|
|
|
58
62
|
end
|
|
59
63
|
end
|
|
60
64
|
end
|
|
65
|
+
|
|
66
|
+
def run_hook(name)
|
|
67
|
+
return unless (hook = Perron.configuration.public_send(name))
|
|
68
|
+
|
|
69
|
+
context = Context.new(
|
|
70
|
+
output_path: @output_path.to_s,
|
|
71
|
+
mode: Perron.configuration.mode
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
hook.call(context)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
class Context
|
|
78
|
+
attr_reader :output_path, :mode
|
|
79
|
+
|
|
80
|
+
def initialize(output_path:, mode:)
|
|
81
|
+
@output_path = output_path
|
|
82
|
+
@mode = mode
|
|
83
|
+
end
|
|
84
|
+
end
|
|
61
85
|
end
|
|
62
86
|
end
|
|
63
87
|
end
|
data/lib/perron/site/validate.rb
CHANGED
|
@@ -36,7 +36,11 @@ module Perron
|
|
|
36
36
|
def validate_collection(collection)
|
|
37
37
|
collection.resources.each do |resource|
|
|
38
38
|
resource.validate ? success : failed(resource)
|
|
39
|
+
rescue Psych::SyntaxError => error
|
|
40
|
+
render_yaml error, resource.file_path
|
|
39
41
|
end
|
|
42
|
+
rescue Psych::SyntaxError => error
|
|
43
|
+
render_yaml error, "unknown"
|
|
40
44
|
end
|
|
41
45
|
|
|
42
46
|
def success = print "#{GREEN}.#{RESET}"
|
|
@@ -44,13 +48,22 @@ module Perron
|
|
|
44
48
|
def failed(resource)
|
|
45
49
|
print "#{RED}F#{RESET}"
|
|
46
50
|
|
|
47
|
-
|
|
51
|
+
@failures << Failure.new(
|
|
52
|
+
identifier: resource.file_path,
|
|
53
|
+
errors: resource.errors.respond_to?(:full_messages) ? resource.errors.full_messages : []
|
|
54
|
+
)
|
|
55
|
+
end
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
def render_yaml(error, identifier)
|
|
58
|
+
print "#{RED}F#{RESET}"
|
|
59
|
+
|
|
60
|
+
line_info = error.line ? " at line #{error.line}" : ""
|
|
61
|
+
column_info = error.column ? ", column #{error.column}" : ""
|
|
52
62
|
|
|
53
|
-
@failures << Failure.new(
|
|
63
|
+
@failures << Failure.new(
|
|
64
|
+
identifier: identifier,
|
|
65
|
+
errors: ["Invalid YAML#{line_info}#{column_info}: #{error.problem}"]
|
|
66
|
+
)
|
|
54
67
|
end
|
|
55
68
|
|
|
56
69
|
def failures_report
|