RSS zu 66 sozialen Netzwerken: Facebook, Instagram, X, LinkedIn, Telegram und mehr Blog Affiliate Kontakt
Anmelden Kostenlos starten
Updated: 2026-10-22
Sanic RSS Feed: Async Route, feedgen and Auto-Post

Short answer: To serve an RSS feed from a Sanic application, add an async GET route for /feed.xml, read the newest published rows with an async database driver, describe the channel and its items with the feedgen library, set the last build date from the newest item so the output stays identical between changes, and return the XML with the application/rss+xml content type. Add an ETag computed from the body so repeat requests get a 304 answer, test the route with curl and a validator, then give the public address to an auto-poster so every new item reaches your social channels.

Sanic is an asynchronous Python web framework built for speed. It starts its own production-ready server, handles thousands of concurrent connections and keeps the code of a small service short. That makes it a natural choice for APIs, dashboards and content services, and sooner or later such a service publishes something that people want to follow: articles, release notes, offers, events or listings. A feed is the standard way to let readers, aggregators and auto-posters watch that content without an account or an API key. This guide builds a complete Sanic RSS feed with a clean query, stable identifiers, correct dates and conditional requests, explains each decision and ends with the checks that keep the feed dependable once other systems rely on it.

When a Sanic service needs a feed

A feed is worth adding whenever the service publishes things that people outside it want to follow:

  • articles, news or announcements stored in a database and served by the application;
  • release notes or a changelog for a product, a library or an internal tool;
  • listings, events, offers or openings added by staff or by users;
  • any content that should also appear on social networks without being copied by hand.

If your content already lives in a blog or a CMS, use the feed it provides instead; see how to find your WordPress feed link. If you prefer a different Python framework, the approach is the same in Flask und FastAPI; only the route and the response class change.

Set up the project

Create a virtual environment and install Sanic, the feedgen library and an async SQLite driver. The example uses SQLite so that it runs anywhere, but the query and the loop stay the same with PostgreSQL or MySQL and their async drivers.

python -m venv .venv
source .venv/bin/activate
pip install sanic feedgen aiosqlite

Keep the public address of the site in configuration rather than reading it from the incoming request. Behind a reverse proxy, the request may show an internal host name or a plain http scheme, and a feed full of internal addresses is useless to every reader. The example reads the address from an environment variable with a safe default.

The complete route

The module below opens the database when the server starts, closes it when the server stops and serves the feed from one route. The condition on the publication time matters: without it, an item scheduled for tomorrow would appear in the feed today, and an auto-poster would share it before it exists on the website. Publication times are stored as UTC timestamps.

import hashlib
import os
from datetime import datetime, timezone

import aiosqlite
from feedgen.feed import FeedGenerator
from sanic import Sanic
from sanic.response import HTTPResponse, raw

SITE_URL = os.environ.get("SITE_URL", "https://www.example.com")

app = Sanic("FeedApp")


@app.before_server_start
async def open_db(app, loop):
    app.ctx.db = await aiosqlite.connect("data/site.db")
    app.ctx.db.row_factory = aiosqlite.Row


@app.after_server_stop
async def close_db(app, loop):
    await app.ctx.db.close()


@app.get("/feed.xml")
async def feed(request):
    now = int(datetime.now(timezone.utc).timestamp())
    cursor = await request.app.ctx.db.execute(
        "SELECT id, title, slug, summary, published_at FROM posts "
        "WHERE status = 'published' AND published_at <= ? "
        "ORDER BY published_at DESC LIMIT 20",
        (now,),
    )
    rows = await cursor.fetchall()

    fg = FeedGenerator()
    fg.title("Example News")
    fg.link(href=SITE_URL + "/", rel="alternate")
    fg.link(href=SITE_URL + "/feed.xml", rel="self")
    fg.description("The latest posts from Example News")
    fg.language("en")

    newest = datetime.fromtimestamp(rows[0]["published_at"], tz=timezone.utc) if rows else datetime.fromtimestamp(0, tz=timezone.utc)
    fg.lastBuildDate(newest)

    for row in rows:
        fe = fg.add_entry(order="append")
        fe.title(row["title"])
        fe.link(href=SITE_URL + "/" + row["slug"] + "/")
        fe.guid("post-" + str(row["id"]), permalink=False)
        fe.description(row["summary"])
        fe.pubDate(datetime.fromtimestamp(row["published_at"], tz=timezone.utc))

    body = fg.rss_str(pretty=True)
    etag = '"' + hashlib.sha256(body).hexdigest()[:32] + '"'
    if request.headers.get("If-None-Match") == etag:
        return HTTPResponse(status=304, headers={"ETag": etag})
    return raw(
        body,
        content_type="application/rss+xml; charset=utf-8",
        headers={"ETag": etag, "Cache-Control": "public, max-age=300"},
    )

Save it as server.py and start it with sanic server:app --host 0.0.0.0 --port 8000. Open the route in a browser or with curl and you should see well-formed XML with at most twenty items, newest first. The feedgen library escapes every value on the way out, so a title containing an ampersand or an angle bracket is converted to the proper entity and the feed stays valid.

Why the last build date is set by hand

The feedgen library fills the channel’s last build date with the current time when you do not set one. That is harmless for a page that is rendered once, but it is a trap for a conditional request: every response then contains a different timestamp, so the hash of the body changes every second and the ETag never matches. The route would never answer 304, and every poll would download the full file. Setting the last build date from the newest item makes the body identical for as long as the content is identical, which is exactly what a conditional request needs. The same rule applies to any generator that stamps the current time into the output, in any language.

Where each field comes from

Feed fieldSource in the exampleRule to keep
titleThe title columnPlain text, no markup
linkSite address plus slugAbsolute address that returns 200 directly
guidThe row id with a prefixNever changes, even if the slug changes
pubDateThe publication timestamp in UTCRFC 822 format, never in the future
descriptionThe summary columnOne or two sentences that make sense alone
lastBuildDateThe newest publication timestampChanges only when the content changes

The identifier is the most important decision. An auto-poster remembers which items it has already shared by their GUID. If the value changes when someone edits a slug or republishes an article, the same story is shared again. Using the database id with a fixed prefix, and marking it as not a permanent link, avoids that. The problem and its fixes are described in why a feed auto-posts the same article twice.

Dates in the right format

RSS dates follow RFC 822, with English abbreviations for days and months. The feedgen library accepts a timezone-aware datetime and writes it in the correct shape, so the only thing you have to do is pass a value with a time zone. A naive datetime raises an error, which is a useful reminder: convert everything to UTC before it reaches the feed. Do not use ISO 8601 here; that format belongs to Atom feeds, and many RSS readers reject it in an RSS document. If a validator complains about dates, read the guide to RSS date format errors.

Conditional requests and caching

Auto-posters, readers and search engines request a feed often, and almost always nothing has changed. The route handles that with two small pieces. The ETag header carries a hash of the body, and a request whose If-None-Match header matches it receives a 304 answer with no body. The Cache-Control header allows shared caches to keep the response for five minutes. Together they cut the cost of each check to almost nothing, and the query is cheap enough that you can run it on every request without a separate cache. If the table grows large, add an index on the status and publication columns. The background to both techniques is in the guide to ETags and conditional GET und the guide to feed caching.

Choose the cache period with care. A long period delays every new post by the same amount, because shared caches keep serving the old list until it expires. A few minutes is a good balance for most sites.

Running Sanic in production

In production, run Sanic with several worker processes, keep it under a process manager and put a reverse proxy in front of it that terminates HTTPS. Keep the public address in your own setting rather than trusting forwarded headers, so that links in the feed are always correct. Check that the proxy does not strip the ETag and Cache-Control headers and that it does not add its own long cache that keeps old XML for too long. After every deployment, request the feed once to confirm that it still returns 200 and that the address is exactly the one you gave to the auto-poster. If the feed ever stops updating, this checklist shows what to inspect first.

Images and richer items

An auto-poster is more useful when each item carries a picture. The feedgen library can add an enclosure to an entry with the address, the length in bytes and the type of the image, and you can fill it from an image column. Use an absolute https address and a file of reasonable size. Without an image, many networks show a plain text card. How tools read these elements is explained in the guide to enclosure tags. If a summary contains HTML, keep it escaped as the library does, or wrap it in a CDATA block as described in the CDATA guide; both are valid, and readers handle both.

Test the feed before you connect it

  1. Request the route with curl and look at the headers: the status should be 200, the content type should be application/rss+xml and an ETag should be present.
  2. Repeat the request with the ETag in an If-None-Match header and confirm that a 304 comes back.
  3. Paste the public address into the W3C Feed Validation Service and fix every error.
  4. Insert a test row and check that it appears at the top of the feed on the next request.
  5. Set a publication time in the future and check that the item stays hidden until then.

There is a longer checklist in the feed validation guide and a short curl routine in the guide to debugging a feed with curl.

Common mistakes to avoid

  • Building XML with string concatenation. Use a library so that every value is escaped.
  • Letting the build date follow the clock. Derive it from the newest item so that the ETag can match.
  • Future items. Filter by publication time so scheduled content stays hidden until its moment.
  • Unstable GUIDs. Use an identifier that never changes.
  • Addresses taken from the request. Build links from the configured public address.
  • Changing the feed address. Keep the route, or redirect the old one permanently.

How PostRSS works with a Sanic feed

PostRSS has been publishing RSS and Atom feeds to social networks since 2014. Paste the address of your feed 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.

Weiterführende Lektüre

The bottom line

Sanic needs only a route, an async query and a small feed library to serve a dependable RSS feed: select published items dated in the past, let the library escape every value, use a stable GUID, pass timezone-aware dates, derive the last build date from the newest item and send the result with the right content type and an ETag. Test the route 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 Sanic have a built-in RSS generator?

No. Sanic has no feed support, but a library such as feedgen produces a valid RSS document from a few method calls, and Sanic returns it with the right content type through a raw response.

Why does my feed never return 304?

Most often the body changes between requests. A generator that writes the current time into the last build date makes every response different. Set that date from the newest item and the hash stays the same until the content changes.

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.

Mehr Tools von unserem Team

Entwickelt von Internet Solutions, dem Team hinter PostRSS. Jedes Produkt spart Ihnen auf seine Weise Zeit.