Goal: have a personal blog, and try out another point in the 'modular app design with elixir' space. Designing OTP systems with elixir had some interesting ideas.
140 lines
4.2 KiB
Elixir
140 lines
4.2 KiB
Elixir
defmodule Blogex.Feed do
|
|
@moduledoc """
|
|
Generates RSS 2.0 and Atom feeds for a blog.
|
|
|
|
## Usage
|
|
|
|
# In a controller or plug:
|
|
xml = Blogex.Feed.rss(MyApp.EngineeringBlog, "https://myapp.com")
|
|
conn |> put_resp_content_type("application/rss+xml") |> send_resp(200, xml)
|
|
"""
|
|
|
|
@doc """
|
|
Generates an RSS 2.0 XML feed for the given blog module.
|
|
|
|
## Options
|
|
|
|
* `:limit` - max number of posts to include (default: 20)
|
|
* `:language` - feed language (default: "en-us")
|
|
"""
|
|
def rss(blog_module, base_url, opts \\ []) do
|
|
limit = Keyword.get(opts, :limit, 20)
|
|
language = Keyword.get(opts, :language, "en-us")
|
|
posts = Enum.take(blog_module.all_posts(), limit)
|
|
|
|
blog_url = "#{base_url}#{blog_module.base_path()}"
|
|
feed_url = "#{blog_url}/feed.xml"
|
|
|
|
pub_date =
|
|
case posts do
|
|
[latest | _] -> format_rfc822(latest.date)
|
|
[] -> format_rfc822(Date.utc_today())
|
|
end
|
|
|
|
"""
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
|
<channel>
|
|
<title>#{escape(blog_module.title())}</title>
|
|
<link>#{blog_url}</link>
|
|
<description>#{escape(blog_module.description())}</description>
|
|
<language>#{language}</language>
|
|
<pubDate>#{pub_date}</pubDate>
|
|
<atom:link href="#{feed_url}" rel="self" type="application/rss+xml"/>
|
|
#{Enum.map_join(posts, "\n", &item_xml(&1, base_url, blog_module))}
|
|
</channel>
|
|
</rss>
|
|
"""
|
|
|> String.trim()
|
|
end
|
|
|
|
@doc """
|
|
Generates an Atom feed for the given blog module.
|
|
|
|
## Options
|
|
|
|
* `:limit` - max number of posts to include (default: 20)
|
|
"""
|
|
def atom(blog_module, base_url, opts \\ []) do
|
|
limit = Keyword.get(opts, :limit, 20)
|
|
posts = Enum.take(blog_module.all_posts(), limit)
|
|
|
|
blog_url = "#{base_url}#{blog_module.base_path()}"
|
|
feed_url = "#{blog_url}/feed.xml"
|
|
|
|
updated =
|
|
case posts do
|
|
[latest | _] -> format_iso8601(latest.date)
|
|
[] -> format_iso8601(Date.utc_today())
|
|
end
|
|
|
|
"""
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
|
<title>#{escape(blog_module.title())}</title>
|
|
<link href="#{blog_url}" rel="alternate"/>
|
|
<link href="#{feed_url}" rel="self"/>
|
|
<id>#{blog_url}</id>
|
|
<updated>#{updated}</updated>
|
|
#{Enum.map_join(posts, "\n", &entry_xml(&1, base_url, blog_module))}
|
|
</feed>
|
|
"""
|
|
|> String.trim()
|
|
end
|
|
|
|
# Private helpers
|
|
|
|
defp item_xml(post, base_url, blog_module) do
|
|
url = "#{base_url}#{blog_module.base_path()}/#{post.id}"
|
|
|
|
"""
|
|
<item>
|
|
<title>#{escape(post.title)}</title>
|
|
<link>#{url}</link>
|
|
<guid isPermaLink="true">#{url}</guid>
|
|
<pubDate>#{format_rfc822(post.date)}</pubDate>
|
|
<description>#{escape(post.description)}</description>
|
|
<content:encoded><![CDATA[#{post.body}]]></content:encoded>
|
|
#{Enum.map_join(post.tags, "\n", &" <category>#{escape(&1)}</category>")}
|
|
</item>
|
|
"""
|
|
end
|
|
|
|
defp entry_xml(post, base_url, blog_module) do
|
|
url = "#{base_url}#{blog_module.base_path()}/#{post.id}"
|
|
|
|
"""
|
|
<entry>
|
|
<title>#{escape(post.title)}</title>
|
|
<link href="#{url}" rel="alternate"/>
|
|
<id>#{url}</id>
|
|
<published>#{format_iso8601(post.date)}</published>
|
|
<updated>#{format_iso8601(post.date)}</updated>
|
|
<author><name>#{escape(post.author)}</name></author>
|
|
<summary>#{escape(post.description)}</summary>
|
|
<content type="html"><![CDATA[#{post.body}]]></content>
|
|
#{Enum.map_join(post.tags, "\n", &" <category term=\"#{escape(&1)}\"/>")}
|
|
</entry>
|
|
"""
|
|
end
|
|
|
|
defp format_rfc822(date) do
|
|
date
|
|
|> DateTime.new!(~T[00:00:00], "Etc/UTC")
|
|
|> Calendar.strftime("%a, %d %b %Y %H:%M:%S +0000")
|
|
end
|
|
|
|
defp format_iso8601(date) do
|
|
"#{Date.to_iso8601(date)}T00:00:00Z"
|
|
end
|
|
|
|
defp escape(text) when is_binary(text) do
|
|
text
|
|
|> String.replace("&", "&")
|
|
|> String.replace("<", "<")
|
|
|> String.replace(">", ">")
|
|
|> String.replace("\"", """)
|
|
|> String.replace("'", "'")
|
|
end
|
|
end
|