RSS zu 66 sozialen Netzwerken: Facebook, Instagram, X, LinkedIn, Telegram und mehr Blog Affiliate Kontakt
Anmelden Kostenlos starten
Updated: 2026-10-18
Dart Shelf RSS Feed: Build a Server-Side Feed and Auto-Post

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.

When a Dart server needs a feed

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

  • news, announcements or release notes shown in a mobile app and stored in a database;
  • listings, events or offers that users or staff add through the app;
  • a public status or changelog page for a product;
  • any content that should also reach people who never open 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 with other server languages, see the Express guide oder the FastAPI guide.

Set up the project

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 sqlite3

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

The server

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.

Where each field comes from

RSS elementSource in the appRule
titlepost titleEscaped by the builder, under about 100 characters
linksite address plus slugAlways absolute and always the public address
guiddatabase identifierNever changes, even if the slug changes
pubDatepublication dateUTC, 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. If your summaries contain HTML, escape them as shown, or wrap them in CDATA as described in CDATA and HTML in RSS descriptions.

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

Compile and deploy

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_server

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

Caching, compression and speed

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.

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 and the cache header. The method is explained in debugging an RSS feed with curl.
  2. Open the file in a browser or editor and check that the newest post is first.
  3. Run it through the W3C Feed Validation Service and fix every error.
  4. Add a draft and a post dated in the future, and confirm that neither appears.
  5. Publish a real test post and confirm that it appears within the cache lifetime.

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

Why a Flutter team may want this

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.

Common mistakes to avoid

  • Including drafts or future posts. Filter on status and date in the query itself.
  • Building XML by joining strings. One raw ampersand can invalidate the whole file. Use the builder.
  • Using the local time zone. Convert every date to UTC before formatting.
  • Slug-based GUIDs. Renaming a post then creates a duplicate share.
  • Internal addresses in links. Behind a proxy, building links from the request can leak a private name. 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 Dart 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.

Weiterführende Lektüre

The bottom line

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.

FAQ

Does Dart have a built-in RSS generator?

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.

Why use the xml package instead of string templates?

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.

Can I use a framework other than Shelf?

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.

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.