<%= sanitize(feed[:content]) %>
+diff --git a/README.md b/README.md
index 5729f6c..e122627 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,7 @@ community Recipes:
- [Nano Banana Dashboard](/lib/nano_banana_dashboard)
- [Notion](/lib/notion)
- [Parcel](/lib/parcel)
+- [Rss Feed](/lib/rss_feed)
- [Shopify](/lib/shopify)
- [Stock Price](/lib/stock_price)
- [Tempest Weather Station](/lib/tempest_weather_station)
diff --git a/lib/rss_feed/rss_feed.rb b/lib/rss_feed/rss_feed.rb
new file mode 100644
index 0000000..5fbce6c
--- /dev/null
+++ b/lib/rss_feed/rss_feed.rb
@@ -0,0 +1,100 @@
+module Plugins
+ class RssFeed < Base
+
+ def locals
+ { rss_feed: }
+ end
+
+ private
+
+ def rss_feed
+ feed = process_feed
+ feed.is_a?(Array) ? feed : [feed]
+ end
+
+ def process_feed
+ validate_uri(settings['url'])
+ response = fetch(settings['url'].strip, headers:)
+
+ raise InvalidURL unless response.respond_to?(:code) && response.respond_to?(:body)
+ raise InvalidURL if response.code.to_i >= 400
+ return content_error(StandardError.new("Response Code: #{response.code}")) if response.code != 200
+ return content_error(StandardError.new("No Content")) if response.body.nil? || response.body.empty?
+
+ feed = RSS::Parser.parse(response.body, validate: false)
+
+ if layout == 'featured'
+ normalize_feed(feed&.items&.first)
+ elsif layout == 'list'
+ feed&.items&.[](0..7)&.map do |post|
+ normalize_feed(post, strip_to_text: true)
+ end
+ end
+ rescue OpenURI::HTTPError, RSS::NotWellFormedError, RSS::TooMuchTagError => e
+ content_error(e)
+ rescue InvalidURL, URI::InvalidURIError, OpenSSL::SSL::SSLError => e
+ handle_erroring_state(e)
+ content_error(e)
+ end
+
+ def normalize_feed(post, strip_to_text: false)
+ content = if post.is_a?(RSS::Atom::Feed::Entry)
+ {
+ title: post&.title&.content,
+ content: post&.summary&.content || post&.content&.to_s
+ }.compact
+ else
+ {
+ title: post&.title,
+ content: post&.content_encoded || post&.description
+ }.compact
+ end
+ return content_error(StandardError.new("No Content")) if content.empty?
+
+ if strip_to_text
+ strip_html(content)
+ else
+ dither_image(content)
+ end
+ end
+
+ def validate_uri(url)
+ raise InvalidURL unless url =~ URI::DEFAULT_PARSER.make_regexp
+ end
+
+ # added 2025-01-08 per customer observation that some rss feeds prevent fetches without user-agent present
+ def headers
+ {
+ 'user-agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9'
+ }
+ end
+
+ def dither_image(feed)
+ content = feed[:content]
+ return feed if content.nil?
+
+ content.gsub!(%r{}, '
')
+ feed[:content] = content
+ feed
+ end
+
+ # Make content in the list view plaintext so we don't overflow.
+ def strip_html(feed)
+ content = feed[:content]
+ return feed if content.nil?
+
+ plain_text = ActionController::Base.helpers.strip_tags(content)
+ feed[:content] = plain_text.squish.truncate(200)
+ feed
+ end
+
+ def content_error(error)
+ {
+ title: 'Error while parsing RSS content',
+ content: "Message: #{error&.message}"
+ }
+ end
+
+ def layout = settings['layout'] || 'featured'
+ end
+end
diff --git a/lib/rss_feed/views/full.html.erb b/lib/rss_feed/views/full.html.erb
new file mode 100644
index 0000000..bb2601a
--- /dev/null
+++ b/lib/rss_feed/views/full.html.erb
@@ -0,0 +1,7 @@
+
<%= sanitize(feed[:content]) %>
+