README
Syndicate
Syndication feed parser and generator supporting RSS 2.0, RSS 0.91, RSS 1.0, Atom 1.0, and JSON Feed 1.1.
Dependencies
XML - XML parsing and generation (RSS, Atom)
JSON::Fast - JSON parsing and generation
DateTime::Grammar - W3C/ISO 8601 date parsing
DateTime::Format - RFC 2822 date formatting
HTTP::Tiny - Feed fetching via HTTP/HTTPS
IO::Socket::SSL - HTTPS support (required by HTTP::Tiny)
URI - URL resolution
Installation
zef install SyndicateInstalling from source
To install from a local folder:
cd /path/to/Syndicate
zef install .Requires Raku 6.d or later.
Supported Formats
| Format | Parse | Generate | Class |
|---|---|---|---|
| RSS 2.0 | ā | ā | Syndicate::RSS |
| RSS 0.91 | ā | ā | Syndicate::RSS::V0_91 |
| RSS 1.0 | ā | ā | Syndicate::RSS::V1_0 |
| Atom 1.0 | ā | ā | Syndicate::Atom |
| JSON Feed | ā | ā | Syndicate::JSONFeed |
Quick Start
use Syndicate;
# --- Parse any feed format (auto-detected) ---
my $feed = parse($xml-or-json-string);
say $feed.title;
say $feed.items[0].title;
# --- Build a feed programmatically ---
use Syndicate::Builder::Feed;
my $fb = Syndicate::Builder::Feed.new;
$fb.title("My Podcast");
$fb.link("https://example.com");
$fb.description("A great podcast");
$fb.language("en");
$fb.generator("Syndicate");
my $entry = $fb.add-entry;
$entry.title("Episode 1");
$entry.link("https://example.com/ep1");
$entry.summary("First episode");
$entry.id("urn:uuid:abc-123");
$entry.updated(DateTime.now);
# Generate any format from the same builder
say $fb.rss-str; # RSS 2.0 XML
say $fb.atom-str; # Atom 1.0 XML
say $fb.rss091-str; # RSS 0.91 XML
say $fb.rss1-str; # RSS 1.0 XML
say $fb.json-str; # JSON FeedParsing Feeds
Auto-detect format
use Syndicate;
use Syndicate::Parse;
my $feed = parse-feed($xml-or-json-string);
# or
my $feed = parse($xml-or-json-string); # exported by SyndicateThe parser detects the format automatically:
{ā JSON Feed<feedā Atom 1.0<rss version="0.91"ā RSS 0.91<rss version="0.92|0.93|0.94"ā RSS 2.0 (legacy 0.9x flavors are structurally RSS 2.0-compatible)<rssā RSS 2.0<rdf:RDFā RSS 1.0
Parse from a file
use Syndicate::Parse;
my $feed = parse-file("feed.xml"); # Str path
my $feed = parse-file("feed.xml".IO); # IO::Path also worksExplicit format parsing
my $rss = Syndicate::RSS.new($xml-string);
my $atom = Syndicate::Atom.new($xml-string);
my $rss091 = Syndicate::RSS::V0_91.new($xml-string);
my $rss1 = Syndicate::RSS::V1_0.new($xml-string);
my $json = Syndicate::JSONFeed.new($json-string);Convenience subs are also exported by Syndicate:
my $rss = parse-rss($xml-string);
my $atom = parse-atom($xml-string);Building Feeds
Using the Builder API (format-agnostic)
The builder lets you construct a feed once and output it in any format:
use Syndicate::Builder::Feed;
my $fb = Syndicate::Builder::Feed.new;
$fb.title("My Feed");
$fb.link("https://example.com");
$fb.description("A test feed");
$fb.id("https://example.com/feed");
$fb.language("en");
$fb.rights("Ā© 2026 Me");
$fb.generator("MyApp");
$fb.icon("https://example.com/icon.png");
$fb.logo("https://example.com/logo.png");
$fb.updated(DateTime.new("2026-06-15T12:00:00Z"));
# Author (stored as name, email, uri)
$fb.author(:name("Jane Doe"), :email("[email protected]"));
# Categories (multiple allowed)
$fb.category("Tech");
$fb.category("News");
# Add entries
my $e = $fb.add-entry;
$e.title("Article 1");
$e.link("https://example.com/1");
$e.summary("First article");
$e.id("urn:uuid:1111-1111");
$e.updated(DateTime.new("2026-06-15T10:00:00Z"));
$e.published(DateTime.new("2026-06-14T08:00:00Z"));
$e.author(:name("Jane"), :email("[email protected]"));
$e.category("Raku");
$e.content("<p>Hello <em>world</em></p>", :type("xhtml"));
$e.rights("Ā© Entry");
# Output in any format
say $fb.rss-str; # RSS 2.0
say $fb.atom-str; # Atom 1.0
say $fb.rss091-str; # RSS 0.91
say $fb.rss1-str; # RSS 1.0
say $fb.json-str; # JSON Feed
# Or get the feed object directly
my $feed = $fb.atom-feed;
say $feed.updated;Direct construction (format-specific)
You can also construct feed objects directly with named arguments:
# RSS 2.0
my $rss = Syndicate::RSS.new(
:title("My RSS Feed"),
:link("https://example.com"),
:description("RSS description"),
:language("en"),
:generator("MyApp"),
:copyright("Ā© 2026"),
:items([
Syndicate::RSS::Item.new(
:title("Item 1"),
:link("https://example.com/1"),
:summary("Item description"),
:author("[email protected]"),
:updated(DateTime.new("2026-06-15T10:00:00Z")),
:guid("https://example.com/1"),
),
]),
);
# Atom 1.0
my $atom = Syndicate::Atom.new(
:title("My Atom Feed"),
:id("https://example.com/atom"),
:link("https://example.com"),
:description("Atom subtitle"),
:updated(DateTime.now),
:rights("Ā© 2026"),
:generator("MyApp"),
:items([
Syndicate::Atom::Item.new(
:title("Entry 1"),
:id("https://example.com/1"),
:link("https://example.com/1"),
:summary("Entry summary"),
:content("<p>Hello world</p>"),
:content-type("xhtml"),
:updated(DateTime.new("2026-06-15T10:00:00Z")),
:published(DateTime.new("2026-06-14T08:00:00Z")),
),
]),
);
# JSON Feed
my $json = Syndicate::JSONFeed.new(
:title("My JSON Feed"),
:link("https://example.com"),
:description("JSON Feed description"),
:feed_url("https://example.com/feed.json"),
:language("en"),
:items([
Syndicate::JSONFeed::Item.new(
:title("Post 1"),
:id("https://example.com/1"),
:link("https://example.com/1"),
:summary("Post summary"),
:content_html("<p>Hello world</p>"),
),
]),
);iTunes Podcast Extensions
The builder supports iTunes podcast fields at the channel level for RSS 2.0:
$fb.itunes-author("John Doe");
$fb.itunes-summary("A great podcast about Raku");Per-item iTunes fields (itunes-author, itunes-summary, itunes-duration)
are read-only: they are populated when an RSS 2.0 feed is parsed, not via the
builder.
Generating Output
Every feed and item can be stringified to its native format:
# RSS / Atom ā XML string
say ~$rss-feed; # XML output via .Str
say $rss-feed.Str; # same
say $rss-feed.XML; # XML::Element object
# JSON Feed ā JSON string
say $json-feed.Str; # JSON output
say $json-feed.to-json; # same
say $json-feed.to-hash; # Perl Hash structureConverting Between Formats
Parse any feed and re-emit it in another format with convert:
use Syndicate;
my $rss-xml = convert($atom-string, 'rss'); # Atom ā RSS 2.0
my $atom-xml = convert($rss-string, 'atom'); # RSS 2.0 ā Atom
my $json = convert($rss-string, 'json'); # RSS 2.0 ā JSON Feed$to accepts rss (aliases rss2, rss2.0), rss091 (rss0.91),
rss1 (rss1.0), atom, and json. Conversion goes through
Syndicate::Builder::Feed.new-from-feed, which copies every field the builder
models. Format-specific extras that the builder does not model (RSS
webMaster/docs/ttl/pubDate, Atom contributors and extra/self links,
media groups, item-level iTunes fields, JSON external_url/image/
banner_image) are not carried over. Emission may die if the target format
requires a field the source lacks ā e.g. an Atom feed without a description
cannot be emitted as RSS 2.0.
For programmatic access, populate a builder directly:
use Syndicate;
use Syndicate::Builder::Feed;
my $fb = Syndicate::Builder::Feed.new-from-feed(parse($rss-string));
say $fb.atom-str; # RSS 2.0 ā Atom, with a single builder
say $fb.json-str; # and JSON Feed from the same builderFeed Discovery
Fetch and auto-detect feeds from URLs:
use Syndicate::Discovery;
my $disc = Syndicate::Discovery.new;
# Fetch a known feed URL
my $feed = $disc.fetch("https://example.com/feed.xml");
# Discover feed from a webpage (finds <link> tags)
my $feed = $disc.discover("https://example.com");
# Customize redirect handling (maximum of 10 redirects)
my $disc = Syndicate::Discovery.new(:max-redirect(10));
my $feed = $disc.fetch("https://example.com/feed.xml");
# Find feed URLs without fetching
my @urls = $disc.find-feeds($html-string, "https://example.com");Extensions
Extensions are automatically applied during parsing and generation via the Syndicate::Extensions registry.
Dublin Core
Adds dc:creator, dc:date, dc:subject elements to RSS items.
use Syndicate::Extension::DublinCore;
# Automatically registered - parses and generates dc:* elementsMedia RSS
Adds media content and thumbnails to RSS items.
use Syndicate::Extension::MediaRSS;
# Access parsed media data
.say for $rss-item.media-contents; # Array of Hashes (url, type, medium, ...)
.say for $rss-item.media-thumbnails; # Array of Hashes (url, width, height)
say $rss-item.media-title;
say $rss-item.media-description;iTunes Podcast
Adds iTunes podcast elements to RSS feeds and items.
use Syndicate::Extension::ITunes;
say $rss-feed.itunes-author;
say $rss-feed.itunes-summary;
say $rss-item.itunes-author;
say $rss-item.itunes-summary;
say $rss-item.itunes-duration;Common API
All feed types do the Syndicate::Feed role. All item types do the Syndicate::Item role. This provides a uniform interface:
Feed Role Attributes
| Attribute | Type | RSS 2.0 | RSS 0.91 | RSS 1.0 | Atom | JSON Feed |
|---|---|---|---|---|---|---|
.title | Str | ā | ā | ā | ā | ā |
.link | Str | ā | ā | ā | ā | ā |
.description | Str | ā | ā | ā | ā | ā |
.generator | Str | ā | ā | ā | ā | ā |
.language | Str | ā | ā | ā | ā | ā |
.items | @ | ā | ā | ā | ā | ā |
Item Role Attributes
| Attribute | Type | RSS 2.0 | RSS 0.91 | RSS 1.0 | Atom | JSON Feed |
|---|---|---|---|---|---|---|
.title | Str | ā | ā | ā | ā | ā |
.link | Str | ā | ā | ā | ā | ā |
.summary | Str | ā | ā | ā | ā | ā |
.author | Str | ā | ā | ā | ā | ā |
.updated | DateTime | ā | ā | ā | ā | ā |
.id | Str | ā | ā | ā | ā | ā |
.content | Str | ā | ā | ā | ā |
RSS 0.91 and RSS 1.0 populate author and updated from Dublin Core
(dc:creator, dc:date); JSON Feed's updated derives from date_modified.
RSS 0.91 items have no content module, so .content is never populated.
Format-Specific Attributes
Each format also exposes its own attributes:
RSS 2.0 Feed: copyright, managingEditor, webMaster, pubDate, lastBuildDate, categories, docs, ttl, image, itunes-author, itunes-summary
RSS 2.0 Item: guid, guid-is-permalink, categories, comments, enclosure, source, media-contents, media-thumbnails, media-title, media-description, itunes-author, itunes-summary, itunes-duration
RSS 0.91 Feed: copyright, managingEditor, webMaster, rating, docs, pubDate, lastBuildDate, image, textInput, skipHours, skipDays
RSS 1.0 Feed: about, image (hash with url/title/link/about)
RSS 1.0 Item: about, dc-subjects
Atom Feed: id, subtitle, author, author-detail, categories, updated, rights, icon, logo, contributors, link-self, link-alternate
Atom Item: author-detail, categories, published, content-type, rights, source-feed, contributors
JSON Feed: version, feed_url, user_comment, next_url, icon, favicon, author, expired
JSON Feed Item: external_url, content_html, content_text, image, banner_image, date_published, date_modified, authors, tags
Statistics
use Syndicate::Stats;
say "Feeds parsed: {Syndicate::Stats.feeds-parsed}";
say "Items parsed: {Syndicate::Stats.items-parsed}";
Syndicate::Stats.record-feed;
Syndicate::Stats.record-item;AUTHOR
Sasha Abbott [email protected]
LICENSE
This software is dedicated to the public domain under the CC0 1.0 Universal (CC0 1.0) Public Domain Dedication.
To the extent possible under law, the author has waived all copyright and related or neighboring rights to this work.