
Short answer: To serve an RSS feed from a Dart server, add a Shelf route for /feed.xml, query the newest published items from your database, build RSS 2.0 XML with the xml package so every value is escaped for you, format dates with HttpDate in UTC, and return the result with the content type application/rss+xml and a short cache header. Compile the server, run it 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.
Dart is best known for Flutter, but it is also a fast, typed language for servers, and many mobile teams use it for their own back ends so that the app and the API share one language. A common gap appears when the app publishes news, offers, events or release notes: the content lives in a database and in the app, but nothing outside can follow it. A feed fixes that. It lets readers, aggregators and auto-posters watch your content without installing anything. This guide builds a complete Dart Shelf RSS feed, explains each decision and ends with the checks that keep it dependable.
A feed is worth adding whenever your server 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 with other server languages, see the Express guide atau the FastAPI guide.
Create a server project and add the packages used in this guide. Shelf provides the request and response model, shelf_router adds routes, xml builds the document and sqlite3 reads the example database. Replace the last one with the driver for your own database.
dart create -t server-shelf rss_feed_server
cd rss_feed_server
dart pub add shelf shelf_router xml sqlite3The template creates a working server in bin/server.dart. You will replace its content with the code below, which keeps everything in one file to make it easy to read. In a real project you would split the query, the feed builder and the route into separate files.
import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_router/shelf_router.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:xml/xml.dart';
final siteUrl = Platform.environment['SITE_URL'] ?? 'https://www.example.com';
final db = sqlite3.open(Platform.environment['DB_PATH'] ?? 'data/site.db');
class Post {
Post(this.id, this.title, this.slug, this.summary, this.publishedAt);
final String id, title, slug, summary;
final DateTime publishedAt;
}
List<Post> latestPosts({int limit = 20}) {
final nowSeconds = DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000;
final rows = db.select(
'SELECT id, title, slug, summary, published_at FROM posts '
'WHERE status = ? AND published_at <= ? '
'ORDER BY published_at DESC LIMIT ?',
['published', nowSeconds, limit],
);
return [
for (final r in rows)
Post(
r['id'].toString(),
r['title'] as String,
r['slug'] as String,
r['summary'] as String,
DateTime.fromMillisecondsSinceEpoch((r['published_at'] as int) * 1000, isUtc: true),
),
];
}
String buildFeed(List<Post> posts) {
final builder = XmlBuilder();
builder.processing('xml', 'version="1.0" encoding="UTF-8"');
builder.element('rss', attributes: {
'version': '2.0',
'xmlns:atom': 'http://www.w3.org/2005/Atom',
}, nest: () {
builder.element('channel', nest: () {
builder.element('title', nest: 'Example Product News');
builder.element('link', nest: '$siteUrl/news/');
builder.element('description', nest: 'Release notes and announcements');
builder.element('language', nest: 'en');
final newest = posts.isEmpty
? DateTime.fromMillisecondsSinceEpoch(0, isUtc: true)
: posts.first.publishedAt;
builder.element('lastBuildDate', nest: HttpDate.format(newest));
builder.element('atom:link', attributes: {
'href': '$siteUrl/feed.xml',
'rel': 'self',
'type': 'application/rss+xml',
});
for (final p in posts) {
builder.element('item', nest: () {
builder.element('title', nest: p.title);
builder.element('link', nest: '$siteUrl/news/${p.slug}/');
builder.element('guid', attributes: {'isPermaLink': 'false'}, nest: p.id);
builder.element('pubDate', nest: HttpDate.format(p.publishedAt));
builder.element('description', nest: p.summary);
});
}
});
});
return builder.buildDocument().toXmlString(pretty: true);
}
Response feedHandler(Request request) {
final xml = buildFeed(latestPosts());
return Response.ok(xml, headers: {
'content-type': 'application/rss+xml; charset=utf-8',
'cache-control': 'public, max-age=300',
});
}
Future<void> main() async {
final router = Router()..get('/feed.xml', feedHandler);
final handler = const Pipeline().addMiddleware(logRequests()).addHandler(router.call);
final port = int.parse(Platform.environment['PORT'] ?? '8080');
final server = await io.serve(handler, InternetAddress.anyIPv4, port);
print('Feed server listening on port ${server.port}');
}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. The XmlBuilder escapes every text value and attribute, so an ampersand in a title can no longer break the file. HttpDate.format produces a date such as Sun, 18 Oct 2026 07:40:00 GMT in English UTC, which is what the RSS specification asks for, regardless of the language of the server. The GUID is marked with isPermaLink set to false because it is an identifier and not an address.
| RSS element | Source in the app | Rule |
|---|---|---|
| title | post title | Escaped by the 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 date | 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. 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 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 with its address, size and real type, in the same builder block. If the size is unknown, many readers accept zero, but the type should be accurate. 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.
Dart can compile the server to a native executable, which starts quickly and needs no runtime on the target machine:
dart compile exe bin/server.dart -o build/feed_server
SITE_URL=https://www.example.com DB_PATH=/var/lib/site/site.db PORT=8080 ./build/feed_serverRun the program 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 to the server and can cache the answer for a few minutes. If you build a container, copy only the compiled file and the database driver library into a small image. Remember that the sqlite3 package needs the native SQLite library on the machine; with other databases, the driver has its own requirements.
A feed is requested often, mostly by robots. Each request here runs one small query, so the cost is low, but a short cache is a sensible protection. The header in the example 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. Let the proxy compress the response; see RSS feed compression. 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.
curl -i https://www.example.com/feed.xml and check for a 200 status, the XML content type 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.
Mobile teams often treat the server as a hidden part of the product, yet it is the one place where public content can be exposed in a standard form. A feed costs little to add, it does not change the app and it opens a channel to people who prefer to follow updates through readers, newsletters or social networks. It also gives support and marketing staff an address they can hand to any tool, without asking a developer for an export each time.
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.
Dart does not ship an RSS generator, but a feed is a small amount of code: query only published items dated in the past, build the XML with a builder so every value is escaped, use a stable GUID, format dates in English UTC, build links from a configured site address and return the XML with the right content type and a short cache. Compile the server, put a proxy in front, test with curl and a validator, keep the address unchanged and connect it to an auto-poster. After that, every item you publish in your app can reach your social channels automatically.
No. The standard libraries and Shelf focus on HTTP, and you build the XML yourself. The xml package makes this safe because it escapes values for you.
String templates break when a title contains an ampersand or another special character. The builder writes a valid document every time and keeps the structure visible in the code.
Yes. Frameworks such as Dart Frog or Serverpod can serve the same XML. Keep the same rules: only published items, newest first, a limit of about twenty and a stable GUID for each item.
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.
Dibuat oleh Internet Solutions, tim di balik PostRSS. Masing-masing menghemat waktu Anda dengan caranya sendiri.