diff --git a/lib/perron/site/builder/feeds/atom.erb b/lib/perron/site/builder/feeds/atom.erb
new file mode 100644
index 0000000..66e7c65
--- /dev/null
+++ b/lib/perron/site/builder/feeds/atom.erb
@@ -0,0 +1,44 @@
+
+
+ Perron
+ <%= current_feed_url %>
+ <%= config.title.presence || @configuration.site_name %>
+ <%= config.description.presence || @configuration.site_description %>
+
+
+ <%= resources.first&.published_at&.iso8601 || Time.current.iso8601 %>
+
+ <% feed_author = config.author || { name: @configuration.site_name, email: "noreply@#{URI.parse(@configuration.url).host}" } %>
+
+ <% if feed_author[:name] %><%= feed_author[:name] %><% end %>
+ <% if feed_author[:email] %><%= feed_author[:email] %><% end %>
+
+
+ <% resources.each do |resource| %>
+
+ <%= url_for_resource(resource) || "#{@configuration.url}/posts/#{resource.id}" %>
+ <%= resource.metadata.title %>
+
+ <%= resource.published_at&.iso8601 %>
+ <%= (resource.metadata.updated_at || resource.published_at)&.iso8601 %>
+
+ <% entry_author = author(resource); if entry_author %>
+
+ <% if entry_author.name %><%= entry_author.name %><% end %>
+ <% if entry_author.email %><%= entry_author.email %><% end %>
+
+ <% end %>
+
+ <% base_url = url_for_resource(resource) %>
+ <% if base_url %>
+ ]]>
+ <% else %>
+ ]]>
+ <% end %>
+
+ <% resource.metadata.tags&.each do |tag| %>
+
+ <% end %>
+
+ <% end %>
+
diff --git a/lib/perron/site/builder/feeds/atom.rb b/lib/perron/site/builder/feeds/atom.rb
index 3b5758e..332ba58 100644
--- a/lib/perron/site/builder/feeds/atom.rb
+++ b/lib/perron/site/builder/feeds/atom.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require "nokogiri"
require "perron/site/builder/feeds/author"
+require "perron/site/builder/feeds/template"
module Perron
module Site
@@ -9,6 +9,7 @@ class Builder
class Feeds
class Atom
include Feeds::Author
+ include Feeds::Template
def initialize(collection:)
@collection = collection
@@ -18,58 +19,10 @@ def initialize(collection:)
def generate
return if resources.empty?
- Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
- xml.feed(xmlns: "http://www.w3.org/2005/Atom") do
- xml.generator "Perron", uri: @configuration.url, version: Perron::VERSION
- xml.id current_feed_url
- xml.title feed_configuration.title.presence || @configuration.site_name
- xml.subtitle feed_configuration.description.presence || @configuration.site_description
- xml.link href: current_feed_url, rel: "self", type: "application/atom+xml"
- xml.link href: @configuration.url, rel: "alternate", type: "text/html"
- xml.updated resources.first&.published_at&.iso8601 || Time.current.iso8601
+ template = find_template("atom")
+ return unless template
- feed_author = feed_configuration.author || {
- name: @configuration.site_name,
- email: "noreply@#{URI.parse(@configuration.url).host}"
- }
-
- xml.author do
- xml.name feed_author[:name] if feed_author[:name]
- xml.email feed_author[:email] if feed_author[:email]
- end
-
- resources.each do |resource|
- xml.entry do
- xml.title resource.metadata.title
- xml.link href: url_for_resource(resource), rel: "alternate", type: "text/html"
- xml.published resource.published_at&.iso8601
- xml.updated (resource.metadata.updated_at || resource.published_at)&.iso8601
- xml.id url_for_resource(resource) || "#{@configuration.url}/posts/#{resource.id}"
-
- if (entry_author = author(resource))
- xml.author do
- xml.name entry_author.name if entry_author.name
- xml.email entry_author.email if entry_author.email
- end
- end
-
- if (base_url = url_for_resource(resource))
- xml.content :type => "html", "xml:base" => base_url do
- xml.cdata(Perron::Markdown.render(resource.content))
- end
- else
- xml.content type: "html" do
- xml.cdata(Perron::Markdown.render(resource.content))
- end
- end
-
- resource.metadata.tags&.each do |tag|
- xml.category term: tag
- end
- end
- end
- end
- end.to_xml
+ render(template, feed_configuration)
end
private
@@ -81,24 +34,6 @@ def resources
.reverse
.take(feed_configuration.max_items)
end
-
- def url_for_resource(resource)
- routes
- .polymorphic_url(resource, **@configuration.default_url_options.merge(ref: feed_configuration.ref))
- .delete_suffix("?ref=")
- rescue
- nil
- end
-
- def current_feed_url
- path = feed_configuration.path || "feed.atom"
-
- URI.join(@configuration.url, path).to_s
- end
-
- def feed_configuration = @collection.configuration.feeds.atom
-
- def routes = Rails.application.routes.url_helpers
end
end
end
diff --git a/lib/perron/site/builder/feeds/json.erb b/lib/perron/site/builder/feeds/json.erb
new file mode 100644
index 0000000..be8dac8
--- /dev/null
+++ b/lib/perron/site/builder/feeds/json.erb
@@ -0,0 +1,19 @@
+<%= {
+ generator: "Perron (#{Perron::VERSION})",
+ version: "https://jsonfeed.org/version/1.1",
+ home_page_url: @configuration.url,
+ title: config.title.presence || @configuration.site_name,
+ description: config.description.presence || @configuration.site_description,
+
+ items: resources.map { |resource|
+ item_author = author.call(resource)
+ {
+ id: resource.id,
+ url: url_for_resource.call(resource),
+ 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)
+ }.compact
+ }
+}.to_json %>
diff --git a/lib/perron/site/builder/feeds/json.rb b/lib/perron/site/builder/feeds/json.rb
index be12f39..7f35155 100644
--- a/lib/perron/site/builder/feeds/json.rb
+++ b/lib/perron/site/builder/feeds/json.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require "json"
require "perron/site/builder/feeds/author"
+require "perron/site/builder/feeds/template"
module Perron
module Site
@@ -9,6 +9,7 @@ class Builder
class Feeds
class Json
include Feeds::Author
+ include Feeds::Template
def initialize(collection:)
@collection = collection
@@ -16,60 +17,23 @@ def initialize(collection:)
end
def generate
- return nil if resources.empty?
+ return if resources.empty?
- hash = {
- generator: "Perron (#{Perron::VERSION})",
- version: "https://jsonfeed.org/version/1.1",
- home_page_url: @configuration.url,
- title: feed_configuration.title.presence || @configuration.site_name,
- description: feed_configuration.description.presence || @configuration.site_description,
- items: resources.filter_map { jsonify(it) }
- }
+ template = find_template("json")
+ return unless template
- JSON.pretty_generate hash
+ render(template, feed_configuration)
end
private
def resources
- @resources ||= @collection.resources
+ @resource ||= @collection.resources
.reject { it.metadata.feed == false }
.sort_by { it.metadata.published_at || it.metadata.updated_at || Time.current }
.reverse
.take(feed_configuration.max_items)
end
-
- def jsonify(resource)
- {
- id: resource.id,
- url: url_for_resource(resource),
- date_published: resource.published_at&.iso8601,
- authors: authors(resource),
- title: resource.metadata.title,
- content_html: Perron::Markdown.render(resource.content)
- }.compact
- end
-
- def url_for_resource(resource)
- routes
- .polymorphic_url(resource, **@configuration.default_url_options.merge(ref: feed_configuration.ref))
- .delete_suffix("?ref=")
- rescue
- nil
- end
-
- def authors(resource)
- author = author(resource)
-
- return nil unless author&.name
-
- [{name: author.name, email: author.email, url: author.url, avatar: author.avatar}.compact].presence
- end
-
- def feed_configuration = @collection.configuration.feeds.json
-
- def routes = Rails.application.routes.url_helpers
end
end
end
diff --git a/lib/perron/site/builder/feeds/rss.erb b/lib/perron/site/builder/feeds/rss.erb
new file mode 100644
index 0000000..1cdec1f
--- /dev/null
+++ b/lib/perron/site/builder/feeds/rss.erb
@@ -0,0 +1,28 @@
+
+
+
+ Perron (<%= Perron::VERSION %>)
+ <%= @configuration.url %>
+ <%= config.title.presence || @configuration.site_name %>
+ <%= config.description.presence || @configuration.site_description %>
+
+ <% resources.each do |resource| %>
+ -
+ <%= resource.id %>
+
+ <% resource_url = url_for_resource(resource) %>
+ <% if resource_url %>
+ <%= resource_url %>
+ <% end %>
+
+ <%= resource.published_at&.rfc822 %>
+ <% author = author(resource); if author && author.email %>
+ <%= author.name ? "#{author.email} (#{author.name})" : author.email %>
+ <% end %>
+ <%= resource.metadata.title %>
+
+ ]]>
+
+ <% end %>
+
+
diff --git a/lib/perron/site/builder/feeds/rss.rb b/lib/perron/site/builder/feeds/rss.rb
index 78bf199..7225d8d 100644
--- a/lib/perron/site/builder/feeds/rss.rb
+++ b/lib/perron/site/builder/feeds/rss.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require "nokogiri"
require "perron/site/builder/feeds/author"
+require "perron/site/builder/feeds/template"
module Perron
module Site
@@ -9,6 +9,7 @@ class Builder
class Feeds
class Rss
include Feeds::Author
+ include Feeds::Template
def initialize(collection:)
@collection = collection
@@ -18,35 +19,10 @@ def initialize(collection:)
def generate
return if resources.empty?
- Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
- xml.rss(:version => "2.0", "xmlns:atom" => "http://www.w3.org/2005/Atom") do
- xml.channel do
- xml.generator "Perron (#{Perron::VERSION})"
- xml.title feed_configuration.title.presence || @configuration.site_name
- xml.description feed_configuration.description.presence || @configuration.site_description
- xml.link @configuration.url
+ template = find_template("rss")
+ return unless template
- resources.each do |resource|
- xml.item do
- xml.guid resource.id, isPermaLink: false
-
- if (resource_url = url_for_resource(resource))
- xml.link resource_url
- end
-
- xml.pubDate(resource.published_at&.rfc822)
-
- if (author = author(resource)) && author.email
- xml.author author.name ? "#{author.email} (#{author.name})" : author.email
- end
-
- xml.title resource.metadata.title
- xml.description { xml.cdata(Perron::Markdown.render(resource.content)) }
- end
- end
- end
- end
- end.to_xml
+ render(template, feed_configuration)
end
private
@@ -58,18 +34,6 @@ def resources
.reverse
.take(feed_configuration.max_items)
end
-
- def url_for_resource(resource)
- routes
- .polymorphic_url(resource, **@configuration.default_url_options.merge(ref: feed_configuration.ref))
- .delete_suffix("?ref=")
- rescue
- nil
- end
-
- def feed_configuration = @collection.configuration.feeds.rss
-
- def routes = Rails.application.routes.url_helpers
end
end
end
diff --git a/lib/perron/site/builder/feeds/template.rb b/lib/perron/site/builder/feeds/template.rb
new file mode 100644
index 0000000..6497b67
--- /dev/null
+++ b/lib/perron/site/builder/feeds/template.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module Perron
+ module Site
+ class Builder
+ class Feeds
+ module Template
+ def find_template(type)
+ collection_name = @collection.name.to_s.pluralize
+
+ user_path = Rails.root.join("app/views/content/#{collection_name}/#{type}.erb")
+ return user_path if File.exist?(user_path)
+
+ default_path = Pathname.new(__dir__).join("#{type}.erb")
+ return default_path if File.exist?(default_path)
+
+ nil
+ end
+
+ def render(template_path, feed_config)
+ template = File.read(template_path)
+ b = binding
+
+ b.local_variable_set(:collection, @collection)
+ b.local_variable_set(:resources, resources)
+ b.local_variable_set(:config, feed_config)
+ b.local_variable_set(:routes, routes)
+ b.local_variable_set(:author, method(:author))
+ b.local_variable_set(:url_for_resource, method(:url_for_resource))
+ b.local_variable_set(:current_feed_url, method(:current_feed_url))
+
+ ERB.new(template).result(b)
+ end
+
+ def url_for_resource(resource)
+ routes
+ .polymorphic_url(resource, **@configuration.default_url_options.merge(ref: feed_configuration.ref))
+ .delete_suffix("?ref=")
+ rescue
+ nil
+ end
+
+ def current_feed_url
+ path = feed_configuration.path || "feed.atom"
+ URI.join(@configuration.url, path).to_s
+ end
+
+ def routes
+ Rails.application.routes.url_helpers
+ end
+
+ def feed_configuration
+ case self.class.name.demodulize
+ when "Rss" then @collection.configuration.feeds.rss
+ when "Atom" then @collection.configuration.feeds.atom
+ when "Json" then @collection.configuration.feeds.json
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/test/perron/site/builder/feeds_test.rb b/test/perron/site/builder/feeds_test.rb
index de64dc4..5d57f2c 100644
--- a/test/perron/site/builder/feeds_test.rb
+++ b/test/perron/site/builder/feeds_test.rb
@@ -10,14 +10,28 @@ class Perron::Site::Builder::FeedsTest < ActiveSupport::TestCase
FileUtils.rm_rf(@output_path)
FileUtils.mkdir_p(@output_path)
+
+ Content::Post.configure do |config|
+ config.feeds.rss.enabled = false
+ config.feeds.atom.enabled = false
+ config.feeds.json.enabled = false
+ end
end
teardown do
FileUtils.rm_rf(@output_path)
+
+ %w[rss.erb atom.erb json.erb].each do |file|
+ path = Rails.root.join("app/views/content/posts/#{file}")
+ FileUtils.rm_f(path)
+ end
end
test "does not instantiate any builders if feeds are disabled" do
- Content::Post.configure { |it| it.feeds.rss.enabled = false; it.feeds.json.enabled = false }
+ Content::Post.configure do |it|
+ it.feeds.rss.enabled = false
+ it.feeds.json.enabled = false
+ end
rss_never_called = -> { flunk "Rss.new should not have been called" }
json_never_called = -> { flunk "Json.new should not have been called" }
@@ -74,4 +88,73 @@ class Perron::Site::Builder::FeedsTest < ActiveSupport::TestCase
json_builder_stub.verify
end
+
+ test "uses custom RSS template when present" do
+ posts = Perron::Site.collection("posts")
+
+ posts.configuration.feeds.rss.enabled = true
+ posts.configuration.feeds.rss.path = "feeds/posts.xml"
+
+ File.write(Rails.root.join("app/views/content/posts/rss.erb"), "Custom RSS: <%= resources.map(&:id).join(',') %>")
+
+ rss = Perron::Site::Builder::Feeds::Rss.new(collection: posts)
+ output = rss.generate
+
+ assert output.start_with?("Custom RSS: ")
+ end
+
+ test "uses custom Atom template when present" do
+ posts = Perron::Site.collection("posts")
+
+ posts.configuration.feeds.atom.enabled = true
+ posts.configuration.feeds.atom.path = "feeds/posts.atom"
+
+ File.write(Rails.root.join("app/views/content/posts/atom.erb"), "Custom Atom: <%= resources.count %>")
+
+ atom = Perron::Site::Builder::Feeds::Atom.new(collection: posts)
+ output = atom.generate
+
+ assert_equal "Custom Atom: 4", output
+ end
+
+ test "uses custom JSON template when present" do
+ posts = Perron::Site.collection("posts")
+
+ posts.configuration.feeds.json.enabled = true
+ posts.configuration.feeds.json.path = "feeds/posts.json"
+
+ File.write(Rails.root.join("app/views/content/posts/json.erb"), '{"custom": true, "items": <%= resources.count %>}')
+
+ json = Perron::Site::Builder::Feeds::Json.new(collection: posts)
+ output = json.generate
+
+ assert_equal '{"custom": true, "items": 4}', output
+ end
+
+ test "template has access to collection, resources, and config" do
+ posts = Perron::Site.collection("posts")
+
+ posts.configuration.feeds.rss.enabled = true
+
+ File.write(
+ Rails.root.join("app/views/content/posts/rss.erb"),
+ "<%= collection.name %>:<%= resources.count %>:<%= config.title || 'default' %>"
+ )
+
+ rss = Perron::Site::Builder::Feeds::Rss.new(collection: posts)
+ output = rss.generate
+
+ assert_equal "posts:4:default", output
+ end
+
+ test "falls back to default generation when no custom template exists" do
+ posts = Perron::Site.collection('posts')
+
+ posts.configuration.feeds.rss.enabled = true
+
+ rss = Perron::Site::Builder::Feeds::Rss.new(collection: posts)
+ output = rss.generate
+
+ assert output.start_with?("