RSS 2.0 and Atom 1.0 feed generation library for MoonBit.
It supports channel and item metadata, authors, categories, dates, enclosures with MIME type detection, GeoRSS coordinates, PubSubHubbub links, feed images, custom XML namespaces, and custom XML elements.
Repository: https://github.com/tkancf/feed_gen.mbt
moon add tkancf/feed_gen///|
let item = @feed_gen.FeedItem::new(
"First Post",
description="This is the summary of my first post.",
)
.set_content(Some("<p>This is the full post.</p>"))
.set_url(Some("https://example.com/posts/first"))
.set_pub_date(Some(@feed_gen.FeedDate::utc(2026, 6, 27, hour=12)))
///|
let config = @feed_gen.FeedConfig::new(
"My Blog",
"A blog about technology and programming",
"https://example.com",
items=[item],
)
.set_feed_url(Some("https://example.com/rss.xml"))
.set_last_build_date(Some(@feed_gen.FeedDate::utc(2026, 6, 27, hour=12)))
.set_author(Some(@feed_gen.Author::new("Jane Doe", email="jane@example.com")))
///|
let rss = config.to_xml(indent=true).unwrap()
///|
let atom = config.to_atom(indent=true).unwrap()Run the wasm-compatible sample command:
moon run cmd/mainIt prints both RSS 2.0 and Atom 1.0 XML for a small in-memory feed.
FeedConfig describes the feed/channel. Construct it with FeedConfig::new
and chain the fluent setters for construction and updates. Record fields are
read-only to external users, so field renames or removals are treated as
breaking changes. The config covers the required title, description, and site
URL, plus optional feed URL, authorship, dates, image metadata, categories,
RSS metadata, custom namespaces, custom elements, and items.
Create one with:
///|
let config = @feed_gen.FeedConfig::new(
"My Feed", "Feed description", "https://example.com",
)FeedItem describes each entry:
///|
let item = @feed_gen.FeedItem::new(
"Article",
description="Short summary",
)
.set_content(Some("<p>Full HTML content.</p>"))
.set_url(Some("https://example.com/article"))
.set_guid(Some("article-1"))
.set_pub_date(Some(@feed_gen.FeedDate::utc(2026, 6, 27)))Item titles are required; rendering returns MissingRequiredField("item.title")
when an item title is empty.
RSS renders authors as dc:creator using the display name only. Author email
addresses are emitted by Atom output, not RSS output.
Render RSS 2.0:
///|
let xml = config.to_xml(indent=true).unwrap()Render Atom 1.0:
///|
let xml = config.to_atom(indent=true).unwrap()Atom requires an updated value, supplied by last_build_date or pub_date.
When the feed has entries, Atom also requires either a feed-level author or an
author on every item. Each Atom item must have a guid or url; guid is
used as the entry <id> when present, otherwise url is used.
Both renderers return Result[String, Array[FeedError]].
set_hub renders WebSub/PubSubHubbub hub links in both RSS and Atom output.
Sort items with FeedConfig::sort_by; use item getters inside the comparator
instead of relying on record fields:
///|
let newest_first = config.sort_by(fn(a, b) {
match (a.pub_date(), b.pub_date()) {
(Some(x), Some(y)) => y.compare(x)
(Some(_), None) => -1
(None, Some(_)) => 1
(None, None) => a.title().compare(b.title())
}
})///|
let utc = @feed_gen.FeedDate::utc(2026, 6, 27, hour=12, minute=30)
///|
let jst = @feed_gen.FeedDate::new(
2026,
6,
27,
hour=21,
minute=30,
offset_minutes=540,
)Dates are validated when rendering. Years must be in 1..9999; invalid
date/time fields return InvalidDate instead of producing XML.
Geo coordinates are validated when rendering. Latitude and longitude must be
provided together; either coordinate on its own returns InvalidGeo.
///|
let episode = @feed_gen.FeedItem::new(
"Episode 1",
description="The first episode",
)
.set_enclosure(
Some(
@feed_gen.FeedEnclosure::new(
"https://example.com/media/episode-1.m4a", 12_345_678L,
),
),
)The MIME type is detected from the URL extension. Use set_mime_type to
override detection.
///|
let image = @feed_gen.FeedImage::new(
"https://example.com/logo.png",
"Example Feed",
"https://example.com",
)
.set_width(Some(144))
.set_height(Some(144))
.set_description(Some("Feed artwork"))
///|
let config = @feed_gen.FeedConfig::new(
"Example Feed",
"A feed with artwork",
"https://example.com",
)
.set_image(Some(image))///|
let config = @feed_gen.FeedConfig::new("Podcast", "A show", "https://example.com")
.add_custom_namespace("itunes", "http://www.itunes.com/dtds/podcast-1.0.dtd")
.add_custom_element(@feed_gen.Text("itunes:author", "Jane Doe"))
.add_custom_element(@feed_gen.Element("itunes:owner", [], [
@feed_gen.Text("itunes:name", "Jane Doe"),
@feed_gen.Text("itunes:email", "jane@example.com"),
]))Custom tags and attribute names are validated before successful rendering.
Custom element names, attribute names, and namespace prefixes intentionally
support ASCII XML names only.
CDATA content is sanitized and embedded ]]> markers are split so generated
XML remains well-formed.
When validation fails, renderers return all collected FeedError values.
Custom namespace prefixes must be valid XML NCNames. RSS reserves the built-in
prefixes dc, content, atom, and geo; Atom reserves atom and geo.
Those prefixes cannot be redefined in custom_namespaces for the matching
renderer. A custom namespace prefix can only be declared once.
Apache-2.0