RSS 66 sotsiaalvõrgustikku: Facebook, Instagram, X, LinkedIn, Telegram ja teised Blogi Partnerprogramm Kontaktid
Logi sisse Alusta tasuta
Updated: 2026-10-06
Ktor RSS Feed in Kotlin: Generate It and Auto-Post Items

Short answer: Ktor has no feed plugin, but a Ktor RSS feed is one route. Load recent published posts, write the XML with the JDK’s built-in XMLStreamWriter (or a library such as Rome) so all text is escaped, format dates with DateTimeFormatter.RFC_1123_DATE_TIME, and answer with call.respondText(xml, ContentType.Application.Rss). Use a configured base URL for absolute links, add caching and compression plugins, and the feed is ready for readers and automatic social posting.

Ktor is JetBrains’ Kotlin framework for servers and is used for APIs, content back ends, internal tools and an increasing number of websites. Kotlin teams often start with JSON endpoints and only later notice that nothing announces new content to the outside world. An RSS feed fills that gap with very little code. Readers, aggregators, team chat integrations and social auto-posting services all understand it, so one endpoint replaces many custom integrations. This guide builds a clean feed route, covers the details that typically break feeds in Ktor projects, and shows how to use the feed for social sharing.

What the feed must deliver

A feed that serves readers and automation tools well:

  • lists the most recent public items, newest first, usually 20 to 50 of them;
  • gives each item a title, an absolute link, a short description, a publication date and a permanent guid;
  • describes the channel with a title, a link and a description;
  • is served in UTF-8 with an RSS content type and without redirects;
  • keeps IDs stable, so an old item is never reported as new.

For a reminder of what each element does, see what guid and pubDate actually do.

Choosing how to generate the XML

There are three reasonable options on the JVM:

  • JDK XMLStreamWriter (StAX). No extra dependency, escapes all text automatically and is fast. It is a little verbose, but a small helper function solves that.
  • The Rome library. Mature and widely used, supports RSS and Atom and has modules for media and other extensions. It adds a dependency and has a Java-style API.
  • String templates. Short to write, but escaping becomes your responsibility, which makes them easy to break.

This guide uses StAX because it is part of the JDK and escapes everything you write as text. If you already use Rome elsewhere, it is an equally good choice. Avoid building XML with Kotlin string templates: a single title containing an ampersand or a less-than sign makes the whole document invalid, and the bug appears only when an editor happens to type that character.

Step 1: load the posts

Whatever data layer you use, such as Exposed, jOOQ, JDBC or a repository over another service, the query should return only published posts whose publication time has passed. The second condition matters when posts are scheduled: otherwise a post meant for next week appears in the feed today.

data class Post(
    val slug: String,
    val title: String,
    val summary: String,
    val publishedAt: Instant,
)

interface PostRepository {
    suspend fun latestPublished(limit: Int): List<Post> // published and publishedAt <= now, newest first
}

Store publication times as Instant or UTC timestamps. Mixing local times and UTC is one of the most common reasons why feed items show up hours early or late.

Step 2: build the RSS document

A small helper writes the channel and items with XMLStreamWriter:

import java.io.StringWriter
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import javax.xml.stream.XMLOutputFactory
import javax.xml.stream.XMLStreamWriter

private fun XMLStreamWriter.el(name: String, text: String) {
    writeStartElement(name); writeCharacters(text); writeEndElement()
}

private fun rfc822(i: Instant): String =
    DateTimeFormatter.RFC_1123_DATE_TIME.format(i.atZone(ZoneOffset.UTC))

fun buildRss(posts: List<Post>, baseUrl: String): String {
    val out = StringWriter()
    val w = XMLOutputFactory.newInstance().createXMLStreamWriter(out)
    w.writeStartDocument("UTF-8", "1.0")
    w.writeStartElement("rss"); w.writeAttribute("version", "2.0")
    w.writeStartElement("channel")
    w.el("title", "Example Blog")
    w.el("link", "$baseUrl/")
    w.el("description", "New articles from Example Blog")
    w.el("language", "en")
    for (p in posts) {
        val url = "$baseUrl/blog/${p.slug}"
        w.writeStartElement("item")
        w.el("title", p.title)
        w.el("link", url)
        w.writeStartElement("guid"); w.writeAttribute("isPermaLink", "true")
        w.writeCharacters(url); w.writeEndElement()
        w.el("pubDate", rfc822(p.publishedAt))
        w.el("description", p.summary)
        w.writeEndElement() // item
    }
    w.writeEndElement(); w.writeEndElement(); w.writeEndDocument(); w.close()
    return out.toString()
}

RFC_1123_DATE_TIME produces dates such as Sat, 10 Oct 2026 10:45:00 GMT. That matches the RFC 822 style RSS requires, and the formatter uses fixed English day and month names, so it is not affected by the server’s locale. Avoid ISO 8601 strings in pubDate; they belong in Atom, not RSS. Our article on pubDate errors lists more pitfalls.

Step 3: the route

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun Application.feedRoutes(repo: PostRepository, baseUrl: String) {
    routing {
        get("/feed.xml") {
            val xml = buildRss(repo.latestPublished(30), baseUrl)
            call.response.header(HttpHeaders.CacheControl, "public, max-age=300")
            call.respondText(xml, ContentType.Application.Rss.withCharset(Charsets.UTF_8))
        }
    }
}

ContentType.Application.Rss is Ktor’s constant for application/rss+xml. Setting the charset explicitly avoids readers guessing the encoding. If your application uses the ContentNegotiation plugin, respondText bypasses JSON serialisation, so the XML reaches the client unchanged.

Absolute URLs and reverse proxies

The route uses a baseUrl passed in from configuration, for example a custom key in application.conf or an environment variable, instead of building URLs from the request. That is deliberate. A Ktor service usually runs behind Nginx, a load balancer or a Kubernetes ingress, and the request it receives may show an internal host, port 8080 or plain HTTP.

If you prefer request-based URLs, install the XForwardedHeaders või ForwardedHeaders plugin so call.request.origin reflects the public scheme and host, and make sure the proxy sends those headers. Either way, check the live feed once: the first item link should open the page exactly as visitors see it. Relative or internal links are a classic reason why auto-posted links fail; see relative URLs in RSS feeds.

Images, categories and full content

When the feed drives social posts, the image decides how each post looks. Add an enclosure for items with a cover image:

w.writeEmptyElement("enclosure")
w.writeAttribute("url", p.coverUrl)
w.writeAttribute("type", "image/jpeg")
w.writeAttribute("length", p.coverBytes.toString())

Many auto-posters also read the article page’s og:image tag, so set it in your HTML templates. Categories become <category> elements, one per tag, which helps route topics to different accounts and turn tags into hashtags. For full HTML content, declare the content namespace and write content:encoded with writeCData, keeping description short and plain. See how to control which image gets auto-posted.

Caching, conditional requests and compression

Feeds are polled frequently, so make repeated requests cheap. Ktor has plugins for each part:

  • Cache-Control. The header in the route lets proxies reuse the response for five minutes. The CachingHeaders plugin can set it centrally by content type.
  • Conditional requests. The ConditionalHeaders plugin answers 304 Not Modified when the client’s If-None-Match või If-Modified-Since matches the version you provide, such as an ETag built from the newest post’s slug and timestamp.
  • Compression. install(Compression) { gzip() } compresses the XML for clients that ask for it. Feeds shrink dramatically.
  • In-memory cache. For busy sites, cache the generated string and invalidate it when a post is published.

Do not cache the feed for hours. Long caches, especially at a CDN, are a frequent cause of delayed auto-posts; see RSS feed caching explained.

Feed discovery and category feeds

Add an autodiscovery link to the head of your HTML pages, so browsers, readers and tools can find the feed:

<link rel="alternate" type="application/rss+xml"
      title="Example Blog" href="https://example.com/feed.xml">

For topic feeds, add get("/category/{slug}/feed.xml"), read the parameter with call.parameters["slug"], reuse the same builder with a filtered query, and respond with HttpStatusCode.NotFound for unknown categories so mistakes are noticed immediately.

Test and validate

Ktor’s testApplication runs the module in memory:

@Test
fun feedIsRss() = testApplication {
    application { feedRoutes(FakeRepo(), "https://example.com") }
    val res = client.get("/feed.xml")
    assertEquals(HttpStatusCode.OK, res.status)
    assertTrue(res.contentType()!!.match(ContentType.Application.Rss))
    assertTrue(res.bodyAsText().contains("<link>https://example.com/blog/"))
}

Then validate the deployed feed with the W3C Feed Validation Service. The Ktor documentation on sending responses covers respondText and content types in more detail.

Common Ktor feed problems

SymptomCauseFix
Feed served as text/plainrespondText without a content typePass ContentType.Application.Rss
XML breaks on one titleString templates without escapingStAX or Rome
Links show port 8080 or httpURLs built from the internal requestConfigured base URL or forwarded headers plugin
Dates rejected or localisedISO strings or a locale-dependent patternRFC_1123_DATE_TIME in UTC
Scheduled posts appear earlyQuery ignores publication timeFilter on publishedAt up to now
Whole archive resharedChanged URL scheme or domain, so guids changedKeep guids permanent

The guid issue is the most expensive one, because it happens after a seemingly harmless change such as moving the blog to a new path. Read duplicate guid fixes before you migrate.

Error handling and monitoring

A feed endpoint fails quietly. Nobody opens /feed.xml in a browser every day, so a broken feed can go unnoticed for weeks while readers and auto-posters see errors or stale content. A few habits prevent that:

  • Fail loudly. If the database query fails, return a 500 response and log the exception through the StatusPages plugin. Returning an empty but valid channel hides the problem and makes tools think you stopped publishing.
  • Watch response times. Feed clients often give up after a few seconds. If building the feed is slow, cache it, as described above.
  • Monitor the endpoint. Lisa /feed.xml to your uptime checks and alert on non-200 responses or unusually small bodies.
  • Check after deploys. A quick curl of the feed in your deployment pipeline catches broken content types, missing items and wrong base URLs before anyone else notices.

For a structured approach to troubleshooting, see RSS feed not updating: how to diagnose and fix it.

Connect the feed to social auto-posting

You could launch a coroutine that calls each social network’s API whenever a post is published. In practice, every network has its own OAuth flow, token refresh, rate limits and media rules, and they change often. For content, the feed is the simpler contract: publish the post, let the feed announce it, and let an auto-poster handle the networks.

  • make the article page reachable before the item appears in the feed;
  • write a short summary, because it often becomes the post text;
  • attach an image of at least 1200 pixels in width;
  • keep tags consistent if you want them turned into hashtags.

How PostRSS works with a Ktor feed

PostRSS has been turning RSS and Atom feeds into social posts since 2014. You paste the feed address, connect accounts and choose the post format. PostRSS checks the feed as often as every minute and publishes each new item, with its image and link, to the networks you select among 66 supported networks, messengers, team chats and blogs, from LinkedIn and Mastodon to Telegram, Discord and Google Chat. Keyword filters, UTM parameters, hashtags from categories, posting windows, a REST API v1 and outgoing webhooks are available as well. See the PostRSS features page and the plan limits on the pricing page. More developer articles are under the Technical Guide tag.

Related reading

The bottom line

A Ktor RSS feed is a query, a small XML builder and one route. Write the XML with StAX or Rome so it is always escaped, format dates with RFC_1123_DATE_TIME in UTC, respond with ContentType.Application.Rss, and use a configured base URL for absolute links. Add caching, conditional headers and compression, keep guids permanent and validate once. Then every new post can reach your social channels automatically.

FAQ

Does Ktor have a built-in RSS plugin?

No. Ktor does not ship a feed generator. You write a route that returns XML, built with the JDK XMLStreamWriter or a library such as Rome, and respond with the RSS content type.

Which content type should a Ktor feed use?

Use ContentType.Application.Rss, which is application/rss+xml, with the UTF-8 charset. It tells browsers, readers and auto-posters exactly what the response contains.

How do I format RSS dates in Kotlin?

Convert the Instant to UTC and format it with DateTimeFormatter.RFC_1123_DATE_TIME. The result matches the RFC 822 style that RSS expects and does not depend on the server locale.

Should I use Rome instead of StAX?

Both work well. StAX needs no dependency and is enough for a standard RSS feed. Rome is convenient if you also need Atom or feed modules such as media extensions.

Can a Ktor-based blog auto-post to social media?

Yes. Once the feed is public and valid, an RSS auto-poster such as PostRSS can read it and share each new post with the networks you connect, without extra code in your Ktor application.

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.

Veel tööriistu meie meeskonnalt

Loonud Internet Solutions, PostRSS-i taga olev meeskond. Iga toode säästab aega omal moel.

PostRSS – RSS-voogude automatiseerimine ja autopostitus
Privaatsuse ülevaade

See veebisait kasutab küpsiseid, et pakkuda teile võimalikult parimat kasutajakogemust. Küpsiste teave salvestatakse teie brauserisse ja see aitab meil teid tuvastada, kui pöördute meie veebilehe juurde tagasi, ning võimaldab meie meeskonnal mõista, millised veebilehe osad on teie jaoks kõige huvitavamad ja kasulikumad.