Overview
Static sites benefit from an RSS feed so readers can subscribe in a feed reader and get new posts automatically. This post walks through adding an RSS 2.0 feed to this project with minimal code, re‑using the existing build pipeline built around mix build, EEx templates, and the in‑memory list of blog posts.
High-Level Approach
- Gather all posts once during 
build_blog/1. - Render normal blog HTML pages as before.
 - After pages are written, evaluate a new EEx template 
rss.xml.eexthat loops over posts to generate<item>entries. - Write the result to 
output/rss.xml. - Inject 
<link rel="alternate" type="application/rss+xml" ...>inroot.html.eex. - Provide a small date formatting helper returning RFC‑822 (e.g. 
Mon, 14 Oct 2024 00:00:00 GMT). 
RSS Feed XML Template
The template lives with the other templates:
// lib/sour_shark/templates/rss.xml.eex
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
  <channel>
    <title><%= Application.get_env(:sour_shark, :product_name) %></title>
    <link>https://jack-develops.com/</link>
    <description>Updates and posts from <%= Application.get_env(:sour_shark, :product_name) %></description>
    <language>en-us</language>
    <lastBuildDate><%= @last_build_date %></lastBuildDate>
    <ttl>1800</ttl>
    <%= for post <- @posts do %>
      <item>
        <title><![CDATA[<%= post.title %>]]></title>
        <link>https://jack-develops.com/<%= post.path %></link>
        <guid isPermaLink="true">https://jack-develops.com/<%= post.path %></guid>
        <pubDate><%= @format_date.(post.date) %></pubDate>
        <description><![CDATA[<%= post.description %>]]></description>
        <%= if Map.has_key?(post, :tags) and post.tags do %>
          <%= for tag <- post.tags do %>
            <category><![CDATA[<%= tag %>]]></category>
          <% end %>
        <% end %>
      </item>
    <% end %>
  </channel>
</rss>
Notes:
CDATAensures characters inside titles & descriptions don't break XML.- Each 
<guid>is the canonical page URL (permalink). - Optional 
<category>tags come from post tag list. 
Build Changes
The existing build_blog/1 was extended:
# lib/mix/tasks/build.ex
# Build posts once, reuse for HTML & feed
posts = SourShark.Blog.build_posts() |> Enum.sort_by(& &1.date, {:desc, Date})
for post <- posts do
  # (unchanged) write each post page
end
IO.puts("Building rss.xml")
last_build_date =
  case Enum.find(posts, & &1.date) do
    %_{date: date} when not is_nil(date) -> format_rss_date(date)
    _ -> format_rss_date(Date.utc_today())
  end
rss_xml =
  eval_file("lib/sour_shark/templates/rss.xml.eex",
    assigns: [
      posts: posts,
      last_build_date: last_build_date,
      format_date: &format_rss_date/1
    ]
  )
File.write!("#{@output_dir}/rss.xml", rss_xml)
Two important behaviors:
- We only build the posts list once (prevents duplicate parsing work).
 - The feed build happens after all individual post files.
 
Date Formatting Helper
Instead of an anonymous function inside build_blog/1, a private helper improves reuse & clarity:
# date formatting helper functions
defp format_rss_date(%Date{} = date) do
  {:ok, dt} = DateTime.new(date, ~T[00:00:00], "Etc/UTC")
  Calendar.strftime(dt, "%a, %d %b %Y %H:%M:%S GMT")
end
defp format_rss_date(%DateTime{} = dt), do: Calendar.strftime(dt, "%a, %d %b %Y %H:%M:%S GMT")
defp format_rss_date(%NaiveDateTime{} = ndt) do
  {:ok, dt} = DateTime.from_naive(ndt, "Etc/UTC")
  Calendar.strftime(dt, "%a, %d %b %Y %H:%M:%S GMT")
end
defp format_rss_date(binary) when is_binary(binary), do: binary
Handling multiple types guards against future changes in how date might be produced.
Linking the Feed
Inserted into root.html.eex <head>:
<link rel="alternate" type="application/rss+xml" title="RSS" href="/rss.xml" />
This allows browsers and feed readers to auto-discover the feed.
Verification
After running:
mix build
You should see output/rss.xml.
Once the site was deployed I then checked if the RSS feed was valid by going to: https://www.rssboard.org/rss-validator/check.cgi?url=https%3A//jack-develops.com/rss.xml