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

  1. Gather all posts once during build_blog/1.
  2. Render normal blog HTML pages as before.
  3. After pages are written, evaluate a new EEx template rss.xml.eex that loops over posts to generate <item> entries.
  4. Write the result to output/rss.xml.
  5. Inject <link rel="alternate" type="application/rss+xml" ...> in root.html.eex.
  6. 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:

  • CDATA ensures 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