unsplash-logo

Edward Virvel

Outputting XML with Bootleg

The latest release of bootleg (0.1.7) contains some new XML output functionality. I added this to bootleg to facilitate the generation of an RSS news feed form the epiccastle blog page that you are reading right now. Here is how I generate this RSS.

Background

In this blog I have post metadata stored in some short yaml files. I can gather all the relevant filenames in with the glob function:

(for [filename (glob "*/vars.yml")] ...)

And then I can read these vars in with the yaml function:

(let [vars (yaml filename)] ...)

I build all these yaml structures into a single hash map keyed by the post number.

(let [posts (->> (for [filename (glob "*/vars.yml")] (let [post-num (-> filename (string/split #"/") first parse-string) vars (yaml filename)] [post-num vars])) (into {}))] ...)

Generating the RSS

I can now use this datastructure to generate an rss xml file. I do this by passing some hiccup to (convert-to hiccup-data :xml)

(spit "feed.xml" (-> [:rss {:version "2.0"} [:channel [:title "Epiccastle Blog"] [:description "Epiccastle.io Updates"] [:link "https://epiccastle.io/blog/"] [:lastBuildDate now-string] [:pubDate now-string] [:ttl "1440"] (for [[n {:keys [snake-title title rss-date] :as post-data}] (reverse (sort posts))] [:item [:title title] [:description (-> post-data render-post as-html escape-html)] [:link (str "https://epiccastle.io/blog/" snake-title)] [:gid snake-title] [:pubDate rss-date]])]] (convert-to :xml)))

Other helpful functions

I needed to escape the html markup to embed it in the xml description tag with this function:

(defn escape-html "Change special characters into HTML character entities." [text] (-> text (string/replace "&" "&") (string/replace "<" "<") (string/replace ">" ">") (string/replace "\"" """)))

And my now-string variable I generate with the following simple code at the top of my file:

(import [java.time OffsetDateTime] [java.time.format DateTimeFormatter]) (defn string->datetime [s] (OffsetDateTime/parse s DateTimeFormatter/RFC_1123_DATE_TIME)) (defn datetime->string [dt] (.format dt DateTimeFormatter/RFC_1123_DATE_TIME)) (def now (OffsetDateTime/now)) (def now-string (datetime->string now))

You can see the actual source for this process here.