Skip to content

Latest commit

 

History

41 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

tkancf/feed_gen

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

Installation

moon add tkancf/feed_gen

Quick Start

///|
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()

Sample Command

Run the wasm-compatible sample command:

moon run cmd/main

It prints both RSS 2.0 and Atom 1.0 XML for a small in-memory feed.

Core Types

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.

Rendering

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())
  }
})

Dates

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

Enclosures

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

Images

///|
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))

Custom XML

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

License

Apache-2.0

About

RSS 2.0 and Atom 1.0 feed generation library for MoonBit.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages