Your Name bc14696f57 Static blog with front page summary
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.
2026-03-17 11:17:21 +00:00

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("&", "&amp;")
|> String.replace("<", "&lt;")
|> String.replace(">", "&gt;")
|> String.replace("\"", "&quot;")
|> String.replace("'", "&apos;")
end
end