RSS 66 közösségi hálózatra: Facebook, Instagram, X, LinkedIn, Telegram és további platformok Blog Partnerprogram Kapcsolat
Bejelentkezés Kezdje ingyen
Updated: 2026-10-31
Jester RSS Feed in Nim: Build the XML by Hand and Auto-Post

Short answer: In a Jester application written in Nim, an RSS feed is one route that returns an XML string. Describe your posts as plain objects, escape the text, build absolute links, write the dates in the RSS format in UTC, send the RSS content type with a cache validator and test the live address with curl and a validator. When the address, the links and the dates stay stable, an auto-poster can read the feed and share every new post on your social networks.

Nim is a compiled language with a readable syntax, and Jester is its best-known web framework. It describes routes in a small domain-specific block, compiles to a single native binary and starts quickly, which suits small services that sit behind a reverse proxy. A feed is exactly that kind of service: a few kilobytes of XML, requested often and expected to be correct. There is no feed library that everybody uses, so you write the XML yourself, and the quality of the result depends on a handful of details: escaped text, absolute links, dates in the right format and an identifier that never changes. This guide builds a complete route and ends with a test routine.

Set up the project

Install the framework with the Nim package manager and create one source file. The standard library already provides the time and hashing modules that the feed needs.

nimble install jester
mkdir feedapp
cd feedapp
touch app.nim

The examples below use a small list in memory, which keeps the code short. In a real project, the list would come from a database or from Markdown files on disk, and the rest of the code would stay the same. If you build services in other compiled languages, compare this approach with the Kemal feed guide és the Actix Web feed guide.

Describe the posts

A feed item needs a title, a permanent address, a publication date and a summary. An object type carries those fields. Store the date as a time value in UTC, so that the time zone of the server can never change what the feed says.

import std/[times, strutils, sequtils, algorithm, sha1]
import jester

type
  Post = object
    slug, title, summary: string
    publishedAt: DateTime

const Site = "https://example.com"

let posts = @[
  Post(
    slug: "spring-opening-hours",
    title: "Spring Opening Hours",
    summary: "Our new opening hours start on Monday.",
    publishedAt: dateTime(2026, mOct, 31, 8, 0, 0, zone = utc())
  )
]

The slug is part of the permanent address of the post. Choose it once, when the post is created, and never derive it from the title again, because an edited title would then change the address.

Escape the text

Text from a database or a file can contain ampersands, angle brackets and quotation marks, and any of them can break an XML document. Escape every value before it goes into the template.

proc esc(s: string): string =
  s.multiReplace(
    ("&", "&" & "amp;"),
    ("<", "&" & "lt;"),
    (">", "&" & "gt;"),
    ("\"", "&" & "quot;")
  )

The order of the pairs matters in many languages, because replacing the ampersand last would double-encode the others. The function above replaces all characters in one pass over the input, so it does not have that problem. Test it with a title that contains all four characters before you trust it.

Build the document

The RSS specification expects dates in a fixed format, with an English abbreviation for the day and the month and a time zone. The format string below produces it from a UTC time.

proc rfc822(t: DateTime): string =
  t.utc.format("ddd, dd MMM yyyy HH:mm:ss 'GMT'")

proc renderFeed(items: seq[Post]): string =
  let sorted = items.sortedByIt(it.publishedAt).reversed[0 ..< min(20, items.len)]
  let newest = if sorted.len > 0: sorted[0].publishedAt else: dateTime(1970, mJan, 1, zone = utc())
  var body = ""
  for p in sorted:
    let url = Site & "/blog/" & p.slug
    body.add "<item>\n"
    body.add "<title>" & esc(p.title) & "</title>\n"
    body.add "<link>" & esc(url) & "</link>\n"
    body.add "<guid isPermaLink=\"true\">" & esc(url) & "</guid>\n"
    body.add "<pubDate>" & rfc822(p.publishedAt) & "</pubDate>\n"
    body.add "<description>" & esc(p.summary) & "</description>\n"
    body.add "</item>\n"
  result = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" &
    "<rss version=\"2.0\"><channel>\n" &
    "<title>Example Journal</title>\n" &
    "<link>" & Site & "</link>\n" &
    "<description>News and guides from Example Journal.</description>\n" &
    "<lastBuildDate>" & rfc822(newest) & "</lastBuildDate>\n" &
    body & "</channel></rss>\n"

Several details are deliberate. The channel date comes from the newest post and not from the clock, so that the file does not change when nothing was published; a feed that changes on every request defeats caching. The identifier of each item is its permanent address. The address is built from a fixed constant, so every link is absolute. And only the newest twenty items are written, because a short file is fetched quickly.

Add the route and the validator

The route renders the document, computes a hash of it and compares that hash with the header that the client sends. When the two match, the server answers with a 304 status and no body.

routes:
  get "/feed.xml":
    let xml = renderFeed(posts)
    let etag = "\"" & $secureHash(xml) & "\""
    let sent = if request.headers.isSome: $request.headers.get.getOrDefault("If-None-Match") else: ""
    if sent == etag:
      resp Http304, [("ETag", etag)], ""
    else:
      resp Http200, [
        ("Content-Type", "application/rss+xml; charset=utf-8"),
        ("Cache-Control", "public, max-age=300"),
        ("ETag", etag)
      ], xml

The validator is a hash of the finished document, so it changes exactly when the content changes. Check the way that your version of the framework exposes request headers, because the details of that API have changed over time. A 304 answer saves bandwidth for you and for every tool that polls the feed; the mechanism is explained in the guide to ETags and conditional requests, and the status codes in the guide to feed status codes.

Where each field comes from

Feed elementSource in the codeRule
Titletitle fieldPlain text, escaped
Link and identifierSite and slugAbsolute, https, never changes
Publication datepublishedAt in UTCIn the past, never the current time
Descriptionsummary fieldOne or two complete sentences
Channel dateNewest publishedAtChanges only when a post is added
ValidatorHash of the documentChanges only when content changes

The identifier deserves the most attention. If the slug follows the title and someone corrects a typo, the address changes, and an auto-poster that stores identifiers will treat the old and the new address as two different posts. The causes and cures are covered in why a feed auto-posts the same article twice.

Dates, drafts and old posts

Filter the list before you render it. Remove drafts and anything with a date in the future, because an item dated tomorrow may be hidden by one reader and shared at the wrong time by another, and a draft that leaks into the feed is posted on social networks before it exists on your website. A single line that keeps only published posts with a date in the past is enough. If you import old posts, give them their original dates, because a feed full of items with today’s date makes an auto-poster share your whole archive in one afternoon. For the effect of old dates see the guide to feed order and backdated posts.

Summaries, images and encoding

Auto-posters build the best social posts from a feed that gives them a short summary and a picture. Write a real summary for every post instead of cutting the text automatically, because an automatic cut can end in the middle of a sentence and that is exactly what the auto-poster will copy. If a post has an image, add it as an enclosure with an absolute address, a type and a length. Nim strings are sequences of bytes, and the source files are UTF-8, so text passes through unchanged as long as every file you read is also UTF-8. Control characters that XML forbids can still arrive from pasted text, so remove them at the edge. See why special characters and emoji break feeds.

Compile, run and deploy

Compile in release mode for production and run the binary behind a reverse proxy that terminates the encrypted connection.

nim c -d:release app.nim
./app
curl -i http://localhost:5000/feed.xml

Check three things after the first deployment. The proxy must pass the request headers that the caching logic reads, so that a conditional request reaches the application. It must not store an old copy of the feed for longer than you intend, because a feed that lags behind your website delays every social post. And the address that outsiders use must be the same one that the code writes into the links, since the application cannot guess it from an internal request.

Test the feed

Request the route locally, then test the deployed address from outside, because a local test says nothing about the proxy and the certificate in front of your application.

  • The first request returns 200, the RSS content type and a body that starts with an XML declaration.
  • A request that sends back the validator returns 304 and no body.
  • Every link is absolute, uses https and points to your real domain.
  • Paste the address into the W3C Feed Validation Service and fix every error.
  • Add a post, rebuild and confirm that the new item comes first.

A longer routine is in the feed validation guide, and a shorter one in debugging a feed with curl.

Common mistakes to avoid

  • Unescaped text. One ampersand in a title breaks the whole document.
  • The clock as the channel date. The file changes on every request and caching stops working.
  • Slugs that follow titles. An edit changes the address and the post may be shared twice.
  • Dates in a local format. Use the RSS format with the English day and month names.
  • Relative links. They are broken for everyone who reads the feed away from your site.
  • Only a local test. The proxy in front of the application changes headers and caching.

How PostRSS works with a Jester feed

PostRSS has been publishing RSS and Atom feeds to social networks since 2014. Paste the address of your feed, 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 entry, 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. See the PostRSS features page and the plan limits on the pricing page. More developer guides are under the Technical Guide tag.

Kapcsolódó olvasmányok

The bottom line

A Jester feed is one route, but it has to get the details right: posts with stable slugs and dates in UTC, XML with escaped text, absolute links, a channel date taken from the newest post, the correct content type and a validator that lets clients skip unchanged files. Keep drafts and future posts out, test the live address with curl and a validator, then connect it to an auto-poster. After that, every post you publish reaches your readers and your social channels without extra work.

FAQ

How do I return XML from a Jester route?

Use the response template with a status, a list of headers that includes the RSS content type, and the XML string as the body.

Is there a Nim library for RSS?

Small packages exist, but many projects write the XML directly, as shown here. Whatever you use, escape text and format dates correctly.

How do I format dates for RSS in Nim?

Convert the time to UTC and format it with a pattern that gives the abbreviated day, the day, the abbreviated month, the year, the time and the letters GMT in quotation marks.

How do conditional requests work here?

The route sends a hash of the document as the ETag header. When the client sends the same value back, the route answers with a 304 status and no body.

Can PostRSS read a feed served by a Jester application?

Yes. PostRSS reads any valid RSS or Atom address that is reachable without a login. Paste the address, connect your channels and new posts 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.

További eszközök csapatunktól

Az Internet Solutions, a PostRSS mögött álló csapat fejlesztései. Mindegyik a maga módján spórol időt.