
Short answer: To serve an RSS feed from an aiohttp application, register a GET handler for /feed.xml, query the newest published rows with an asynchronous driver, let the feedgen library write the channel and items so every value is escaped, set the last build date from the newest item instead of the clock, and return the bytes as application/rss+xml. Hash the body into an ETag and answer a matching If-None-Match header with a 304, give every item a permanent GUID, test the route with curl and a validator, then hand the public address to an auto-poster.
aiohttp is the asynchronous HTTP framework that many Python teams reach for when a service must handle a lot of connections at once, call other services and still stay small. It runs APIs, webhooks receivers, dashboards and content services. When such a service publishes something that people want to follow, such as articles, offers, events or release notes, a feed is the standard way to let readers, aggregators and auto-posters watch it without an account. Python has a mature library for writing feeds, so you do not need to concatenate XML by hand. This guide builds a complete aiohttp RSS feed with the feedgen package, an asynchronous 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.
A feed is worth adding whenever new items appear on a schedule you do not control: a changelog that grows with every release, a directory of listings, a news section or a library of guides. Readers who follow it in a feed reader get every item in order, search engines discover new pages faster and an auto-poster such as PostRSS can share each new item on social networks as soon as it is published.
If your content already lives in a blog or a CMS, use the feed that system provides; see how to find your WordPress feed link. If you build services with another Python framework, the same ideas appear in the Flask RSS feed guide a the FastAPI RSS feed guide. Build the route yourself when the content is in your own database and no ready-made feed exists.
Create a virtual environment and install the three packages the example needs: aiohttp for the server, aiosqlite for an asynchronous SQLite connection and feedgen for the document.
python -m venv .venv
source .venv/bin/activate
pip install aiohttp aiosqlite feedgenAssume a table named articles with the columns slug, title, summary, image_url, image_bytes, image_type and published_at. The last column holds a UTC unix timestamp. Items scheduled for the future simply have a timestamp that has not arrived yet.
The program opens one database connection when the application starts, closes it when the application stops, builds the document once per request and sends it with a strong validator. 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.
import hashlib
import time
from datetime import datetime, timezone
import aiosqlite
from aiohttp import web
from feedgen.feed import FeedGenerator
SITE = "https://example.com"
DB_PATH = "site.db"
routes = web.RouteTableDef()
async def db_context(app: web.Application):
db = await aiosqlite.connect(DB_PATH)
db.row_factory = aiosqlite.Row
app["db"] = db
yield
await db.close()
def utc(timestamp: int) -> datetime:
return datetime.fromtimestamp(timestamp, tz=timezone.utc)
def build_feed(rows) -> bytes:
fg = FeedGenerator()
fg.title("Example Journal")
fg.link(href=SITE + "/", rel="alternate")
fg.link(href=SITE + "/feed.xml", rel="self")
fg.description("News and guides from Example Journal.")
fg.language("en")
newest = rows[0]["published_at"] if rows else 0
fg.lastBuildDate(utc(newest))
for row in rows:
url = f"{SITE}/articles/{row['slug']}/"
entry = fg.add_entry(order="append")
entry.title(row["title"])
entry.link(href=url)
entry.guid(url, permalink=True)
entry.description(row["summary"])
entry.pubDate(utc(row["published_at"]))
if row["image_url"]:
entry.enclosure(
row["image_url"],
str(row["image_bytes"] or 0),
row["image_type"] or "image/jpeg",
)
return fg.rss_str(pretty=False)
def etag_matches(header: str | None, etag: str) -> bool:
if not header:
return False
for token in header.split(","):
token = token.strip().removeprefix("W/")
if token == "*" or token == etag:
return True
return False
@routes.get("/feed.xml")
async def feed(request: web.Request) -> web.Response:
db = request.app["db"]
async with db.execute(
"SELECT slug, title, summary, image_url, image_bytes, image_type, published_at "
"FROM articles WHERE published_at <= ? "
"ORDER BY published_at DESC LIMIT 20",
(int(time.time()),),
) as cursor:
rows = await cursor.fetchall()
body = build_feed(rows)
etag = '"' + hashlib.sha256(body).hexdigest()[:32] + '"'
headers = {"ETag": etag, "Cache-Control": "public, max-age=300"}
if etag_matches(request.headers.get("If-None-Match"), etag):
return web.Response(status=304, headers=headers)
return web.Response(
body=body,
content_type="application/rss+xml",
charset="utf-8",
headers=headers,
)
def create_app() -> web.Application:
app = web.Application()
app.cleanup_ctx.append(db_context)
app.add_routes(routes)
return app
if __name__ == "__main__":
web.run_app(create_app(), port=8080)Run it with python app.py. The route accepts GET, and aiohttp answers HEAD requests for routes registered with the get helper, which is what some validators and monitoring tools use first.
The library takes care of the details that are easy to get wrong when XML is written by hand: it escapes ampersands and angle brackets in titles and summaries, declares the encoding, writes the namespaces for the self link and formats dates as RFC 822 from timezone-aware values. Two settings matter for stability. First, the example sets the last build date explicitly. If you leave it out, the library fills in the current time, the body changes on every request and the ETag logic below never produces a 304. Second, the order argument of add_entry is set to append, so items stay in the order of the query, newest first, instead of being reversed.
Auto-posters, readers and search engines request a feed often, and almost always nothing has changed. The handler hashes the finished bytes, quotes the hash as an entity tag and compares it with the If-None-Match header. A match produces a 304 response with no body, which saves bandwidth for both sides and keeps your server calm. The hash is computed from the body, which brings one rule: the body must not change unless the content changes. That is why the build date comes from the newest item. The same rule applies to any value generated at request time. A compressing proxy may turn a strong tag into a weak one, so the comparison strips the W/ prefix before it matches.
| Feed element | Source | Rule |
|---|---|---|
| title | Article title | Plain text, escaped by the library |
| link | Public address of the page | Absolute, with https, returning 200 directly |
| guid | Same address, or a stored identifier | Never changes after publication |
| pubDate | Publication timestamp | Aware UTC datetime, never the current time |
| description | Short summary | One or two sentences of plain text |
| enclosure | Featured image | Address, size in bytes and media type |
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. If you may rename slugs, store a separate permanent identifier and write it with permalink set to false.
The reason to use aiohttp is concurrency, and a feed handler should respect it. The example awaits every database call, so while one request waits for SQLite, the loop serves others. If you use another database, choose a driver built for asyncio, share a pool through the application context in the same way and keep the query small: twenty rows, one table, an index on the publication column. If a synchronous library is unavoidable, run it in an executor with loop.run_in_executor so the loop stays free.
The Cache-Control header in the example lets shared caches keep the response for five minutes, and the ETag lets everyone revalidate cheaply after that. If you place a content delivery network in front of the application, keep the lifetime short so a new item does not wait long for its turn. A feed that lags behind the website by an hour defeats the purpose of automation. For the background, read how auto-posting tools use ETags and conditional requests a why slow feeds break auto-posting.
A few minutes of testing prevents days of confusion later. Start the service and check it from the outside:
curl -i https://example.com/feed.xml and confirm a 200 status, the right content type and an ETag header.-H 'If-None-Match: ...' and the tag you received, and confirm that the answer is 304.There is a longer checklist in the feed validation guide and a short curl routine in the guide to debugging a feed with curl. If you prefer automated tests, aiohttp ships a test utilities module that starts the application in a test client, so the same two requests can run in your continuous integration.
Run the application behind a reverse proxy that terminates HTTPS and passes the real host and scheme, so absolute links are correct. Start several worker processes with a process manager, because one asyncio process uses one core. Keep the site address in configuration instead of a constant, so a staging copy never publishes production links, and monitor the feed route separately from the home page. If the route sits behind a login or a firewall rule for unknown clients, an auto-poster will not reach it; the feed must be public.
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.
aiohttp needs only a route, an awaited query and a 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, set the last build date from the newest item and send the result with the right content type. Add a hash-based ETag so repeat requests end in a 304. 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.
No. aiohttp has no feed support, but the feedgen package writes a valid RSS document from a few method calls, and the handler returns it with the right content type.
If you do not, feedgen writes the current time, so the body changes on every request. Setting the date from the newest item keeps the body identical until the content changes, which lets the ETag produce 304 answers.
You can, but run it in an executor so the event loop is not blocked. An asynchronous driver that you await directly is the cleaner choice for an asyncio service.
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.
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.
Vytvořilo je Internet Solutions, tým za PostRSS. Každý produkt vám ušetří čas po svém.