2023-08-18
2023-08-18

I like being able to subscribe to random websites using rss/atom feeds. I self host a miniflux instance, where I manage my subscriptions and read articles. So naturally I wanted to implement such a feed also for thepid.de. I mean how hard can that be?

Basically it involves two steps

  • Implementing a function, which builds the feed
  • Serving the feed as raw xml

The first step was straightforward as soon as I stumbled upon the Atomex library. I just had to copy the example from the docs and make some adoptions regarding my schema.

  alias Atomex.Entry
  alias Atomex.Feed

  def build_feed(posts) do
    "https://thepid.de"
    |> Feed.new(DateTime.utc_now(), "ThePid Feed")
    |> Feed.author("THE", email: "thepid@mailbox.org")
    |> Feed.link("https://thepid.de/feed", rel: "self")
    |> Feed.entries(Enum.map(posts, &get_entry/1))
    |> Feed.build()
    |> Atomex.generate_document()
  end

  defp get_entry(post) do
    inserted_at = DateTime.from_naive!(post.inserted_at, "Europe/Berlin", Tz.TimeZoneDatabase)
    updated_at = DateTime.from_naive!(post.updated_at, "Europe/Berlin", Tz.TimeZoneDatabase)
    content = Earmark.as_html!(post.content, %Earmark.Options{code_class_prefix: "language-", smartypants: false})

    "https://thepid.de/blog/#{post.id}"
    |> Entry.new(updated_at, "#{post.title}")
    |> Entry.content(content, type: "html")
    |> Entry.published(inserted_at)
    |> Entry.build()
  end

The build_feed function builds the feed with entries, created through the get_entry function. I decided to include the whole article as html in the feed, so you can read everything through your reader application. Unfortunately this is not possible with some feeds out there.

Now we have to serve the generated document at the /feed route. Therefor just add this line to your router.ex

  scope "/", ThePidWeb do
    pipe_through :browser

    get "/feed", FeedController, :index
  end

The router will try to hit the feed controller with an index function.

defmodule ThePidWeb.FeedController do
  use ThePidWeb, :controller

  plug :put_layout, false
  plug :put_root_layout, false

  def index(conn, _assigns) do
    conn
    |> put_resp_content_type("text/xml")
    |> render(:feed)
  end
end

Here I specified the response type as text/xml and removed all layouts. Finally the render functions expects a view with a feed function. This is were the build_feed function is finally called.

defmodule ThePidWeb.FeedHTML do
  use ThePidWeb, :html

  alias ThePid.Blog

  def feed(_assigns) do
    Blog.get_all_posts()
    |> Blog.build_feed()
    |> Phoenix.HTML.raw()
  end
end

That's it. Easy peasy. Subscribe now.