RSS uz 66 sociālajiem tīkliem: Facebook, Instagram, X, LinkedIn, Telegram un citiem Blogs Partner programma Kontakti
Pieslēgties Sākt bez maksas
Updated: 2026-10-19
Sinatra RSS Feed: Build It With Builder and Auto-Post

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.

When a Sinatra app needs a feed

A feed is worth adding whenever your app publishes things that people want to follow:

  • news, announcements or short articles stored in a database;
  • release notes and changelogs for a tool or library;
  • listings, events or offers added by staff;
  • any content that should also reach people who never visit the app.

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.

Set up the project

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.

The route

# 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
end

The Builder template

Save 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
end

Several 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.

Where each field comes from

RSS elementSource in the appRule
titlepost titleEscaped by Builder, under about 100 characters
linksite address plus slugAlways absolute and always the public address
guiddatabase identifierNever changes, even if the slug changes
pubDatepublication timeUTC, RFC 822 style, never in the future
descriptionsummaryPlain 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.

ETags, caching and speed

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.

Run it and put a proxy in front

For production, add a rackup file and start the app under Puma:

# config.ru
require './app'
run Sinatra::Application
SITE_URL=https://www.example.com DB_PATH=/var/lib/site/site.db bundle exec puma -p 4567

Run 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.

Images and richer items

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.

Test the feed before connecting it

  1. Request the feed with 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.
  2. Repeat the request with the ETag value in an If-None-Match header and confirm that the answer is 304.
  3. Open the file in a browser or editor and check that the newest post is first.
  4. Run it through the W3C Feed Validation Service and fix every error.
  5. Add a draft and a post dated in the future, and confirm that neither appears.

If anything looks wrong later, the checklist in RSS feed not updating helps you find the cause quickly.

Sinatra compared with Rails

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.

Common mistakes to avoid

  • Including drafts or future posts. Filter on status and date in the query itself.
  • Building XML with string interpolation. One raw ampersand can invalidate the whole file. Use Builder.
  • Using rfc2822 for dates. For UTC it can write an unusual zone. Use httpdate for a clean GMT value.
  • Slug-based GUIDs. Renaming a post then creates a duplicate share.
  • Using the request host in links. Behind a proxy, use the fixed SITE_URL setting.
  • Opening the database on every request. Open it once at start-up and reuse the connection, as the example does.

How PostRSS works with a Sinatra feed

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.

Saistīta lasāmviela

The bottom line

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.

FAQ

Does Sinatra have a built-in RSS generator?

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.

Why use Builder instead of string templates?

String templates break when a title contains an ampersand or another special character. Builder escapes values and writes a valid document every time.

What does the etag helper do?

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.

How many items should the feed contain?

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.

How do I stop old posts from being shared again?

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.

Can I use this feed with PostRSS?

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.

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.

Vairāk mūsu komandas rīku

Izstrādājusi Internet Solutions — PostRSS komanda. Katrs produkts ietaupa laiku savā veidā.