RSS σε 66 κοινωνικά δίκτυα: Facebook, Instagram, X, LinkedIn, Telegram και άλλα Blog Συνεργατικό Πρόγραμμα Επικοινωνία
Σύνδεση Ξεκινήστε δωρεάν
Updated: 2026-10-24
Clojure Ring RSS Feed: Compojure Route, Dates and ETag

Short answer: To serve an RSS feed from a Clojure web application, add a Compojure GET route for /feed.xml, load the newest published rows with next.jdbc, describe the channel and items as data with clojure.data.xml so every value is escaped, format dates in UTC with DateTimeFormatter, and return the string as application/rss+xml from a Ring handler. Hash the body into an ETag and answer a matching If-None-Match header with a 304, take the last build date from the newest item so the body stays identical between changes, test the route with curl and a validator, then give the public address to an auto-poster.

Clojure treats web requests as plain data: a request is a map, a response is a map, and a handler is a function from one to the other. That model is the heart of Ring, the library nearly every Clojure web application builds on, and Compojure adds a concise routing syntax on top. Teams use the pair for APIs, internal tools and content services that run on the Java virtual machine. When such a service publishes something that people want to follow, such as articles, offers, events or release notes, a feed is the standard way to let readers, aggregators and auto-posters watch it without an account or an API key. There is no feed library to learn, because an RSS document is just nested data, and clojure.data.xml turns nested data into correctly escaped XML. This guide builds a complete Ring RSS feed with a query, stable identifiers, correct dates and conditional requests, explains each decision and ends with the checks that keep the feed dependable once other systems rely on it.

When a Clojure service needs a feed

A feed is worth adding whenever new items appear on a schedule you do not control: a changelog that grows with every release, a directory of listings, a news section or a library of guides. Readers who follow it in a feed reader get every item in order, search engines discover new pages faster and an auto-poster such as PostRSS can share each new item on social networks as soon as it is published.

If your content already lives in a blog or a CMS, use the feed that system provides; see how to find your WordPress feed link. If you work on the Java platform with a different style, the Ktor RSS feed guide και the Spring Boot guide show other approaches. Build the route yourself when the content is in your own database and no ready-made feed exists.

Set up the project

Create a deps.edn file that lists the libraries the example uses. The version numbers below were current when this guide was written; check the project pages for newer releases before you copy them.

{:paths ["src"]
 :deps {ring/ring-core {:mvn/version "1.12.2"}
        ring/ring-jetty-adapter {:mvn/version "1.12.2"}
        compojure/compojure {:mvn/version "1.7.1"}
        org.clojure/data.xml {:mvn/version "0.0.8"}
        com.github.seancorfield/next.jdbc {:mvn/version "1.3.939"}
        org.xerial/sqlite-jdbc {:mvn/version "3.46.0.0"}}
 :aliases {:run {:main-opts ["-m" "feed.core"]}}}

Assume a table named articles with the columns slug, title, summary and published_at, where the last one holds a UTC unix timestamp. Items scheduled for the future simply have a timestamp that has not arrived yet.

The complete application

The namespace below holds the query, the document, the validator logic and the route. The condition on the publication time matters: without it, an item scheduled for tomorrow would appear in the feed today, and an auto-poster would share it before it exists on the website.

(ns feed.core
  (:require [clojure.data.xml :as xml]
            [clojure.string :as str]
            [compojure.core :refer [defroutes GET]]
            [next.jdbc :as jdbc]
            [next.jdbc.result-set :as rs]
            [ring.adapter.jetty :refer [run-jetty]])
  (:import (java.security MessageDigest)
           (java.time Instant ZoneOffset)
           (java.time.format DateTimeFormatter)))

(def site "https://example.com")
(def ds (jdbc/get-datasource {:dbtype "sqlite" :dbname "site.db"}))

(defn rfc822 [epoch-seconds]
  (.format DateTimeFormatter/RFC_1123_DATE_TIME
           (.atOffset (Instant/ofEpochSecond epoch-seconds) ZoneOffset/UTC)))

(defn latest-articles []
  (jdbc/execute!
   ds
   ["SELECT slug, title, summary, published_at FROM articles
     WHERE published_at <= ? ORDER BY published_at DESC LIMIT 20"
    (.getEpochSecond (Instant/now))]
   {:builder-fn rs/as-unqualified-lower-maps}))

(defn item-element [{:keys [slug title summary published_at]}]
  (let [url (str site "/articles/" slug "/")]
    (xml/element :item {}
                 (xml/element :title {} title)
                 (xml/element :link {} url)
                 (xml/element :guid {:isPermaLink "true"} url)
                 (xml/element :pubDate {} (rfc822 published_at))
                 (xml/element :description {} summary))))

(defn feed-xml [items]
  (let [newest (if (seq items) (:published_at (first items)) 0)]
    (xml/emit-str
     (xml/element :rss {:version "2.0"}
                  (apply xml/element :channel {}
                         (concat
                          [(xml/element :title {} "Example Journal")
                           (xml/element :link {} (str site "/"))
                           (xml/element :description {} "News and guides from Example Journal.")
                           (xml/element :language {} "en")
                           (xml/element :lastBuildDate {} (rfc822 newest))]
                          (map item-element items)))))))

(defn sha256-hex [^String text]
  (let [digest (.digest (MessageDigest/getInstance "SHA-256")
                        (.getBytes text "UTF-8"))]
    (apply str (map #(format "%02x" %) digest))))

(defn etag-matches? [header etag]
  (boolean
   (when header
     (some (fn [token]
             (let [value (str/replace (str/trim token) #"^W/" "")]
               (or (= value "*") (= value etag))))
           (str/split header #",")))))

(defn feed-response [request]
  (let [body (feed-xml (latest-articles))
        etag (str "\"" (subs (sha256-hex body) 0 32) "\"")
        headers {"ETag" etag "Cache-Control" "public, max-age=300"}]
    (if (etag-matches? (get-in request [:headers "if-none-match"]) etag)
      {:status 304 :headers headers :body ""}
      {:status 200
       :headers (assoc headers "Content-Type" "application/rss+xml; charset=utf-8")
       :body body})))

(defroutes app
  (GET "/feed.xml" request (feed-response request)))

(defn -main [& _]
  (run-jetty app {:port 8080 :join? true}))

Start it with clojure -M:run. Ring lowercases header names in the request map, which is why the handler reads if-none-match in lowercase. Jetty answers HEAD requests for routes that match GET when the route is declared with the GET macro from Compojure.

Why data.xml suits this job

An RSS document is a tree: a root element, one channel, a handful of fields and a list of items. The data.xml library lets you write that tree as nested function calls, and the emitter turns it into text. Because you never concatenate strings, there is no way to forget an escape: a title containing an ampersand or an angle bracket is written as an entity, and the document stays valid. Elements are ordinary values, so the item function can be tested on its own and the list of items is just a map over the rows. Check once with curl that the output starts with the XML declaration, because some validators expect it, and add it by hand if your version of the library omits it.

How the ETag logic works

Auto-posters, readers and search engines request a feed often, and almost always nothing has changed. The handler hashes the finished string, quotes the hash as an entity tag and compares it with the If-None-Match header. A match produces a 304 response with an empty body, which saves bandwidth for both sides. The hash is computed from the body, which brings one rule: the body must not change unless the content changes. That is why the document takes the last build date from the newest item and not from the clock. If you wrote the current time into that element, the hash would change on every request and no client would ever receive a 304. A compressing proxy may turn a strong tag into a weak one, so the comparison strips the W/ prefix before it compares.

Where each field comes from

Feed elementSourceRule
titleArticle titlePlain text, escaped by the emitter
linkPublic address of the pageAbsolute, with https, returning 200 directly
guidSame address, or a stored identifierNever changes after publication
pubDatePublication timestampRFC 822 in UTC, never the current time
descriptionShort summaryOne or two sentences of plain text
enclosureFeatured image, optionalAddress, size in bytes and media type

The identifier is the most important decision. An auto-poster remembers which items it has already shared by their GUID. If the value changes when someone edits a slug or republishes an article, the same story is shared again. If you may rename slugs, store a separate permanent identifier and write it with isPermaLink set to false.

Dates in the right format

RSS dates follow RFC 822, with English abbreviations for days and months. The formatter named RFC_1123_DATE_TIME produces that style, always with English names regardless of the server locale, and it writes the zone as GMT when the offset is zero. The stored timestamp is turned into an instant, attached to the UTC offset and formatted, so the local zone of the server never leaks in. Mixing in local time is the most common cause of feeds whose items appear hours late or early in readers.

Caching without delaying new posts

The Cache-Control header in the example lets shared caches keep the response for five minutes, and the ETag lets everyone revalidate cheaply after that. If you place a content delivery network in front of the application, keep the lifetime short so a new item does not wait long for its turn. A feed that lags behind the website by an hour defeats the purpose of automation. For the background, read how auto-posting tools use ETags and conditional requests και why slow feeds break auto-posting.

Test the feed before you connect it

Because a handler is a function from a map to a map, you can test it at the REPL without starting a server. Call (feed-response {:headers {}}), read the ETag from the result and call the function again with that value under the if-none-match key; the second answer should have status 304. Then start the service and check it from the outside:

  • Run curl -i https://example.com/feed.xml and confirm a 200 status, the right content type and an ETag header.
  • Repeat the request with -H 'If-None-Match: ...' and the tag you received, and confirm that the answer is 304.
  • Paste the public address into the W3C Feed Validation Service and fix every error.
  • Publish a test item, wait for the cache lifetime and confirm that it appears once, at the top.

There is a longer checklist in the feed validation guide and a short curl routine in the guide to debugging a feed with curl.

Running the application in production

Package the application as an uberjar and run it behind a reverse proxy that terminates HTTPS and passes the real host and scheme, so absolute links are correct. Keep the site address in configuration instead of a def, so a staging copy never publishes production links. Replace the plain datasource with a connection pool when traffic grows, and monitor the feed route separately from the home page, because a feed can fail while the rest of the site works. If the route sits behind a login or a firewall rule for unknown clients, an auto-poster will not reach it; the feed must be public.

Common mistakes to avoid

  • Current time in the body. It changes the hash on every request and disables the 304 answer.
  • Unstable GUIDs. Changing them makes auto-posters share old stories again; see why a feed auto-posts the same article twice.
  • Building XML from strings. A missing escape breaks the document; keep the feed as data until the emitter runs.
  • Future items in the feed. Always filter on the publication time.
  • Header case. Request header names are lowercase in the Ring map; reading a capitalised name returns nothing.
  • Wrong content type. A feed sent as text or HTML confuses some readers; see the content type header guide.

How PostRSS works with a Ring feed

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

Σχετική ανάγνωση

The bottom line

A Ring application needs only a handler, a query and clojure.data.xml to serve a dependable RSS feed: select published items dated in the past, keep the feed as data so every value is escaped, use a stable GUID, format dates in UTC, derive the last build date from the newest item and send the result with the right content type. Add a hash-based ETag so repeat requests end in a 304. Test the handler at the REPL and the route with curl and a validator, keep the address unchanged and connect it to an auto-poster. After that, every item you publish can reach your social channels automatically.

FAQ

Does Ring have a built-in RSS generator?

No. Ring and Compojure have no feed support, but clojure.data.xml turns nested data into a valid RSS document and escapes every value, and the handler returns it with the right content type.

Why are request headers lowercase in Ring?

Ring normalises header names to lowercase in the request map, so the handler must read if-none-match in lowercase. Response headers can use any case.

Why does my feed never return 304?

Most often the body changes between requests, for example because the last build date is the current time. Take that date from the newest item and the hash stays the same until the content changes.

How many items should the feed contain?

Between ten and twenty of the newest published items is enough. An auto-poster only needs recent items, and a smaller file is faster to download and parse.

Can I use this feed with PostRSS?

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

Περισσότερα εργαλεία από την ομάδα μας

Από την Internet Solutions, την ομάδα πίσω από το PostRSS. Κάθε προϊόν σάς εξοικονομεί χρόνο με τον δικό του τρόπο.

Live chat με AI για ιστότοπους Talkmio Ο ιστότοπός σας απαντά στους επισκέπτες 24/7 με βάση το δικό σας περιεχόμενο, στη γλώσσα τους. Δωρεάν πρόγραμμα · χωρίς κάρτα Βοηθός AI Ask Mio Συνομιλία, κώδικας, σχεδιασμός, γραφή και έρευνα. Το Mio επιλέγει το καλύτερο μοντέλο για κάθε εργασία. Δωρεάν πρόγραμμα Αυτόματος πιλότος AI για blog και social AI Blog Autopilot Η AI γράφει άρθρα SEO 2.000–3.000 λέξεων και κοινοποιεί το καθένα σε 60 κοινωνικά δίκτυα. Τα 3 πρώτα άρθρα δωρεάν Έλεγχος υγείας ιστότοπου Έλεγχος AI ιστοτόπου SEO, ταχύτητα, SSL, ασφάλεια και ρυθμίσεις email σε μία αναφορά, με σειρά προτεραιότητας. Ο πρώτος έλεγχος δωρεάν Σε βάθος ανίχνευση SEO Έλεγχος SEO AI ιστοτόπου Πλήρης ανίχνευση SEO σε 7 τομείς, μαζί με την ορατότητα στην αναζήτηση AI, με διορθώσεις ταξινομημένες κατά αντίκτυπο. Ο πρώτος έλεγχος δωρεάν RSS και ροές προϊόντων Δημιουργός RSS Feed Δημιουργήστε RSS από οποιαδήποτε σελίδα, καθώς και ροές προϊόντων για Google και Meta που ενημερώνονται μόνες τους. Δωρεάν πρόγραμμα Ανάπτυξη ιστότοπων και SEO Internet Solutions Ιστότοποι, ηλεκτρονικά καταστήματα και εξατομικευμένα συστήματα, που σχεδιάζει, κατασκευάζει και υποστηρίζει η ομάδα μας. Από το 2011