RSS către 66 de rețele sociale: Facebook, Instagram, X, LinkedIn, Telegram și altele Blog Afiliere Contact
Autentificare Începeți gratuit
Updated: 2026-10-06
Phoenix RSS Feed in Elixir: Build It and Auto-Post Updates

Short answer: Phoenix has no built-in feed generator, but a Phoenix RSS feed is one route and one small controller. Query your latest published posts with Ecto, build the XML with a library such as xml_builder so every value is escaped, set the content type to application/rss+xml and send the result. Configure the endpoint’s public URL so links are absolute, add a short cache header, and the feed is ready for readers and automatic social posting.

Phoenix is the leading web framework for Elixir and is used for real-time applications, SaaS products, content platforms and a growing number of blogs and documentation sites. Its LiveView pages are fast and interactive, but feed readers and automation tools do not run JavaScript or open WebSockets: they read plain XML. An RSS feed gives them a simple, stable way to see what is new. This guide shows how to build one cleanly, which Phoenix-specific settings most often break it, and how to use the feed for social sharing.

What the feed must provide

Agree on the target first. A feed that serves readers and auto-posting tools well:

  • lists the most recent public items, newest first, typically 20 to 50;
  • gives every item a title, an absolute link, a short description, a publication date and a permanent guid;
  • has a channel title, a link to the site and a description;
  • is served in UTF-8 with an RSS or XML content type and no redirects;
  • keeps IDs stable, so an old item is never reported as new.

Images and categories come later. For a quick reference to each element, see what guid and pubDate actually do.

Step 1: add a route outside the browser pipeline

The default :browser pipeline in a Phoenix 1.7 project includes plug :accepts, ["html"], session handling, CSRF protection and secure browser headers. None of that is needed for a public feed, and the accepts plug can reject requests from readers that send an Accept: application/rss+xml header. Put the feed in its own scope:

# lib/my_app_web/router.ex
scope "/", MyAppWeb do
  get "/feed.xml", FeedController, :index
end

Keeping the route free of session plugs also means responses carry no cookies, which makes them easy for proxies and CDNs to cache.

Step 2: query the posts with Ecto

Add a function to your content context that returns the latest published posts:

# lib/my_app/blog.ex
def list_feed_posts(limit \\ 30) do
  from(p in Post,
    where: p.published and p.published_at <= ^DateTime.utc_now(),
    order_by: [desc: p.published_at],
    limit: ^limit
  )
  |> Repo.all()
end

The published_at condition matters if you schedule posts in advance: without it, a post prepared for next week appears in the feed today, and tools may share a link that still returns 404.

Step 3: build the XML safely

You could write the XML in an EEx template, but escaping then becomes your job, and one unescaped ampersand in a title makes the whole feed invalid. The xml_builder package builds the document from Elixir data and escapes text for you. Add {:xml_builder, "~> 2.2"} to mix.exs and write the controller:

defmodule MyAppWeb.FeedController do
  use MyAppWeb, :controller
  import XmlBuilder
  alias MyApp.Blog

  def index(conn, _params) do
    posts = Blog.list_feed_posts()

    items =
      Enum.map(posts, fn p ->
        link = url(~p"/blog/#{p.slug}")
        element(:item, [
          element(:title, p.title),
          element(:link, link),
          element(:guid, %{isPermaLink: "true"}, link),
          element(:pubDate, rfc822(p.published_at)),
          element(:description, p.summary)
        ])
      end)

    xml =
      document(:rss, %{version: "2.0", "xmlns:atom": "http://www.w3.org/2005/Atom"}, [
        element(:channel, [
          element(:title, "Example Blog"),
          element(:link, url(~p"/")),
          element(:description, "New articles from Example Blog"),
          element(:"atom:link", %{href: url(~p"/feed.xml"), rel: "self", type: "application/rss+xml"})
          | items
        ])
      ])
      |> generate()

    conn
    |> put_resp_content_type("application/rss+xml")
    |> put_resp_header("cache-control", "public, max-age=300")
    |> send_resp(200, xml)
  end

  defp rfc822(%DateTime{} = dt),
    do: dt |> DateTime.shift_zone!("Etc/UTC") |> Calendar.strftime("%a, %d %b %Y %H:%M:%S +0000")
end

A few notes on that code. put_resp_content_type/2 adds charset=utf-8 automatically. Calendar.strftime/2 uses English day and month abbreviations by default, which is exactly what the RSS date format expects. And url/1 with a verified route builds an absolute URL from your endpoint configuration, which brings us to the most common Phoenix feed bug.

Configure the public URL of the endpoint

Phoenix builds absolute URLs from the endpoint’s :url configuration, not from the incoming request. In production, that configuration usually lives in config/runtime.exs:

config :my_app, MyAppWeb.Endpoint,
  url: [host: "example.com", port: 443, scheme: "https"],
  http: [ip: {0, 0, 0, 0}, port: 4000]

If url is missing or still says localhost, every link in the feed points to the wrong place, often with the internal port 4000 attached. The app behaves perfectly in the browser, because pages use relative paths, but every auto-posted link is broken. The :http port is where Phoenix listens; the :url port is what the public sees. Behind a reverse proxy those are different, and only :url belongs in feed links.

After deploying, open the live feed and check the first <link>. It should match the address in your browser exactly. More about this class of problem is in relative URLs in RSS feeds.

Images, categories and full content

When the feed drives social posts, images matter most. Add an enclosure for each item that has a cover image:

element(:enclosure, %{url: p.cover_url, type: "image/jpeg", length: p.cover_bytes})

Many auto-posters also read the article page’s og:image tag, so set it in your root layout from the post’s cover. Categories become <category> elements, one per tag, which helps route topics to different accounts and turn tags into hashtags. If you want to include full HTML, add the content namespace and a content:encoded element, and keep the description short and plain. See how to control which image gets auto-posted for image tips.

Caching and conditional requests

Feeds are polled often. A few lines keep the endpoint cheap:

  • Cache-Control. The max-age=300 header above lets proxies reuse the response for five minutes. Avoid hour-long values; they delay every reader and auto-poster.
  • ETag. Compute a value from the newest post, for example its ID and updated_at. If the request’s if-none-match header matches, return send_resp(conn, 304, "") without building the XML.
  • Application cache. If the query or rendering is expensive, cache the generated XML in ETS or Cachex and clear it when a post is published.

Long CDN caches are one of the most common reasons auto-posts lag behind publishing; see RSS feed caching explained.

Autodiscovery and category feeds

Add a discovery link to the head of your root layout so browsers and tools can find the feed from any page:

<link rel="alternate" type="application/rss+xml"
      title="Example Blog" href={url(~p"/feed.xml")} />

For topic feeds, add get "/category/:slug/feed.xml", FeedController, :category and reuse the same builder with a filtered query. Return a 404 for unknown categories so a mistyped feed address is noticed immediately instead of silently producing an empty feed.

LiveView pages and the feed

Many Phoenix sites render their blog with LiveView. That is fine for visitors, but it changes nothing for the feed, and it is worth understanding why. A LiveView page is first rendered as normal HTML on the server, then connects over a WebSocket for interactivity. Feed readers and auto-posters never open that socket. They request the feed, read the XML, and then usually request the article page once to look at its Open Graph tags.

Two consequences follow. First, everything a sharing tool needs must be present in the first, static render: the title, the og:image, the og:description and the canonical URL. Assign them in mount/3 or in the controller that renders the layout, not in an event handler that runs only after the socket connects. Second, the feed itself should be a plain controller, as shown above, rather than a LiveView. It needs no interactivity, and a controller is faster, easier to cache and simpler to test.

A quick check is to fetch an article with curl and search the output for og:image. If it is missing there, social networks will not see it either, no matter how the page looks in a browser.

Test and validate

Phoenix’s ConnTest makes a short, useful test:

test "serves the RSS feed", %{conn: conn} do
  conn = get(conn, ~p"/feed.xml")
  assert conn.status == 200
  assert hd(get_resp_header(conn, "content-type")) =~ "application/rss+xml"
  assert response(conn, 200) =~ "<rss version="
  assert response(conn, 200) =~ "<link>https://"
end

In the test environment, set the endpoint URL to an https host so the last assertion reflects production. Then validate the deployed feed once with the W3C Feed Validation Service, and keep the Phoenix routing guide at hand for verified routes and URL helpers.

Common Phoenix feed problems

SymptomCauseFix
Links show localhost or port 4000Endpoint :url not set for productionSet host, scheme and port 443 in runtime.exs
406 Not Acceptable for some readersRoute inside a pipeline with accepts ["html"]Move the feed to a scope without that plug
Feed invalid after one new postUnescaped characters in a hand-written templateBuild XML with xml_builder
Scheduled posts appear earlyQuery ignores published_atFilter on the current UTC time
Dates rejected by validatorsISO 8601 or NaiveDateTime valuesUTC DateTime and an RFC 822 format
Old posts shared againSlugs or domain changed, so guids changedKeep guids permanent; redirect old URLs

The guid issue is worth repeating. If you change the URL structure or move to a new domain, every tool that watches the feed may treat the whole list as new items. Our article on duplicate guid fixes explains how to avoid it.

Connect the feed to social auto-posting

Elixir makes it tempting to write an Oban job that calls each social network’s API when a post goes live. It works for one network, but every network has its own authentication flow, token refresh, rate limits and media rules, and those change often. For a content site, the feed is a simpler contract: publish the post, let the feed announce it, and let an auto-poster take care of the networks.

  • publish the page before the item can appear in the feed, so shared links always work;
  • write a clear summary, because it often becomes the post text;
  • give each post a cover image of at least 1200 pixels in width;
  • use tags consistently if you want them turned into hashtags.

How PostRSS works with a Phoenix feed

PostRSS has been turning RSS and Atom feeds into social posts since 2014. You paste the feed address, connect accounts and choose the post format: title, description, page title or your own text. PostRSS checks the feed as often as every minute and publishes each new item, with its image and link, to the networks you choose among 66 supported networks, messengers, team chats and blogs. Keyword filters, UTM parameters, hashtags from categories, posting windows, a REST API v1 and outgoing webhooks are available as well. See the PostRSS features page for everything it does and the pricing page for plan limits. Related developer articles are under the Technical Guide tag.

Related reading

The bottom line

A Phoenix RSS feed needs a route outside the browser pipeline, an Ecto query for published posts, XML built with an escaping library, the right content type and a short cache header. Configure the endpoint’s public URL so links are absolute, keep guids permanent and validate once. From then on, the feed can carry every new post to your social channels automatically.

FAQ

Does Phoenix include an RSS feed generator?

No. Phoenix does not ship a feed module. You add a controller that queries your posts and returns XML, usually built with a library such as xml_builder so that all text is escaped correctly.

Why do my Phoenix feed links point to localhost?

Phoenix builds absolute URLs from the endpoint’s url configuration. Set the public host, the https scheme and port 443 in config/runtime.exs, redeploy, and the feed links will use your real domain.

Can I use a HEEx or EEx template for the feed?

You can, but HEEx is designed for HTML, and with plain EEx you must handle XML escaping yourself. Building the document with an XML library is safer and keeps the controller short.

How do I format pubDate in Elixir?

Convert the value to a UTC DateTime and format it with Calendar.strftime using day, date, month, year, time and +0000. This produces the RFC 822 style that RSS readers and validators expect.

Can a Phoenix site auto-post new articles to social media?

Yes. Once the feed is public and valid, an RSS auto-poster such as PostRSS can read it and publish each new article to the networks you connect, without extra jobs in your Phoenix app.

New guides, once a month

What changed in the networks, what broke, and how to fix it before it costs you reach.

We send a confirmation e-mail first. Unsubscribe any time.

Mai multe instrumente de la echipa noastră

Create de Internet Solutions, echipa din spatele PostRSS. Fiecare produs vă economisește timp în felul său.

PostRSS - automatizarea fluxurilor RSS și auto-postare
Prezentare Confidențialitate

Acest site utilizează cookie-uri pentru a vă putea oferi cea mai bună experiență de utilizare posibilă. Informațiile despre cookie-uri sunt stocate în browserul dumneavoastră și îndeplinesc funcții precum recunoașterea dumneavoastră atunci când reveniți pe site-ul nostru și ajutorarea echipei noastre de a înțelege care secțiuni ale site-ului sunt cele mai interesante și utile pentru dumneavoastră.