
Short answer: To serve an RSS feed from a Sinatra app, add a get route for /feed.xml, query the newest published items, render the XML with a Builder template so every value is escaped for you, format dates with httpdate, and send the content type application/rss+xml with a short cache header and an ETag. Run the app under Puma behind a reverse proxy, test the feed with curl and a validator, and then give its public address to an auto-poster so every new item reaches your social channels.
Sinatra is the small Ruby framework for people who want a few routes and nothing else. Teams use it for internal tools, small APIs, landing pages and micro-sites, and sooner or later such an app publishes something that outsiders want to follow: announcements, release notes, listings or short articles. The content sits in a database and is shown in a page, but nothing machine-readable exposes it. A feed fixes that without an account or an API key. This guide builds a complete Sinatra RSS feed, explains each decision and ends with the checks that keep it dependable.
A feed is worth adding whenever your app publishes things that people want to follow:
Without a feed, every announcement has to be copied to social networks by hand. If your content lives in a blog or a CMS instead, use the feed it already provides; see how to find your WordPress feed link. For the same job in the larger Ruby framework, see the Rails guide.
Create a folder with a Gemfile that lists the gems used here. Sinatra provides the routes, Builder writes the XML, SQLite3 reads the example database and Puma serves the app. Replace the SQLite gem with the driver for your own database.
source 'https://rubygems.org'
gem 'sinatra'
gem 'rackup'
gem 'puma'
gem 'builder'
gem 'sqlite3'Run bundle install to fetch them. The example uses two files, one for the route and one for the template, which keeps the route short and the XML easy to read.
# app.rb
require 'sinatra'
require 'sqlite3'
require 'digest'
require 'time'
SITE_URL = ENV.fetch('SITE_URL', 'https://www.example.com')
DB = SQLite3::Database.new(ENV.fetch('DB_PATH', 'data/site.db'), readonly: true)
DB.results_as_hash = true
helpers do
def latest_posts(limit = 20)
DB.execute(<<~SQL, [Time.now.to_i, limit])
SELECT id, title, slug, summary, published_at
FROM posts
WHERE status = 'published' AND published_at <= ?
ORDER BY published_at DESC
LIMIT ?
SQL
end
end
get '/feed.xml' do
@posts = latest_posts
stamp = @posts.map { |p| "#{p['id']}-#{p['published_at']}-#{p['title']}" }.join('|')
etag Digest::SHA1.hexdigest(stamp)
content_type 'application/rss+xml', charset: 'utf-8'
cache_control :public, max_age: 300
builder :feed
endSave this as views/feed.builder. Sinatra gives the template an object called xml, and every method call on it writes an element.
xml.instruct! :xml, version: '1.0', encoding: 'UTF-8'
xml.rss version: '2.0', 'xmlns:atom' => 'http://www.w3.org/2005/Atom' do
xml.channel do
xml.title 'Example News'
xml.link "#{SITE_URL}/news/"
xml.description 'Latest news from Example'
xml.language 'en'
newest = @posts.first ? Time.at(@posts.first['published_at']) : Time.at(0)
xml.lastBuildDate newest.utc.httpdate
xml.tag! 'atom:link', href: "#{SITE_URL}/feed.xml", rel: 'self', type: 'application/rss+xml'
@posts.each do |post|
xml.item do
xml.title post['title']
xml.link "#{SITE_URL}/news/#{post['slug']}/"
xml.guid "post-#{post['id']}", isPermaLink: 'false'
xml.pubDate Time.at(post['published_at']).utc.httpdate
xml.description post['summary']
end
end
end
endSeveral details carry most of the correctness. The query filters on both the status and the date, so drafts and items dated in the future never appear. Builder escapes every text value and attribute, so an ampersand in a title can no longer break the file. The httpdate method writes English UTC dates such as Mon, 19 Oct 2026 07:40:00 GMT, which is what the RSS specification asks for, whatever the locale of the server. The GUID is marked with isPermaLink set to false because it is an identifier and not an address. The database is opened once, when the app starts, and reused for every request.
| RSS element | Source in the app | Rule |
|---|---|---|
| title | post title | Escaped by Builder, under about 100 characters |
| link | site address plus slug | Always absolute and always the public address |
| guid | database identifier | Never changes, even if the slug changes |
| pubDate | publication time | UTC, RFC 822 style, never in the future |
| description | summary | Plain text; the first sentences matter most |
The GUID deserves special attention. Use the database identifier and not the slug, because slugs get edited. A new GUID makes an old item look new, and an auto-poster may share it again; see why your RSS feed auto-posts the same article twice. Date mistakes are another common cause of trouble; see RSS date format errors. If your summaries contain HTML, escape them as shown, or wrap them in CDATA as described in CDATA and HTML in RSS descriptions.
Auto-posters and readers request the same feed again and again, and most requests find nothing new. Sinatra has a helper for exactly this. When you call etag with a value, it sends the value as the ETag header and, if the client sent the same value in If-None-Match, it stops the request at once with a 304 answer and no body. The example builds the value from the identifiers, dates and titles of the items, so it changes whenever the visible content changes. Put the call before the expensive part, as shown, so a repeat request skips the template. The mechanism is explained in RSS feed ETags and conditional GET.
The cache_control helper adds a header that lets a proxy or a content delivery network serve the same copy for 300 seconds. Do not cache for hours: a long lifetime delays every share by the same amount, as explained in RSS feed caching explained. Keep an index on the status and published columns so the query stays fast as the table grows. A slow feed is a real problem, because auto-posters give up after a timeout and may skip a check; see RSS feed timeouts.
For production, add a rackup file and start the app under Puma:
# config.ru
require './app'
run Sinatra::ApplicationSITE_URL=https://www.example.com DB_PATH=/var/lib/site/site.db bundle exec puma -p 4567Run Puma under a service manager so that it restarts after a crash or a reboot, and put a reverse proxy such as Nginx or Caddy in front of it. The proxy terminates the secure connection, forwards the path /feed.xml and can compress the answer; see RSS feed compression. Build links from the fixed SITE_URL setting and not from the request, because behind a proxy the host of the request can be an internal name that would leak into every link. Make sure the proxy passes the content type through unchanged; see the RSS content type header.
Auto-posters build the preview card from the image of the item, so add one when you can. Store the full public address of the picture with the post and add an enclosure element inside each item with its address, size and real type. Always use absolute addresses, because a relative one breaks when the feed is read elsewhere; see relative URLs in RSS feeds. The way these tags are handled is explained in RSS enclosure tags explained. Also make sure each article page carries correct Open Graph tags, because many networks use them for the card.
curl -i https://www.example.com/feed.xml and check for a 200 status, the XML content type, the ETag and the cache header. The method is explained in debugging an RSS feed with curl.If anything looks wrong later, the checklist in RSS feed not updating helps you find the cause quickly.
The feed logic is the same in both frameworks, and so is the Builder template. The difference is how much comes ready-made. Rails offers a respond_to block, a database layer and view conventions, while Sinatra leaves each choice to you, which is why this example sets the content type, the ETag and the cache header by hand. That is not extra work, because each is a single line. If your app uses the modular style with a class that inherits from Sinatra::Base, move the route and the helper into that class and keep the template in the views folder. The feed address, the GUID rules and the checks stay exactly the same.
PostRSS has been publishing RSS and Atom feeds to social networks since 2014. Paste the address of your feed.xml route, connect your accounts and choose how each post is built from the title, the description or the page title. PostRSS checks the feed as often as every minute and shares each new item, with its image and link, to the channels you choose among 66 supported networks, messengers, team chats and blogs, including LinkedIn, X, Bluesky, Mastodon and Telegram. Keyword filters, UTM parameters and posting windows let you tune each channel, and webhooks connect PostRSS to your own systems. See the PostRSS features page and the plan limits on the pricing page. More developer guides are under the Technical Guide tag.
Sinatra needs only one route and one template to serve a dependable RSS feed: select published items dated in the past, let Builder escape every value, use a stable GUID, format dates in English UTC with httpdate, build links from a configured site address and return the XML with the right content type, an ETag and a short cache. Run it under Puma behind a proxy, test with curl and a validator, keep the address unchanged and connect it to an auto-poster. After that, every item you publish can reach your social channels automatically.
No. Sinatra offers routes and template support, and the Builder gem writes XML cleanly. The route sets the content type and returns the rendered template.
String templates break when a title contains an ampersand or another special character. Builder escapes values and writes a valid document every time.
It sends an ETag header and, when the client already holds the same version, ends the request with a 304 answer. Auto-posters check feeds very often, so this saves bandwidth and processing.
Between ten and twenty of the newest published items is enough. An auto-poster only needs recent items, and a smaller file is faster to download and parse.
Give every item a stable identifier and use it as the GUID. Avoid editing the publication date of items that are already public, because some tools treat a changed date as a new item.
Yes. PostRSS reads any valid RSS or Atom address that is reachable without a login. Paste the address, connect your channels and new items are shared as soon as they appear.
What changed in the networks, what broke, and how to fix it before it costs you reach.
Realizzati da Internet Solutions, il team dietro PostRSS. Ognuno ti fa risparmiare tempo in modo diverso.